Browse Source

refactor: rename project and base

- Rename `Project`  => `Base`
- Rename `Base` => `Source`
- Remove `db` from data/meta api endpoints
- Add backward compatibility for old apis
- Migrations for renaming table and columns

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/6545/head
Pranav C 1 year ago
parent
commit
e790abdbaf
  1. 16
      packages/nc-gui/assets/nc-icons/project.svg
  2. 12
      packages/nc-gui/assets/nc-icons/record.svg
  3. 14
      packages/nc-gui/assets/nc-icons/table.svg
  4. 2
      packages/nc-gui/assets/style.scss
  5. 2
      packages/nc-gui/components/account/UsersModal.vue
  6. 6
      packages/nc-gui/components/cell/Checkbox.vue
  7. 8
      packages/nc-gui/components/cell/DateTimePicker.vue
  8. 10
      packages/nc-gui/components/cell/MultiSelect.vue
  9. 8
      packages/nc-gui/components/cell/SingleSelect.vue
  10. 3
      packages/nc-gui/components/cell/Text.vue
  11. 5
      packages/nc-gui/components/cell/TextArea.vue
  12. 6
      packages/nc-gui/components/cell/TimePicker.vue
  13. 6
      packages/nc-gui/components/cell/attachment/utils.ts
  14. 2
      packages/nc-gui/components/dashboard/Sidebar.vue
  15. 6
      packages/nc-gui/components/dashboard/Sidebar/TopSection.vue
  16. 98
      packages/nc-gui/components/dashboard/TreeView/AddNewTableNode.vue
  17. 30
      packages/nc-gui/components/dashboard/TreeView/BaseOptions.vue
  18. 4
      packages/nc-gui/components/dashboard/TreeView/CreateViewBtn.vue
  19. 329
      packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
  20. 14
      packages/nc-gui/components/dashboard/TreeView/ProjectWrapper.vue
  21. 54
      packages/nc-gui/components/dashboard/TreeView/TableList.vue
  22. 62
      packages/nc-gui/components/dashboard/TreeView/TableNode.vue
  23. 18
      packages/nc-gui/components/dashboard/TreeView/ViewsList.vue
  24. 8
      packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue
  25. 60
      packages/nc-gui/components/dashboard/TreeView/index.vue
  26. 10
      packages/nc-gui/components/dashboard/settings/AuditTab.vue
  27. 16
      packages/nc-gui/components/dashboard/settings/BaseAudit.vue
  28. 122
      packages/nc-gui/components/dashboard/settings/DataSources.vue
  29. 4
      packages/nc-gui/components/dashboard/settings/Erd.vue
  30. 20
      packages/nc-gui/components/dashboard/settings/Metadata.vue
  31. 26
      packages/nc-gui/components/dashboard/settings/Misc.vue
  32. 16
      packages/nc-gui/components/dashboard/settings/Modal.vue
  33. 27
      packages/nc-gui/components/dashboard/settings/UIAcl.vue
  34. 14
      packages/nc-gui/components/dashboard/settings/UIAclTabs.vue
  35. 53
      packages/nc-gui/components/dashboard/settings/data-sources/CreateBase.vue
  36. 42
      packages/nc-gui/components/dashboard/settings/data-sources/EditBase.vue
  37. 22
      packages/nc-gui/components/dlg/AirtableImport.vue
  38. 24
      packages/nc-gui/components/dlg/ProjectDelete.vue
  39. 18
      packages/nc-gui/components/dlg/ProjectDuplicate.vue
  40. 12
      packages/nc-gui/components/dlg/QuickImport.vue
  41. 32
      packages/nc-gui/components/dlg/TableCreate.vue
  42. 20
      packages/nc-gui/components/dlg/TableDelete.vue
  43. 2
      packages/nc-gui/components/dlg/TableDuplicate.vue
  44. 28
      packages/nc-gui/components/dlg/TableRename.vue
  45. 6
      packages/nc-gui/components/dlg/share-and-collaborate/Collaborate.vue
  46. 4
      packages/nc-gui/components/dlg/share-and-collaborate/ManageUsers.vue
  47. 38
      packages/nc-gui/components/dlg/share-and-collaborate/ShareBase.vue
  48. 2
      packages/nc-gui/components/dlg/share-and-collaborate/SharePage.vue
  49. 6
      packages/nc-gui/components/dlg/share-and-collaborate/ShareProject.vue
  50. 20
      packages/nc-gui/components/dlg/share-and-collaborate/View.vue
  51. 16
      packages/nc-gui/components/erd/View.vue
  52. 4
      packages/nc-gui/components/general/AddBaseButton.vue
  53. 10
      packages/nc-gui/components/general/HelpAndSupport.vue
  54. 18
      packages/nc-gui/components/general/MiniSidebar.vue
  55. 10
      packages/nc-gui/components/general/ProjectIcon.vue
  56. 6
      packages/nc-gui/components/general/ShareProject.vue
  57. 1
      packages/nc-gui/components/general/TableIcon.vue
  58. 23
      packages/nc-gui/components/general/UserIcon.vue
  59. 4
      packages/nc-gui/components/general/language/Menu.vue
  60. 4
      packages/nc-gui/components/notification/Item/ProjectEvent.vue
  61. 4
      packages/nc-gui/components/notification/Item/ProjectInvite.vue
  62. 2
      packages/nc-gui/components/notification/Item/SharedViewEvent.vue
  63. 2
      packages/nc-gui/components/notification/Item/TableEvent.vue
  64. 2
      packages/nc-gui/components/notification/Item/ViewEvent.vue
  65. 4
      packages/nc-gui/components/profile/overview/contributionActivity.vue
  66. 16
      packages/nc-gui/components/project/AccessSettings.vue
  67. 50
      packages/nc-gui/components/project/AllTables.vue
  68. 28
      packages/nc-gui/components/project/ImportModal.vue
  69. 20
      packages/nc-gui/components/project/View.vue
  70. 4
      packages/nc-gui/components/shared-view/Grid.vue
  71. 10
      packages/nc-gui/components/smartsheet/ApiSnippet.vue
  72. 6
      packages/nc-gui/components/smartsheet/Cell.vue
  73. 4
      packages/nc-gui/components/smartsheet/Gallery.vue
  74. 4
      packages/nc-gui/components/smartsheet/Kanban.vue
  75. 6
      packages/nc-gui/components/smartsheet/column/AdvancedOptions.vue
  76. 8
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  77. 10
      packages/nc-gui/components/smartsheet/column/LinkedToAnotherRecordOptions.vue
  78. 8
      packages/nc-gui/components/smartsheet/column/LookupOptions.vue
  79. 8
      packages/nc-gui/components/smartsheet/column/RollupOptions.vue
  80. 4
      packages/nc-gui/components/smartsheet/column/SelectOptions.vue
  81. 10
      packages/nc-gui/components/smartsheet/details/Api.vue
  82. 2
      packages/nc-gui/components/smartsheet/details/Erd.vue
  83. 23
      packages/nc-gui/components/smartsheet/expanded-form/Comments.vue
  84. 111
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  85. 14
      packages/nc-gui/components/smartsheet/grid/Table.vue
  86. 6
      packages/nc-gui/components/smartsheet/header/CellIcon.ts
  87. 2
      packages/nc-gui/components/smartsheet/header/DeleteColumnModal.vue
  88. 2
      packages/nc-gui/components/smartsheet/toolbar/Erd.vue
  89. 12
      packages/nc-gui/components/smartsheet/toolbar/ExportSubActions.vue
  90. 6
      packages/nc-gui/components/smartsheet/toolbar/FilterInput.vue
  91. 6
      packages/nc-gui/components/smartsheet/toolbar/MoreActions.vue
  92. 4
      packages/nc-gui/components/smartsheet/toolbar/QrScannerButton.vue
  93. 2
      packages/nc-gui/components/smartsheet/toolbar/RowHeight.vue
  94. 8
      packages/nc-gui/components/smartsheet/toolbar/ShareView.vue
  95. 16
      packages/nc-gui/components/smartsheet/toolbar/ViewActions.vue
  96. 8
      packages/nc-gui/components/smartsheet/toolbar/ViewInfo.vue
  97. 4
      packages/nc-gui/components/tabs/Smartsheet.vue
  98. 18
      packages/nc-gui/components/tabs/auth/ApiTokenManagement.vue
  99. 34
      packages/nc-gui/components/tabs/auth/UserManagement.vue
  100. 50
      packages/nc-gui/components/tabs/auth/user-management/ShareBase.vue
  101. Some files were not shown because too many files have changed in this diff Show More

16
packages/nc-gui/assets/nc-icons/project.svg

@ -1,9 +1,9 @@
<svg width="16" height="16" viewBox="0 0 1073 1073" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_1749_80944" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="94" y="40" width="885" height="993"> <path fill-rule="evenodd" clip-rule="evenodd" d="M8.99294 14.8095L14.548 12.28C14.8177 12.1572 14.9569 11.9975 14.9659 11.8367H1C1.00896 11.9975 1.14826 12.1572 1.4179 12.28L6.97294 14.8095C7.53075 15.0635 8.43514 15.0635 8.99294 14.8095Z" fill="#142966"/>
<path d="M978.723 40H94V1033H978.723V40Z" fill="white"/> <path d="M14.9999 9.27893H1.00513V11.8367H14.9999V9.27893Z" fill="#142966"/>
</mask> <path fill-rule="evenodd" clip-rule="evenodd" d="M8.99294 12.2518L14.548 9.72223C14.8177 9.59946 14.9569 9.4398 14.9659 9.27893H1C1.00896 9.4398 1.14826 9.59946 1.4179 9.72223L6.97294 12.2518C7.53075 12.5058 8.43514 12.5058 8.99294 12.2518Z" fill="#142966"/>
<g mask="url(#mask0_1749_80944)"> <path d="M14.9999 6.72107H1.00513V9.27885H14.9999V6.72107Z" fill="#142966"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M638.951 291.265L936.342 462.949C966.129 480.145 980.256 502.958 978.723 525.482V774.266C980.256 796.789 966.129 819.602 936.342 836.798L638.951 1008.48C582.292 1041.19 490.431 1041.19 433.773 1008.48L136.381 836.798C106.595 819.602 92.4675 796.789 93.9999 774.266L93.9999 525.482C92.4675 502.957 106.595 480.145 136.381 462.949L433.773 291.265C490.431 258.556 582.292 258.556 638.951 291.265Z" fill="#142966"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M8.99294 10.9729L14.548 8.44332C14.8177 8.32054 14.9569 8.16088 14.9659 8H1C1.00896 8.16088 1.14826 8.32054 1.4179 8.44332L6.97294 10.9729C7.53075 11.2269 8.43514 11.2269 8.99294 10.9729Z" fill="#36BFFF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M638.951 65.0055L936.342 236.69C966.129 253.886 980.256 276.699 978.723 299.222V548.006C980.256 570.529 966.129 593.343 936.342 610.538L638.951 782.223C582.292 814.931 490.431 814.931 433.773 782.223L136.381 610.538C106.595 593.343 92.4675 570.529 93.9999 548.006L93.9999 299.222C92.4675 276.699 106.595 253.886 136.381 236.69L433.773 65.0055C490.431 32.2968 582.292 32.2968 638.951 65.0055Z" fill="#36BFFF"/> <path d="M14.9997 4.16309H1.00491V8.00309H14.9997V4.16309Z" fill="#36BFFF"/>
</g> <path d="M14.5484 4.63991L8.99337 7.16947C8.43561 7.42348 7.53121 7.42348 6.9734 7.16947L1.41836 4.63991C0.860546 4.3859 0.860546 3.97408 1.41836 3.72007L6.9734 1.19051C7.53121 0.936498 8.43561 0.936498 8.99337 1.19051L14.5484 3.72007C15.1063 3.97408 15.1063 4.3859 14.5484 4.63991Z" fill="#36BFFF"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

12
packages/nc-gui/assets/nc-icons/record.svg

@ -1,12 +0,0 @@
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1409_68546)">
<path d="M12.3571 5.96842L4.64282 10.4219" stroke="#1F293A" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="1.15184" width="9.06208" height="9.06208" rx="1.335" transform="matrix(0.866044 -0.499967 0.866044 0.499967 -0.345705 8.77119)" stroke="#1F293A" stroke-width="1.33"/>
<path d="M4 6.33984L11.7143 10.7933" stroke="#1F293A" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_1409_68546">
<rect width="16" height="16" fill="white" transform="translate(0.5)"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 712 B

14
packages/nc-gui/assets/nc-icons/table.svg

@ -1,12 +1,12 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1613_80692)"> <g clip-path="url(#clip0_1409_68546)">
<path d="M11.8571 5.96903L4.14285 10.4225" stroke="#374151" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/> <path d="M12.3571 5.96842L4.64282 10.4219" stroke="#1F293A" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="1.15184" width="9.06208" height="9.06208" rx="1.335" transform="matrix(0.866044 -0.499967 0.866044 0.499967 -0.845705 8.77156)" stroke="#374151" stroke-width="1.33"/> <rect x="1.15184" width="9.06208" height="9.06208" rx="1.335" transform="matrix(0.866044 -0.499967 0.866044 0.499967 -0.345705 8.77119)" stroke="#1F293A" stroke-width="1.33"/>
<path d="M3.5 6.34009L11.2143 10.7935" stroke="#374151" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/> <path d="M4 6.33984L11.7143 10.7933" stroke="#1F293A" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</g> </g>
<defs> <defs>
<clipPath id="clip0_1613_80692"> <clipPath id="clip0_1409_68546">
<rect width="16" height="16" fill="white"/> <rect width="16" height="16" fill="white" transform="translate(0.5)"/>
</clipPath> </clipPath>
</defs> </defs>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 686 B

After

Width:  |  Height:  |  Size: 712 B

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

@ -193,7 +193,7 @@ a {
} }
} }
.nc-project-menu-item { .nc-base-menu-item {
@apply cursor-pointer flex items-center gap-2 py-2 after:(content-[''] absolute top-0 left-0 bottom-0 right-0 w-full h-full bg-current opacity-0 transition transition-opacity duration-100) hover:(after:(opacity-5)); @apply cursor-pointer flex items-center gap-2 py-2 after:(content-[''] absolute top-0 left-0 bottom-0 right-0 w-full h-full bg-current opacity-0 transition transition-opacity duration-100) hover:(after:(opacity-5));
// &:hover { // &:hover {

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

@ -79,7 +79,7 @@ const copyUrl = async () => {
try { try {
await copy(inviteUrl.value) await copy(inviteUrl.value)
// Copied shareable base url to clipboard! // Copied shareable source url to clipboard!
message.success(t('msg.success.shareableURLCopied')) message.success(t('msg.success.shareableURLCopied'))
} catch (e: any) { } catch (e: any) {
message.error(e.message) message.error(e.message)

6
packages/nc-gui/components/cell/Checkbox.vue

@ -8,7 +8,7 @@ import {
getMdiIcon, getMdiIcon,
inject, inject,
parseProp, parseProp,
useProject, useBase,
useSelectedCellKeyupListener, useSelectedCellKeyupListener,
} from '#imports' } from '#imports'
@ -28,7 +28,7 @@ const emits = defineEmits<Emits>()
const active = inject(ActiveCellInj, ref(false)) const active = inject(ActiveCellInj, ref(false))
const { isMssql } = useProject() const { isMssql } = useBase()
const column = inject(ColumnInj) const column = inject(ColumnInj)
@ -53,7 +53,7 @@ const checkboxMeta = computed(() => {
const vModel = computed<boolean | number>({ const vModel = computed<boolean | number>({
get: () => !!props.modelValue && props.modelValue !== '0' && props.modelValue !== 0 && props.modelValue !== 'false', get: () => !!props.modelValue && props.modelValue !== '0' && props.modelValue !== 0 && props.modelValue !== 'false',
set: (val: any) => emits('update:modelValue', isMssql(column?.value?.base_id) ? +val : val), set: (val: any) => emits('update:modelValue', isMssql(column?.value?.source_id) ? +val : val),
}) })
function onClick(force?: boolean, event?: MouseEvent) { function onClick(force?: boolean, event?: MouseEvent) {

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

@ -13,7 +13,7 @@ import {
parseProp, parseProp,
ref, ref,
timeFormats, timeFormats,
useProject, useBase,
useSelectedCellKeyupListener, useSelectedCellKeyupListener,
watch, watch,
} from '#imports' } from '#imports'
@ -28,7 +28,7 @@ const { modelValue, isPk, isUpdatedFromCopyNPaste } = defineProps<Props>()
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const { isMssql, isXcdbBase } = useProject() const { isMssql, isXcdbBase } = useBase()
const { showNull } = useGlobal() const { showNull } = useGlobal()
@ -67,7 +67,7 @@ const localState = computed({
return undefined return undefined
} }
const isXcDB = isXcdbBase(column.value.base_id) const isXcDB = isXcdbBase(column.value.source_id)
// cater copy and paste // cater copy and paste
// when copying a datetime cell, the copied value would be local time // when copying a datetime cell, the copied value would be local time
@ -83,7 +83,7 @@ const localState = computed({
return /^\d+$/.test(modelValue) ? dayjs(+modelValue) : dayjs(modelValue) return /^\d+$/.test(modelValue) ? dayjs(+modelValue) : dayjs(modelValue)
} }
if (isMssql(column.value.base_id)) { if (isMssql(column.value.source_id)) {
// e.g. 2023-04-29T11:41:53.000Z // e.g. 2023-04-29T11:41:53.000Z
return dayjs(modelValue) return dayjs(modelValue)
} }

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

@ -25,7 +25,7 @@ import {
ref, ref,
useEventListener, useEventListener,
useMetas, useMetas,
useProject, useBase,
useRoles, useRoles,
useSelectedCellKeyupListener, useSelectedCellKeyupListener,
watch, watch,
@ -81,7 +81,7 @@ const { getMeta } = useMetas()
const { isUIAllowed } = useRoles() const { isUIAllowed } = useRoles()
const { isPg, isMysql } = useProject() const { isPg, isMysql } = useBase()
// a variable to keep newly created options value // a variable to keep newly created options value
// temporary until it's add the option to column meta // temporary until it's add the option to column meta
@ -133,7 +133,7 @@ const vModel = computed({
const selectedTitles = computed(() => const selectedTitles = computed(() =>
modelValue modelValue
? typeof modelValue === 'string' ? typeof modelValue === 'string'
? isMysql(column.value.base_id) ? isMysql(column.value.source_id)
? modelValue.split(',').sort((a, b) => { ? modelValue.split(',').sort((a, b) => {
const opa = options.value.find((el) => el.title === a) const opa = options.value.find((el) => el.title === a)
const opb = options.value.find((el) => el.title === b) const opb = options.value.find((el) => el.title === b)
@ -247,7 +247,7 @@ async function addIfMissingAndSave() {
// todo: refactor and avoid repetition // todo: refactor and avoid repetition
if (updatedColMeta.cdf) { if (updatedColMeta.cdf) {
// Postgres returns default value wrapped with single quotes & casted with type so we have to get value between single quotes to keep it unified for all databases // Postgres returns default value wrapped with single quotes & casted with type so we have to get value between single quotes to keep it unified for all databases
if (isPg(column.value.base_id)) { if (isPg(column.value.source_id)) {
updatedColMeta.cdf = updatedColMeta.cdf.substring( updatedColMeta.cdf = updatedColMeta.cdf.substring(
updatedColMeta.cdf.indexOf(`'`) + 1, updatedColMeta.cdf.indexOf(`'`) + 1,
updatedColMeta.cdf.lastIndexOf(`'`), updatedColMeta.cdf.lastIndexOf(`'`),
@ -255,7 +255,7 @@ async function addIfMissingAndSave() {
} }
// Mysql escapes single quotes with backslash so we keep quotes but others have to unescaped // Mysql escapes single quotes with backslash so we keep quotes but others have to unescaped
if (!isMysql(column.value.base_id)) { if (!isMysql(column.value.source_id)) {
updatedColMeta.cdf = updatedColMeta.cdf.replace(/''/g, "'") updatedColMeta.cdf = updatedColMeta.cdf.replace(/''/g, "'")
} }
} }

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

@ -21,7 +21,7 @@ import {
isDrawerOrModalExist, isDrawerOrModalExist,
ref, ref,
useEventListener, useEventListener,
useProject, useBase,
useRoles, useRoles,
useSelectedCellKeyupListener, useSelectedCellKeyupListener,
watch, watch,
@ -71,7 +71,7 @@ const { getMeta } = useMetas()
const { isUIAllowed } = useRoles() const { isUIAllowed } = useRoles()
const { isPg, isMysql } = useProject() const { isPg, isMysql } = useBase()
// a variable to keep newly created option value // a variable to keep newly created option value
// temporary until it's add the option to column meta // temporary until it's add the option to column meta
@ -175,7 +175,7 @@ async function addIfMissingAndSave() {
// todo: refactor and avoid repetition // todo: refactor and avoid repetition
if (updatedColMeta.cdf) { if (updatedColMeta.cdf) {
// Postgres returns default value wrapped with single quotes & casted with type so we have to get value between single quotes to keep it unified for all databases // Postgres returns default value wrapped with single quotes & casted with type so we have to get value between single quotes to keep it unified for all databases
if (isPg(column.value.base_id)) { if (isPg(column.value.source_id)) {
updatedColMeta.cdf = updatedColMeta.cdf.substring( updatedColMeta.cdf = updatedColMeta.cdf.substring(
updatedColMeta.cdf.indexOf(`'`) + 1, updatedColMeta.cdf.indexOf(`'`) + 1,
updatedColMeta.cdf.lastIndexOf(`'`), updatedColMeta.cdf.lastIndexOf(`'`),
@ -183,7 +183,7 @@ async function addIfMissingAndSave() {
} }
// Mysql escapes single quotes with backslash so we keep quotes but others have to unescaped // Mysql escapes single quotes with backslash so we keep quotes but others have to unescaped
if (!isMysql(column.value.base_id)) { if (!isMysql(column.value.source_id)) {
updatedColMeta.cdf = updatedColMeta.cdf.replace(/''/g, "'") updatedColMeta.cdf = updatedColMeta.cdf.replace(/''/g, "'")
} }
} }

3
packages/nc-gui/components/cell/Text.vue

@ -34,9 +34,6 @@ const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value
v-model="vModel" v-model="vModel"
class="h-full w-full outline-none p-2 bg-transparent" class="h-full w-full outline-none p-2 bg-transparent"
:placeholder="isEditColumn ? $t('labels.optional') : ''" :placeholder="isEditColumn ? $t('labels.optional') : ''"
:class="{
'px-1': isExpandedFormOpen,
}"
@blur="editEnabled = false" @blur="editEnabled = false"
@keydown.down.stop @keydown.down.stop
@keydown.left.stop @keydown.left.stop

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

@ -86,7 +86,6 @@ onClickOutside(inputWrapperRef, (e) => {
:class="{ :class="{
'p-2': editEnabled, 'p-2': editEnabled,
'py-1 h-full': isForm, 'py-1 h-full': isForm,
'px-1': isExpandedFormOpen,
}" }"
:style="{ :style="{
minHeight: `${height}px`, minHeight: `${height}px`,
@ -113,8 +112,8 @@ onClickOutside(inputWrapperRef, (e) => {
<span v-else>{{ vModel }}</span> <span v-else>{{ vModel }}</span>
<div <div
v-if="active && !isExpandedFormOpen" v-if="active"
class="!absolute right-0 bottom-0 h-6 w-5 group cursor-pointer flex justify-end gap-1 items-center rounded border-none p-1 hover:(bg-primary bg-opacity-10) dark:(!bg-slate-500)" 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)"
:class="{ 'right-2 bottom-2': editEnabled }" :class="{ 'right-2 bottom-2': editEnabled }"
data-testid="attachment-cell-file-picker-button" data-testid="attachment-cell-file-picker-button"
@click.stop="isVisible = !isVisible" @click.stop="isVisible = !isVisible"

6
packages/nc-gui/components/cell/TimePicker.vue

@ -6,7 +6,7 @@ import {
ReadonlyInj, ReadonlyInj,
inject, inject,
onClickOutside, onClickOutside,
useProject, useBase,
useSelectedCellKeyupListener, useSelectedCellKeyupListener,
watch, watch,
} from '#imports' } from '#imports'
@ -20,7 +20,7 @@ const { modelValue, isPk } = defineProps<Props>()
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const { isMysql } = useProject() const { isMysql } = useBase()
const { showNull } = useGlobal() const { showNull } = useGlobal()
@ -36,7 +36,7 @@ const column = inject(ColumnInj)!
const isTimeInvalid = ref(false) const isTimeInvalid = ref(false)
const dateFormat = isMysql(column.value.base_id) ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ' const dateFormat = isMysql(column.value.source_id) ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ'
const { t } = useI18n() const { t } = useI18n()

6
packages/nc-gui/components/cell/attachment/utils.ts

@ -20,7 +20,7 @@ import {
useFileDialog, useFileDialog,
useI18n, useI18n,
useInjectionState, useInjectionState,
useProject, useBase,
watch, watch,
} from '#imports' } from '#imports'
import MdiPdfBox from '~icons/mdi/pdf-box' import MdiPdfBox from '~icons/mdi/pdf-box'
@ -55,7 +55,7 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
/** for image carousel */ /** for image carousel */
const selectedImage = ref() const selectedImage = ref()
const { project } = storeToRefs(useProject()) const { base } = storeToRefs(useBase())
const { api, isLoading } = useApi() const { api, isLoading } = useApi()
@ -191,7 +191,7 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
try { try {
const data = await api.storage.upload( const data = await api.storage.upload(
{ {
path: [NOCO, project.value.title, meta.value?.title, column.value?.title].join('/'), path: [NOCO, base.value.title, meta.value?.title, column.value?.title].join('/'),
}, },
{ {
files, files,

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

@ -3,7 +3,7 @@ const workspaceStore = useWorkspace()
const { isWorkspaceLoading } = storeToRefs(workspaceStore) const { isWorkspaceLoading } = storeToRefs(workspaceStore)
const { isSharedBase } = storeToRefs(useProject()) const { isSharedBase } = storeToRefs(useBase())
const { isMobileMode } = useGlobal() const { isMobileMode } = useGlobal()

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

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
const workspaceStore = useWorkspace() const workspaceStore = useWorkspace()
const projectStore = useProject() const baseStore = useBase()
const { isUIAllowed } = useRoles() const { isUIAllowed } = useRoles()
@ -10,7 +10,7 @@ const { isWorkspaceLoading, isWorkspaceSettingsPageOpened } = storeToRefs(worksp
const { navigateToWorkspaceSettings } = workspaceStore const { navigateToWorkspaceSettings } = workspaceStore
const { isSharedBase } = storeToRefs(projectStore) const { isSharedBase } = storeToRefs(baseStore)
const isCreateProjectOpen = ref(false) const isCreateProjectOpen = ref(false)
@ -70,7 +70,7 @@ const navigateToSettings = () => {
modal modal
type="text" type="text"
class="nc-sidebar-top-button !hover:bg-gray-200 !xs:hidden" class="nc-sidebar-top-button !hover:bg-gray-200 !xs:hidden"
data-testid="nc-sidebar-create-project-btn" data-testid="nc-sidebar-create-base-btn"
> >
<div class="gap-x-2 flex flex-row w-full items-center !font-normal"> <div class="gap-x-2 flex flex-row w-full items-center !font-normal">
<GeneralIcon icon="plus" /> <GeneralIcon icon="plus" />

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

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ProjectType } from 'nocodb-sdk' import type { BaseType } from 'nocodb-sdk'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { toRef } from '@vue/reactivity' import { toRef } from '@vue/reactivity'
import { resolveComponent } from '@vue/runtime-core' import { resolveComponent } from '@vue/runtime-core'
@ -8,11 +8,11 @@ import { ProjectRoleInj, useDialog, useRoles } from '#imports'
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
project: ProjectType base: BaseType
baseIndex?: number sourceIndex?: number
}>(), }>(),
{ {
baseIndex: 0, sourceIndex: 0,
}, },
) )
@ -22,18 +22,18 @@ const emit = defineEmits<{
const { isUIAllowed } = useRoles() const { isUIAllowed } = useRoles()
const project = toRef(props, 'project') const base = toRef(props, 'base')
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
const projectStore = useProject() const baseStore = useBase()
const { isSharedBase } = storeToRefs(projectStore) const { isSharedBase } = storeToRefs(baseStore)
const projectRole = inject(ProjectRoleInj) const baseRole = inject(ProjectRoleInj)
function openSchemaMagicDialog(baseId?: string) { function openSchemaMagicDialog(sourceId?: string) {
if (!baseId) return if (!sourceId) return
$e('c:table:create:navdraw') $e('c:table:create:navdraw')
@ -41,7 +41,7 @@ function openSchemaMagicDialog(baseId?: string) {
const { close } = useDialog(resolveComponent('DlgSchemaMagic'), { const { close } = useDialog(resolveComponent('DlgSchemaMagic'), {
'modelValue': isOpen, 'modelValue': isOpen,
'baseId': baseId, 'sourceId': sourceId,
'onUpdate:modelValue': closeDialog, 'onUpdate:modelValue': closeDialog,
}) })
@ -52,8 +52,8 @@ function openSchemaMagicDialog(baseId?: string) {
} }
} }
function openQuickImportDialog(type: string, baseId?: string) { function openQuickImportDialog(type: string, sourceId?: string) {
if (!baseId) return if (!sourceId) return
$e(`a:actions:import-${type}`) $e(`a:actions:import-${type}`)
@ -62,7 +62,7 @@ function openQuickImportDialog(type: string, baseId?: string) {
const { close } = useDialog(resolveComponent('DlgQuickImport'), { const { close } = useDialog(resolveComponent('DlgQuickImport'), {
'modelValue': isOpen, 'modelValue': isOpen,
'importType': type, 'importType': type,
'baseId': baseId, 'sourceId': sourceId,
'onUpdate:modelValue': closeDialog, 'onUpdate:modelValue': closeDialog,
}) })
@ -73,8 +73,8 @@ function openQuickImportDialog(type: string, baseId?: string) {
} }
} }
function openAirtableImportDialog(baseId?: string) { function openAirtableImportDialog(sourceId?: string) {
if (!baseId) return if (!sourceId) return
$e('a:actions:import-airtable') $e('a:actions:import-airtable')
@ -82,7 +82,7 @@ function openAirtableImportDialog(baseId?: string) {
const { close } = useDialog(resolveComponent('DlgAirtableImport'), { const { close } = useDialog(resolveComponent('DlgAirtableImport'), {
'modelValue': isOpen, 'modelValue': isOpen,
'baseId': baseId, 'sourceId': sourceId,
'onUpdate:modelValue': closeDialog, 'onUpdate:modelValue': closeDialog,
}) })
@ -93,8 +93,8 @@ function openAirtableImportDialog(baseId?: string) {
} }
} }
function openTableCreateMagicDialog(baseId?: string) { function openTableCreateMagicDialog(sourceId?: string) {
if (!baseId) return if (!sourceId) return
$e('c:table:create:navdraw') $e('c:table:create:navdraw')
@ -102,7 +102,7 @@ function openTableCreateMagicDialog(baseId?: string) {
const { close } = useDialog(resolveComponent('DlgTableMagic'), { const { close } = useDialog(resolveComponent('DlgTableMagic'), {
'modelValue': isOpen, 'modelValue': isOpen,
'baseId': baseId, 'sourceId': sourceId,
'onUpdate:modelValue': closeDialog, 'onUpdate:modelValue': closeDialog,
}) })
@ -116,7 +116,7 @@ function openTableCreateMagicDialog(baseId?: string) {
<template> <template>
<div <div
v-if="isUIAllowed('tableCreate', { roles: projectRole })" v-if="isUIAllowed('tableCreate', { roles: baseRole })"
class="group flex items-center gap-2 pl-2 pr-4.75 py-1 text-primary/70 hover:(text-primary/100) cursor-pointer select-none" class="group flex items-center gap-2 pl-2 pr-4.75 py-1 text-primary/70 hover:(text-primary/100) cursor-pointer select-none"
@click="emit('openTableCreateDialog')" @click="emit('openTableCreateDialog')"
> >
@ -139,14 +139,14 @@ function openTableCreateMagicDialog(baseId?: string) {
<GeneralIcon icon="magic" class="ml-1 text-orange-400" /> <GeneralIcon icon="magic" class="ml-1 text-orange-400" />
</div> </div>
</template> </template>
<a-menu-item key="table-magic" @click="openTableCreateMagicDialog(project.bases[baseIndex].id)"> <a-menu-item key="table-magic" @click="openTableCreateMagicDialog(base.sources[sourceIndex].id)">
<div class="color-transition nc-project-menu-item group"> <div class="color-transition nc-base-menu-item group">
<GeneralIcon icon="magic1" class="group-hover:text-accent" /> <GeneralIcon icon="magic1" class="group-hover:text-accent" />
Create table Create table
</div> </div>
</a-menu-item> </a-menu-item>
<a-menu-item key="schema-magic" @click="openSchemaMagicDialog(project.bases[baseIndex].id)"> <a-menu-item key="schema-magic" @click="openSchemaMagicDialog(base.sources[sourceIndex].id)">
<div class="color-transition nc-project-menu-item group"> <div class="color-transition nc-base-menu-item group">
<GeneralIcon icon="magic1" class="group-hover:text-accent" /> <GeneralIcon icon="magic1" class="group-hover:text-accent" />
Create schema Create schema
</div> </div>
@ -158,44 +158,44 @@ function openTableCreateMagicDialog(baseId?: string) {
<!-- Quick Import From --> <!-- Quick Import From -->
<a-menu-item-group :title="$t('title.quickImportFrom')" class="!px-0 !mx-0"> <a-menu-item-group :title="$t('title.quickImportFrom')" class="!px-0 !mx-0">
<a-menu-item <a-menu-item
v-if="isUIAllowed('airtableImport', { roles: projectRole })" v-if="isUIAllowed('airtableImport', { roles: baseRole })"
key="quick-import-airtable" key="quick-import-airtable"
@click="openAirtableImportDialog(project.bases[baseIndex].id)" @click="openAirtableImportDialog(base.sources[sourceIndex].id)"
> >
<div class="color-transition nc-project-menu-item group"> <div class="color-transition nc-base-menu-item group">
<GeneralIcon icon="airtable" class="group-hover:text-accent" /> <GeneralIcon icon="airtable" class="group-hover:text-accent" />
Airtable Airtable
</div> </div>
</a-menu-item> </a-menu-item>
<a-menu-item <a-menu-item
v-if="isUIAllowed('csvImport', { roles: projectRole })" v-if="isUIAllowed('csvImport', { roles: baseRole })"
key="quick-import-csv" key="quick-import-csv"
@click="openQuickImportDialog('csv', project.bases[baseIndex].id)" @click="openQuickImportDialog('csv', base.sources[sourceIndex].id)"
> >
<div class="color-transition nc-project-menu-item group"> <div class="color-transition nc-base-menu-item group">
<GeneralIcon icon="csv" class="group-hover:text-accent" /> <GeneralIcon icon="csv" class="group-hover:text-accent" />
CSV file CSV file
</div> </div>
</a-menu-item> </a-menu-item>
<a-menu-item <a-menu-item
v-if="isUIAllowed('jsonImport', { roles: projectRole })" v-if="isUIAllowed('jsonImport', { roles: baseRole })"
key="quick-import-json" key="quick-import-json"
@click="openQuickImportDialog('json', project.bases[baseIndex].id)" @click="openQuickImportDialog('json', base.sources[sourceIndex].id)"
> >
<div class="color-transition nc-project-menu-item group"> <div class="color-transition nc-base-menu-item group">
<GeneralIcon icon="json" class="group-hover:text-accent" /> <GeneralIcon icon="json" class="group-hover:text-accent" />
JSON file JSON file
</div> </div>
</a-menu-item> </a-menu-item>
<a-menu-item <a-menu-item
v-if="isUIAllowed('excelImport', { roles: projectRole })" v-if="isUIAllowed('excelImport', { roles: baseRole })"
key="quick-import-excel" key="quick-import-excel"
@click="openQuickImportDialog('excel', project.bases[baseIndex].id)" @click="openQuickImportDialog('excel', base.sources[sourceIndex].id)"
> >
<div class="color-transition nc-project-menu-item group"> <div class="color-transition nc-base-menu-item group">
<GeneralIcon icon="excel" class="group-hover:text-accent" /> <GeneralIcon icon="excel" class="group-hover:text-accent" />
Microsoft Excel Microsoft Excel
</div> </div>
@ -205,26 +205,26 @@ function openTableCreateMagicDialog(baseId?: string) {
<a-menu-divider class="my-0" /> <a-menu-divider class="my-0" />
<!-- <a-menu-item-group title="Connect to new datasource" class="!px-0 !mx-0"> <!-- <a-menu-item-group title="Connect to new datasource" class="!px-0 !mx-0">
<a-menu-item key="connect-new-source" @click="toggleDialog(true, 'dataSources', ClientType.MYSQL, project.id)"> <a-menu-item key="connect-new-source" @click="toggleDialog(true, 'dataSources', ClientType.MYSQL, base.id)">
<div class="color-transition nc-project-menu-item group"> <div class="color-transition nc-base-menu-item group">
<LogosMysqlIcon class="group-hover:text-accent" /> <LogosMysqlIcon class="group-hover:text-accent" />
MySQL MySQL
</div> </div>
</a-menu-item> </a-menu-item>
<a-menu-item key="connect-new-source" @click="toggleDialog(true, 'dataSources', ClientType.PG, project.id)"> <a-menu-item key="connect-new-source" @click="toggleDialog(true, 'dataSources', ClientType.PG, base.id)">
<div class="color-transition nc-project-menu-item group"> <div class="color-transition nc-base-menu-item group">
<LogosPostgresql class="group-hover:text-accent" /> <LogosPostgresql class="group-hover:text-accent" />
Postgres Postgres
</div> </div>
</a-menu-item> </a-menu-item>
<a-menu-item key="connect-new-source" @click="toggleDialog(true, 'dataSources', ClientType.SQLITE, project.id)"> <a-menu-item key="connect-new-source" @click="toggleDialog(true, 'dataSources', ClientType.SQLITE, base.id)">
<div class="color-transition nc-project-menu-item group"> <div class="color-transition nc-base-menu-item group">
<VscodeIconsFileTypeSqlite class="group-hover:text-accent" /> <VscodeIconsFileTypeSqlite class="group-hover:text-accent" />
SQLite SQLite
</div> </div>
</a-menu-item> </a-menu-item>
<a-menu-item key="connect-new-source" @click="toggleDialog(true, 'dataSources', ClientType.MSSQL, project.id)"> <a-menu-item key="connect-new-source" @click="toggleDialog(true, 'dataSources', ClientType.MSSQL, base.id)">
<div class="color-transition nc-project-menu-item group"> <div class="color-transition nc-base-menu-item group">
<SimpleIconsMicrosoftsqlserver class="group-hover:text-accent" /> <SimpleIconsMicrosoftsqlserver class="group-hover:text-accent" />
MSSQL MSSQL
</div> </div>
@ -232,9 +232,9 @@ function openTableCreateMagicDialog(baseId?: string) {
<a-menu-item <a-menu-item
v-if="appInfo.ee" v-if="appInfo.ee"
key="connect-new-source" key="connect-new-source"
@click="toggleDialog(true, 'dataSources', ClientType.SNOWFLAKE, project.id)" @click="toggleDialog(true, 'dataSources', ClientType.SNOWFLAKE, base.id)"
> >
<div class="color-transition nc-project-menu-item group"> <div class="color-transition nc-base-menu-item group">
<LogosSnowflakeIcon class="group-hover:text-accent" /> <LogosSnowflakeIcon class="group-hover:text-accent" />
Snowflake Snowflake
</div> </div>
@ -243,12 +243,12 @@ function openTableCreateMagicDialog(baseId?: string) {
<a-menu-divider class="my-0" /> --> <a-menu-divider class="my-0" /> -->
<a-menu-item v-if="isUIAllowed('importRequest', { roles: projectRole })" key="add-new-table" class="py-1 rounded-b"> <a-menu-item v-if="isUIAllowed('importRequest', { roles: baseRole })" key="add-new-table" class="py-1 rounded-b">
<a <a
v-e="['e:datasource:import-request']" v-e="['e:datasource:import-request']"
href="https://github.com/nocodb/nocodb/issues/2052" href="https://github.com/nocodb/nocodb/issues/2052"
target="_blank" target="_blank"
class="prose-sm hover:(!text-primary !opacity-100) color-transition nc-project-menu-item group after:(!rounded-b)" class="prose-sm hover:(!text-primary !opacity-100) color-transition nc-base-menu-item group after:(!rounded-b)"
> >
<GeneralIcon icon="openInNew" class="group-hover:text-accent" /> <GeneralIcon icon="openInNew" class="group-hover:text-accent" />
<!-- Request a data source you need? --> <!-- Request a data source you need? -->

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

@ -1,21 +1,21 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { BaseType, ProjectType } from 'nocodb-sdk' import type { SourceType, BaseType } from 'nocodb-sdk'
const props = defineProps<{ const props = defineProps<{
source: SourceType
base: BaseType base: BaseType
project: ProjectType
}>() }>()
const base = toRef(props, 'base') const source = toRef(props, 'source')
const { isUIAllowed } = useRoles() const { isUIAllowed } = useRoles()
const projectRole = inject(ProjectRoleInj) const baseRole = inject(ProjectRoleInj)
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
function openAirtableImportDialog(baseId?: string) { function openAirtableImportDialog(sourceId?: string) {
if (!baseId) return if (!sourceId) return
$e('a:actions:import-airtable') $e('a:actions:import-airtable')
@ -23,7 +23,7 @@ function openAirtableImportDialog(baseId?: string) {
const { close } = useDialog(resolveComponent('DlgAirtableImport'), { const { close } = useDialog(resolveComponent('DlgAirtableImport'), {
'modelValue': isOpen, 'modelValue': isOpen,
'baseId': baseId, 'sourceId': sourceId,
'onUpdate:modelValue': closeDialog, 'onUpdate:modelValue': closeDialog,
}) })
@ -35,7 +35,7 @@ function openAirtableImportDialog(baseId?: string) {
} }
function openQuickImportDialog(type: string) { function openQuickImportDialog(type: string) {
if (!base.value?.id) return if (!source.value?.id) return
$e(`a:actions:import-${type}`) $e(`a:actions:import-${type}`)
@ -44,7 +44,7 @@ function openQuickImportDialog(type: string) {
const { close } = useDialog(resolveComponent('DlgQuickImport'), { const { close } = useDialog(resolveComponent('DlgQuickImport'), {
'modelValue': isOpen, 'modelValue': isOpen,
'importType': type, 'importType': type,
'baseId': base.value.id, 'sourceId': source.value.id,
'onUpdate:modelValue': closeDialog, 'onUpdate:modelValue': closeDialog,
}) })
@ -58,7 +58,7 @@ function openQuickImportDialog(type: string) {
<template> <template>
<!-- Quick Import From --> <!-- Quick Import From -->
<NcSubMenu class="py-0" data-testid="nc-sidebar-project-import"> <NcSubMenu class="py-0" data-testid="nc-sidebar-base-import">
<template #title> <template #title>
<GeneralIcon icon="download" /> <GeneralIcon icon="download" />
@ -68,17 +68,17 @@ function openQuickImportDialog(type: string) {
<template #expandIcon></template> <template #expandIcon></template>
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('airtableImport', { roles: projectRole })" v-if="isUIAllowed('airtableImport', { roles: baseRole })"
key="quick-import-airtable" key="quick-import-airtable"
v-e="['c:import:airtable']" v-e="['c:import:airtable']"
@click="openAirtableImportDialog(base.id)" @click="openAirtableImportDialog(source.id)"
> >
<GeneralIcon icon="airtable" class="max-w-3.75 group-hover:text-black" /> <GeneralIcon icon="airtable" class="max-w-3.75 group-hover:text-black" />
<div class="ml-0.5">{{ $t('labels.airtable') }}</div> <div class="ml-0.5">{{ $t('labels.airtable') }}</div>
</NcMenuItem> </NcMenuItem>
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('csvImport', { roles: projectRole })" v-if="isUIAllowed('csvImport', { roles: baseRole })"
key="quick-import-csv" key="quick-import-csv"
v-e="['c:import:csv']" v-e="['c:import:csv']"
@click="openQuickImportDialog('csv')" @click="openQuickImportDialog('csv')"
@ -88,7 +88,7 @@ function openQuickImportDialog(type: string) {
</NcMenuItem> </NcMenuItem>
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('jsonImport', { roles: projectRole })" v-if="isUIAllowed('jsonImport', { roles: baseRole })"
key="quick-import-json" key="quick-import-json"
v-e="['c:import:json']" v-e="['c:import:json']"
@click="openQuickImportDialog('json')" @click="openQuickImportDialog('json')"
@ -98,7 +98,7 @@ function openQuickImportDialog(type: string) {
</NcMenuItem> </NcMenuItem>
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('excelImport', { roles: projectRole })" v-if="isUIAllowed('excelImport', { roles: baseRole })"
key="quick-import-excel" key="quick-import-excel"
v-e="['c:import:excel']" v-e="['c:import:excel']"
@click="openQuickImportDialog('excel')" @click="openQuickImportDialog('excel')"

4
packages/nc-gui/components/dashboard/TreeView/CreateViewBtn.vue

@ -9,7 +9,7 @@ const viewsStore = useViewsStore()
const { loadViews, navigateToView } = viewsStore const { loadViews, navigateToView } = viewsStore
const table = inject(SidebarTableInj)! const table = inject(SidebarTableInj)!
const project = inject(ProjectInj)! const base = inject(ProjectInj)!
const isViewListLoading = ref(false) const isViewListLoading = ref(false)
const toBeCreateType = ref<ViewTypes>() const toBeCreateType = ref<ViewTypes>()
@ -67,7 +67,7 @@ async function onOpenModal({
navigateToView({ navigateToView({
view, view,
tableId: table.value.id!, tableId: table.value.id!,
projectId: project.value.id!, baseId: base.value.id!,
}) })
$e('a:view:create', { view: view.type }) $e('a:view:create', { view: view.type })

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

@ -2,7 +2,7 @@
import { nextTick } from '@vue/runtime-core' import { nextTick } from '@vue/runtime-core'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { stringifyRolesObj } from 'nocodb-sdk' import { stringifyRolesObj } from 'nocodb-sdk'
import type { BaseType, ProjectType, TableType } from 'nocodb-sdk' import type { SourceType, BaseType, TableType } from 'nocodb-sdk'
import { LoadingOutlined } from '@ant-design/icons-vue' import { LoadingOutlined } from '@ant-design/icons-vue'
import { useTitle } from '@vueuse/core' import { useTitle } from '@vueuse/core'
import { import {
@ -13,7 +13,7 @@ import {
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
openLink, openLink,
storeToRefs, storeToRefs,
useProjects, useBases,
} from '#imports' } from '#imports'
import type { NcProject } from '#imports' import type { NcProject } from '#imports'
import { useNuxtApp } from '#app' import { useNuxtApp } from '#app'
@ -29,19 +29,19 @@ const indicator = h(LoadingOutlined, {
const router = useRouter() const router = useRouter()
const route = router.currentRoute const route = router.currentRoute
const { isSharedBase } = storeToRefs(useProject()) const { isSharedBase } = storeToRefs(useBase())
const { projectUrl } = useProject() const { projectUrl } = useBase()
const { setMenuContext, openRenameTableDialog, duplicateTable, contextMenuTarget } = inject(TreeViewInj)! const { setMenuContext, openRenameTableDialog, duplicateTable, contextMenuTarget } = inject(TreeViewInj)!
const project = inject(ProjectInj)! const base = inject(ProjectInj)!
const projectsStore = useProjects() const basesStore = useBases()
const { isMobileMode } = useGlobal() const { isMobileMode } = useGlobal()
const { loadProject, loadProjects, createProject: _createProject, updateProject, getProjectMetaInfo } = projectsStore const { loadProject, loadProjects, createProject: _createProject, updateProject, getProjectMetaInfo } = basesStore
const { projects } = storeToRefs(projectsStore) const { bases } = storeToRefs(basesStore)
const { loadProjectTables } = useTablesStore() const { loadProjectTables } = useTablesStore()
const { activeTable } = storeToRefs(useTablesStore()) const { activeTable } = storeToRefs(useTablesStore())
@ -64,9 +64,11 @@ const { t } = useI18n()
const input = ref<HTMLInputElement>() const input = ref<HTMLInputElement>()
const projectRole = inject(ProjectRoleInj) const baseRole = inject(ProjectRoleInj)
const { activeProjectId } = storeToRefs(useProjects()) const { activeProjectId } = storeToRefs(useBases())
const { baseUrl } = useBase()
const toggleDialog = inject(ToggleDialogInj, () => {}) const toggleDialog = inject(ToggleDialogInj, () => {})
@ -82,9 +84,9 @@ const keys = ref<Record<string, number>>({})
const isTableDeleteDialogVisible = ref(false) const isTableDeleteDialogVisible = ref(false)
const isProjectDeleteDialogVisible = ref(false) const isProjectDeleteDialogVisible = ref(false)
// If only project is open, i.e in case of docs, project view is open and not the page view // If only base is open, i.e in case of docs, base view is open and not the page view
const projectViewOpen = computed(() => { const baseViewOpen = computed(() => {
const routeNameSplit = String(route.value?.name).split('projectId-index-index') const routeNameSplit = String(route.value?.name).split('baseId-index-index')
if (routeNameSplit.length <= 1) return false if (routeNameSplit.length <= 1) return false
const routeNameAfterProjectView = routeNameSplit[routeNameSplit.length - 1] const routeNameAfterProjectView = routeNameSplit[routeNameSplit.length - 1]
@ -97,7 +99,7 @@ const showBaseOption = computed(() => {
const enableEditMode = () => { const enableEditMode = () => {
editMode.value = true editMode.value = true
tempTitle.value = project.value.title! tempTitle.value = base.value.title!
nextTick(() => { nextTick(() => {
input.value?.focus() input.value?.focus()
input.value?.select() input.value?.select()
@ -109,15 +111,15 @@ const updateProjectTitle = async () => {
if (!tempTitle.value) return if (!tempTitle.value) return
try { try {
await updateProject(project.value.id!, { await updateProject(base.value.id!, {
title: tempTitle.value, title: tempTitle.value,
}) })
editMode.value = false editMode.value = false
tempTitle.value = '' tempTitle.value = ''
$e('a:project:rename') $e('a:base:rename')
useTitle(`${project.value?.title}`) useTitle(`${base.value?.title}`)
} catch (e: any) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} }
@ -129,7 +131,7 @@ const copyProjectInfo = async () => {
try { try {
if ( if (
await copy( await copy(
Object.entries(await getProjectMetaInfo(project.value.id!)!) Object.entries(await getProjectMetaInfo(base.value.id!)!)
.map(([k, v]) => `${k}: **${v}**`) .map(([k, v]) => `${k}: **${v}**`)
.join('\n'), .join('\n'),
) )
@ -147,34 +149,34 @@ defineExpose({
enableEditMode, enableEditMode,
}) })
const setIcon = async (icon: string, project: ProjectType) => { const setIcon = async (icon: string, base: BaseType) => {
try { try {
const meta = { const meta = {
...((project.meta as object) || {}), ...((base.meta as object) || {}),
icon, icon,
} }
projectsStore.updateProject(project.id!, { meta: JSON.stringify(meta) }) basesStore.updateProject(base.id!, { meta: JSON.stringify(meta) })
$e('a:project:icon:navdraw', { icon }) $e('a:base:icon:navdraw', { icon })
} catch (e: any) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} }
} }
function openTableCreateDialog(baseIndex?: number | undefined) { function openTableCreateDialog(sourceIndex?: number | undefined) {
const isOpen = ref(true) const isOpen = ref(true)
let baseId = project.value!.bases?.[0].id let sourceId = base.value!.sources?.[0].id
if (typeof baseIndex === 'number') { if (typeof sourceIndex === 'number') {
baseId = project.value!.bases?.[baseIndex].id sourceId = base.value!.sources?.[sourceIndex].id
} }
if (!baseId || !project.value?.id) return if (!sourceId || !base.value?.id) return
const { close } = useDialog(resolveComponent('DlgTableCreate'), { const { close } = useDialog(resolveComponent('DlgTableCreate'), {
'modelValue': isOpen, 'modelValue': isOpen,
baseId, // || bases.value[0].id, sourceId, // || sources.value[0].id,
'projectId': project.value!.id, 'baseId': base.value!.id,
'onCreate': closeDialog, 'onCreate': closeDialog,
'onUpdate:modelValue': () => closeDialog(), 'onUpdate:modelValue': () => closeDialog(),
}) })
@ -184,10 +186,10 @@ function openTableCreateDialog(baseIndex?: number | undefined) {
if (!table) return if (!table) return
project.value.isExpanded = true base.value.isExpanded = true
if (!activeKey.value || !activeKey.value.includes(`collapse-${baseId}`)) { if (!activeKey.value || !activeKey.value.includes(`collapse-${sourceId}`)) {
activeKey.value.push(`collapse-${baseId}`) activeKey.value.push(`collapse-${sourceId}`)
} }
// TODO: Better way to know when the table node dom is available // TODO: Better way to know when the table node dom is available
@ -208,27 +210,27 @@ const addNewProjectChildEntity = async () => {
isAddNewProjectChildEntityLoading.value = true isAddNewProjectChildEntityLoading.value = true
const isProjectPopulated = projectsStore.isProjectPopulated(project.value.id!) const isProjectPopulated = basesStore.isProjectPopulated(base.value.id!)
if (!isProjectPopulated && project.value.type === NcProjectType.DB) { if (!isProjectPopulated && base.value.type === NcProjectType.DB) {
// We do not wait for tables api, so that add new table is seamless. // 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 // 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 // If the table list api takes time to load before the table name validation
loadProjectTables(project.value.id!) loadProjectTables(base.value.id!)
} }
try { try {
openTableCreateDialog() openTableCreateDialog()
if (!project.value.isExpanded && project.value.type !== NcProjectType.DB) { if (!base.value.isExpanded && base.value.type !== NcProjectType.DB) {
project.value.isExpanded = true base.value.isExpanded = true
} }
} finally { } finally {
isAddNewProjectChildEntityLoading.value = false isAddNewProjectChildEntityLoading.value = false
} }
} }
const onProjectClick = async (project: NcProject, ignoreNavigation?: boolean, toggleIsExpanded?: boolean) => { const onProjectClick = async (base: NcProject, ignoreNavigation?: boolean, toggleIsExpanded?: boolean) => {
if (!project) { if (!base) {
return return
} }
@ -238,19 +240,19 @@ const onProjectClick = async (project: NcProject, ignoreNavigation?: boolean, to
toggleIsExpanded = isMobileMode.value || toggleIsExpanded toggleIsExpanded = isMobileMode.value || toggleIsExpanded
if (toggleIsExpanded) { if (toggleIsExpanded) {
project.isExpanded = !project.isExpanded base.isExpanded = !base.isExpanded
} else { } else {
project.isExpanded = true base.isExpanded = true
} }
const isProjectPopulated = projectsStore.isProjectPopulated(project.id!) const isProjectPopulated = basesStore.isProjectPopulated(base.id!)
if (!isProjectPopulated) project.isLoading = true if (!isProjectPopulated) base.isLoading = true
if (!ignoreNavigation) { if (!ignoreNavigation) {
await navigateTo( await navigateTo(
projectUrl({ baseUrl({
id: project.id!, id: base.id!,
type: 'database', type: 'database',
isSharedBase: isSharedBase.value, isSharedBase: isSharedBase.value,
}), }),
@ -258,32 +260,32 @@ const onProjectClick = async (project: NcProject, ignoreNavigation?: boolean, to
} }
if (!isProjectPopulated) { if (!isProjectPopulated) {
await loadProjectTables(project.id!) await loadProjectTables(base.id!)
} }
if (!isProjectPopulated) { if (!isProjectPopulated) {
const updatedProject = projects.value.get(project.id!)! const updatedProject = bases.value.get(base.id!)!
updatedProject.isLoading = false updatedProject.isLoading = false
} }
} }
function openErdView(base: BaseType) { function openErdView(source: SourceType) {
activeBaseId.value = base.id activeBaseId.value = source.id
isErdModalOpen.value = !isErdModalOpen.value isErdModalOpen.value = !isErdModalOpen.value
} }
async function openProjectErdView(_project: ProjectType) { async function openProjectErdView(_project: BaseType) {
if (!_project.id) return if (!_project.id) return
if (!projectsStore.isProjectPopulated(_project.id)) { if (!basesStore.isProjectPopulated(_project.id)) {
await loadProject(_project.id) await loadProject(_project.id)
} }
const project = projects.value.get(_project.id) const base = bases.value.get(_project.id)
const base = project?.bases?.[0] const source = base?.sources?.[0]
if (!base) return if (!source) return
openErdView(base) openErdView(source)
} }
const reloadTables = async () => { const reloadTables = async () => {
@ -293,11 +295,11 @@ const reloadTables = async () => {
} }
const contextMenuBase = computed(() => { const contextMenuBase = computed(() => {
if (contextMenuTarget.type === 'base') { if (contextMenuTarget.type === 'source') {
return contextMenuTarget.value return contextMenuTarget.value
} else if (contextMenuTarget.type === 'table') { } else if (contextMenuTarget.type === 'table') {
const base = project.value?.bases?.find((b) => b.id === contextMenuTarget.value.base_id) const source = base.value?.sources?.find((b) => b.id === contextMenuTarget.value.source_id)
if (base) return base if (source) return source
} }
return null return null
}) })
@ -307,11 +309,11 @@ watch(
async () => { async () => {
if (!activeTable.value) return if (!activeTable.value) return
const baseId = activeTable.value.base_id const sourceId = activeTable.value.source_id
if (!baseId) return if (!sourceId) return
if (!activeKey.value.includes(`collapse-${baseId}`)) { if (!activeKey.value.includes(`collapse-${sourceId}`)) {
activeKey.value.push(`collapse-${baseId}`) activeKey.value.push(`collapse-${sourceId}`)
} }
}, },
{ {
@ -332,13 +334,13 @@ onKeyStroke('Escape', () => {
const isDuplicateDlgOpen = ref(false) const isDuplicateDlgOpen = ref(false)
const selectedProjectToDuplicate = ref() const selectedProjectToDuplicate = ref()
const duplicateProject = (project: ProjectType) => { const duplicateProject = (base: BaseType) => {
selectedProjectToDuplicate.value = project selectedProjectToDuplicate.value = base
isDuplicateDlgOpen.value = true isDuplicateDlgOpen.value = true
} }
const { $poller } = useNuxtApp() const { $poller } = useNuxtApp()
const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string }) => { const DlgProjectDuplicateOnOk = async (jobData: { id: string; base_id: string }) => {
await loadProjects('workspace') await loadProjects('workspace')
$poller.subscribe( $poller.subscribe(
@ -358,23 +360,23 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
if (data.status === JobStatus.COMPLETED) { if (data.status === JobStatus.COMPLETED) {
await loadProjects('workspace') await loadProjects('workspace')
const project = projects.value.get(jobData.project_id) const base = bases.value.get(jobData.base_id)
// open project after duplication // open base after duplication
if (project) { if (base) {
await navigateToProject({ await navigateToProject({
projectId: project.id, baseId: base.id,
type: project.type, type: base.type,
}) })
} }
} else if (data.status === JobStatus.FAILED) { } else if (data.status === JobStatus.FAILED) {
message.error('Failed to duplicate project') message.error('Failed to duplicate base')
await loadProjects('workspace') await loadProjects('workspace')
} }
} }
}, },
) )
$e('a:project:duplicate') $e('a:base:duplicate')
} }
const tableDelete = () => { const tableDelete = () => {
@ -391,54 +393,54 @@ const projectDelete = () => {
<template> <template>
<NcDropdown :trigger="['contextmenu']" overlay-class-name="nc-dropdown-tree-view-context-menu"> <NcDropdown :trigger="['contextmenu']" overlay-class-name="nc-dropdown-tree-view-context-menu">
<div <div
class="mx-1 nc-project-sub-menu rounded-md" class="mx-1 nc-base-sub-menu rounded-md"
:class="{ active: project.isExpanded }" :class="{ active: base.isExpanded }"
:data-testid="`nc-sidebar-project-${project.title}`" :data-testid="`nc-sidebar-base-${base.title}`"
:data-project-id="project.id" :data-base-id="base.id"
> >
<div class="flex items-center gap-0.75 py-0.25 cursor-pointer" @contextmenu="setMenuContext('project', project)"> <div class="flex items-center gap-0.75 py-0.25 cursor-pointer" @contextmenu="setMenuContext('base', base)">
<div <div
ref="projectNodeRefs" ref="baseNodeRefs"
:class="{ :class="{
'bg-primary-selected active': activeProjectId === project.id && projectViewOpen && !isMobileMode, 'bg-primary-selected active': activeProjectId === base.id && baseViewOpen && !isMobileMode,
'hover:bg-gray-200': !(activeProjectId === project.id && projectViewOpen), 'hover:bg-gray-200': !(activeProjectId === base.id && baseViewOpen),
}" }"
:data-testid="`nc-sidebar-project-title-${project.title}`" :data-testid="`nc-sidebar-base-title-${base.title}`"
class="nc-sidebar-node project-title-node h-7.25 flex-grow rounded-md group flex items-center w-full pr-1" class="nc-sidebar-node base-title-node h-7.25 flex-grow rounded-md group flex items-center w-full pr-1"
> >
<NcButton <NcButton
v-e="['c:base:expand']" v-e="['c:base:expand']"
type="text" type="text"
size="xxsmall" size="xxsmall"
class="nc-sidebar-node-btn nc-sidebar-expand ml-0.75 !xs:visible" class="nc-sidebar-node-btn nc-sidebar-expand ml-0.75 !xs:visible"
@click="onProjectClick(project, true, true)" @click="onProjectClick(base, true, true)"
> >
<GeneralIcon <GeneralIcon
icon="triangleFill" icon="triangleFill"
class="group-hover:visible cursor-pointer transform transition-transform duration-500 h-1.5 w-1.75 rotate-90 !xs:visible" class="group-hover:visible cursor-pointer transform transition-transform duration-500 h-1.5 w-1.75 rotate-90 !xs:visible"
:class="{ '!rotate-180': project.isExpanded, '!visible': isOptionsOpen }" :class="{ '!rotate-180': base.isExpanded, '!visible': isOptionsOpen }"
/> />
</NcButton> </NcButton>
<div class="flex items-center mr-1" @click="onProjectClick(project)"> <div class="flex items-center mr-1" @click="onProjectClick(base)">
<div class="flex items-center select-none w-6 h-full"> <div class="flex items-center select-none w-6 h-full">
<a-spin <a-spin
v-if="project.isLoading" v-if="base.isLoading"
class="!ml-1.25 !flex !flex-row !items-center !my-0.5 w-8" class="!ml-1.25 !flex !flex-row !items-center !my-0.5 w-8"
:indicator="indicator" :indicator="indicator"
/> />
<LazyGeneralEmojiPicker <LazyGeneralEmojiPicker
v-else v-else
:key="project.meta?.icon" :key="base.meta?.icon"
:emoji="base.meta?.icon"
v-e="['c:base:emojiSelect']" v-e="['c:base:emojiSelect']"
:emoji="project.meta?.icon"
:readonly="true" :readonly="true"
size="small" size="small"
@emoji-selected="setIcon($event, project)" @emoji-selected="setIcon($event, base)"
> >
<template #default> <template #default>
<GeneralProjectIcon :type="project.type" /> <GeneralProjectIcon :type="base.type" />
</template> </template>
</LazyGeneralEmojiPicker> </LazyGeneralEmojiPicker>
</div> </div>
@ -449,7 +451,7 @@ const projectDelete = () => {
ref="input" ref="input"
v-model="tempTitle" v-model="tempTitle"
class="flex-grow leading-1 outline-0 ring-none capitalize !text-inherit !bg-transparent w-4/5" class="flex-grow leading-1 outline-0 ring-none capitalize !text-inherit !bg-transparent w-4/5"
:class="{ 'text-black font-semibold': activeProjectId === project.id && projectViewOpen && !isMobileMode }" :class="{ 'text-black font-semibold': activeProjectId === base.id && baseViewOpen && !isMobileMode }"
@click.stop @click.stop
@keyup.enter="updateProjectTitle" @keyup.enter="updateProjectTitle"
@keyup.esc="updateProjectTitle" @keyup.esc="updateProjectTitle"
@ -459,12 +461,12 @@ const projectDelete = () => {
v-else v-else
class="nc-sidebar-node-title capitalize text-ellipsis overflow-hidden select-none" class="nc-sidebar-node-title capitalize text-ellipsis overflow-hidden select-none"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }" :style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
:class="{ 'text-black font-semibold': activeProjectId === project.id && projectViewOpen }" :class="{ 'text-black font-semibold': activeProjectId === base.id && baseViewOpen }"
@click="onProjectClick(project)" @click="onProjectClick(base)"
> >
{{ project.title }} {{ base.title }}
</span> </span>
<div :class="{ 'flex flex-grow h-full': !editMode }" @click="onProjectClick(project)"></div> <div :class="{ 'flex flex-grow h-full': !editMode }" @click="onProjectClick(base)"></div>
<NcDropdown v-model:visible="isOptionsOpen" :trigger="['click']"> <NcDropdown v-model:visible="isOptionsOpen" :trigger="['click']">
<NcButton <NcButton
@ -485,12 +487,12 @@ const projectDelete = () => {
maxHeight: '70vh', maxHeight: '70vh',
overflow: 'overlay', overflow: 'overlay',
}" }"
:data-testid="`nc-sidebar-project-${project.title}-options`" :data-testid="`nc-sidebar-base-${base.title}-options`"
@click="isOptionsOpen = false" @click="isOptionsOpen = false"
> >
<template v-if="!isSharedBase"> <template v-if="!isSharedBase">
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('projectRename')" v-if="isUIAllowed('baseRename')"
v-e="['c:base:rename']" v-e="['c:base:rename']"
data-testid="nc-sidebar-project-rename" data-testid="nc-sidebar-project-rename"
@click="enableEditMode" @click="enableEditMode"
@ -500,23 +502,23 @@ const projectDelete = () => {
</NcMenuItem> </NcMenuItem>
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('projectDuplicate', { roles: [stringifyRolesObj(orgRoles), projectRole].join() })" v-if="isUIAllowed('baseDuplicate', { roles: [stringifyRolesObj(orgRoles), baseRole].join() })"
v-e="['c:base:duplicate']" v-e="['c:base:duplicate']"
data-testid="nc-sidebar-project-duplicate" data-testid="nc-sidebar-base-duplicate"
@click="duplicateProject(project)" @click="duplicateProject(base)"
> >
<GeneralIcon icon="duplicate" class="text-gray-700" /> <GeneralIcon icon="duplicate" class="text-gray-700" />
{{ $t('general.duplicate') }} {{ $t('general.duplicate') }}
</NcMenuItem> </NcMenuItem>
<NcDivider v-if="['projectDuplicate', 'projectRename'].some((permission) => isUIAllowed(permission))" /> <NcDivider v-if="['baseDuplicate', 'baseRename'].some((permission) => isUIAllowed(permission))" />
<!-- Copy Project Info --> <!-- Copy Project Info -->
<NcMenuItem <NcMenuItem
v-if="!isEeUI" v-if="!isEeUI"
key="copy" key="copy"
data-testid="nc-sidebar-base-copy-base-info"
v-e="['c:base:copy-proj-info']" v-e="['c:base:copy-proj-info']"
data-testid="nc-sidebar-project-copy-project-info"
@click.stop="copyProjectInfo" @click.stop="copyProjectInfo"
> >
<GeneralIcon icon="copy" class="group-hover:text-black" /> <GeneralIcon icon="copy" class="group-hover:text-black" />
@ -527,8 +529,8 @@ const projectDelete = () => {
<NcMenuItem <NcMenuItem
key="erd" key="erd"
v-e="['c:base:erd']" v-e="['c:base:erd']"
data-testid="nc-sidebar-project-relations" data-testid="nc-sidebar-base-relations"
@click="openProjectErdView(project)" @click="openProjectErdView(base)"
> >
<GeneralIcon icon="erd" /> <GeneralIcon icon="erd" />
{{ $t('title.relations') }} {{ $t('title.relations') }}
@ -539,11 +541,11 @@ const projectDelete = () => {
v-if="isUIAllowed('apiDocs')" v-if="isUIAllowed('apiDocs')"
key="api" key="api"
v-e="['c:base:api-docs']" v-e="['c:base:api-docs']"
data-testid="nc-sidebar-project-rest-apis" data-testid="nc-sidebar-base-rest-apis"
@click.stop=" @click.stop="
() => { () => {
$e('c:base:api-docs') $e('c:base:api-docs')
openLink(`/api/v1/db/meta/projects/${project.id}/swagger`, appInfo.ncSiteUrl) openLink(`/api/v1/meta/bases/${base.id}/swagger`, appInfo.ncSiteUrl)
} }
" "
> >
@ -552,27 +554,27 @@ const projectDelete = () => {
</NcMenuItem> </NcMenuItem>
</template> </template>
<template v-if="project.bases && project.bases[0] && showBaseOption"> <template v-if="base.sources && base.sources[0] && showBaseOption">
<NcDivider /> <NcDivider />
<DashboardTreeViewBaseOptions v-model:project="project" :base="project.bases[0]" /> <DashboardTreeViewBaseOptions v-model:base="base" :source="base.sources[0]" />
</template> </template>
<NcDivider v-if="['projectMiscSettings', 'projectDelete'].some((permission) => isUIAllowed(permission))" /> <NcDivider v-if="['baseMiscSettings', 'baseDelete'].some((permission) => isUIAllowed(permission))" />
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('projectMiscSettings')" v-if="isUIAllowed('baseMiscSettings')"
key="teamAndSettings" key="teamAndSettings"
v-e="['c:base:settings']" v-e="['c:base:settings']"
data-testid="nc-sidebar-project-settings" data-testid="nc-sidebar-base-settings"
class="nc-sidebar-project-project-settings" class="nc-sidebar-base-base-settings"
@click="toggleDialog(true, 'teamAndAuth', undefined, project.id)" @click="toggleDialog(true, 'teamAndAuth', undefined, base.id)"
> >
<GeneralIcon icon="settings" class="group-hover:text-black" /> <GeneralIcon icon="settings" class="group-hover:text-black" />
{{ $t('activity.settings') }} {{ $t('activity.settings') }}
</NcMenuItem> </NcMenuItem>
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('projectDelete', { roles: [stringifyRolesObj(orgRoles), projectRole].join() })" v-if="isUIAllowed('baseDelete', { roles: [stringifyRolesObj(orgRoles), baseRole].join() })"
data-testid="nc-sidebar-project-delete" data-testid="nc-sidebar-base-delete"
class="!text-red-500 !hover:bg-red-50" class="!text-red-500 !hover:bg-red-50"
@click="projectDelete" @click="projectDelete"
> >
@ -584,12 +586,12 @@ const projectDelete = () => {
</NcDropdown> </NcDropdown>
<NcButton <NcButton
v-if="isUIAllowed('tableCreate', { roles: projectRole })" v-if="isUIAllowed('tableCreate', { roles: baseRole })"
v-e="['c:base:create-table']" v-e="['c:base:create-table']"
class="nc-sidebar-node-btn" class="nc-sidebar-node-btn"
size="xxsmall" size="xxsmall"
type="text" type="text"
data-testid="nc-sidebar-add-project-entity" data-testid="nc-sidebar-add-base-entity"
:class="{ '!text-black !visible': isAddNewProjectChildEntityLoading, '!visible': isOptionsOpen }" :class="{ '!text-black !visible': isAddNewProjectChildEntityLoading, '!visible': isOptionsOpen }"
:loading="isAddNewProjectChildEntityLoading" :loading="isAddNewProjectChildEntityLoading"
@click.stop="addNewProjectChildEntity" @click.stop="addNewProjectChildEntity"
@ -600,28 +602,28 @@ const projectDelete = () => {
</div> </div>
<div <div
v-if="project.id && !project.isLoading" v-if="base.id && !base.isLoading"
key="g1" key="g1"
class="overflow-x-hidden transition-max-height" class="overflow-x-hidden transition-max-height"
:class="{ 'max-h-0': !project.isExpanded }" :class="{ 'max-h-0': !base.isExpanded }"
> >
<template v-if="project && project?.bases"> <template v-if="base && base?.sources">
<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="base?.sources?.[0]?.enabled" class="flex-1">
<div class="transition-height duration-200"> <div class="transition-height duration-200">
<DashboardTreeViewTableList :project="project" :base-index="0" /> <DashboardTreeViewTableList :base="base" :source-index="0" />
</div> </div>
</div> </div>
<div v-if="project?.bases?.slice(1).filter((el) => el.enabled)?.length" class="transition-height duration-200"> <div v-if="base?.sources?.slice(1).filter((el) => el.enabled)?.length" class="transition-height duration-200">
<div class="border-none sortable-list"> <div class="border-none sortable-list">
<div v-for="(base, baseIndex) of project.bases" :key="`base-${base.id}`"> <div v-for="(source, sourceIndex) of base.sources" :key="`source-${source.id}`">
<template v-if="baseIndex === 0"></template> <template v-if="sourceIndex === 0"></template>
<a-collapse <a-collapse
v-else-if="base && base.enabled" v-else-if="source && source.enabled"
v-model:activeKey="activeKey" v-model:activeKey="activeKey"
class="!mx-0 !px-0 nc-sidebar-source-node"
v-e="['c:source:toggle-expand']" v-e="['c:source:toggle-expand']"
class="!mx-0 !px-0 nc-sidebar-base-node"
:class="[{ hidden: searchActive && !!filterQuery }]" :class="[{ hidden: searchActive && !!filterQuery }]"
expand-icon-position="left" expand-icon-position="left"
:bordered="false" :bordered="false"
@ -633,34 +635,34 @@ const projectDelete = () => {
> >
<GeneralIcon <GeneralIcon
icon="triangleFill" icon="triangleFill"
class="nc-sidebar-base-node-btns -mt-0.75 invisible xs:visible cursor-pointer transform transition-transform duration-500 h-1.5 w-1.5 text-gray-500 rotate-90" class="nc-sidebar-source-node-btns -mt-0.75 invisible xs:visible cursor-pointer transform transition-transform duration-500 h-1.5 w-1.5 text-gray-500 rotate-90"
:class="{ '!rotate-180': isActive }" :class="{ '!rotate-180': isActive }"
/> />
</div> </div>
</template> </template>
<a-collapse-panel :key="`collapse-${base.id}`"> <a-collapse-panel :key="`collapse-${source.id}`">
<template #header> <template #header>
<div class="nc-sidebar-node min-w-20 w-full flex flex-row group py-0.25"> <div class="nc-sidebar-node min-w-20 w-full flex flex-row group py-0.25">
<div <div
v-if="baseIndex === 0" v-if="sourceIndex === 0"
class="base-context flex items-center gap-2 text-gray-800 nc-sidebar-node-title" class="source-context flex items-center gap-2 text-gray-800 nc-sidebar-node-title"
@contextmenu="setMenuContext('base', base)" @contextmenu="setMenuContext('source', source)"
> >
<GeneralBaseLogo :base-type="base.type" class="min-w-4 !xs:(min-w-4.25 w-4.25 text-sm)" /> <GeneralBaseLogo :source-type="source.type" class="min-w-4 !xs:(min-w-4.25 w-4.25 text-sm)" />
{{ $t('general.default') }} {{ $t('general.default') }}
</div> </div>
<div <div
v-else v-else
class="base-context flex flex-grow items-center gap-1.75 text-gray-800 min-w-1/20 max-w-full" class="source-context flex flex-grow items-center gap-1.75 text-gray-800 min-w-1/20 max-w-full"
@contextmenu="setMenuContext('base', base)" @contextmenu="setMenuContext('source', source)"
> >
<GeneralBaseLogo :base-type="base.type" class="min-w-4 !xs:(min-w-4.25 w-4.25 text-sm)" /> <GeneralBaseLogo :source-type="source.type" class="min-w-4 !xs:(min-w-4.25 w-4.25 text-sm)" />
<div <div
:data-testid="`nc-sidebar-project-${base.alias}`" :data-testid="`nc-sidebar-base-${source.alias}`"
class="nc-sidebar-node-title flex capitalize text-ellipsis overflow-hidden select-none" class="nc-sidebar-node-title flex capitalize text-ellipsis overflow-hidden select-none"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }" :style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
> >
{{ base.alias || '' }} {{ source.alias || '' }}
</div> </div>
<a-tooltip class="xs:(hidden)"> <a-tooltip class="xs:(hidden)">
<template #title>{{ $t('objects.externalDb') }}</template> <template #title>{{ $t('objects.externalDb') }}</template>
@ -671,17 +673,17 @@ const projectDelete = () => {
</div> </div>
<div class="flex flex-row items-center gap-x-0.25 w-12.25"> <div class="flex flex-row items-center gap-x-0.25 w-12.25">
<NcDropdown <NcDropdown
:visible="isBasesOptionsOpen[base!.id!]" :visible="isBasesOptionsOpen[source!.id!]"
:trigger="['click']" :trigger="['click']"
@update:visible="isBasesOptionsOpen[base!.id!] = $event" @update:visible="isBasesOptionsOpen[source!.id!] = $event"
> >
<NcButton <NcButton
v-e="['c:source:options']" v-e="['c:source:options']"
class="nc-sidebar-node-btn" class="nc-sidebar-node-btn"
:class="{ '!text-black !opacity-100': isBasesOptionsOpen[base!.id!] }" :class="{ '!text-black !opacity-100': isBasesOptionsOpen[source!.id!] }"
type="text" type="text"
size="xxsmall" size="xxsmall"
@click.stop="isBasesOptionsOpen[base!.id!] = !isBasesOptionsOpen[base!.id!]" @click.stop="isBasesOptionsOpen[source!.id!] = !isBasesOptionsOpen[source!.id!]"
> >
<GeneralIcon icon="threeDotHorizontal" class="text-xl w-4.75" /> <GeneralIcon icon="threeDotHorizontal" class="text-xl w-4.75" />
</NcButton> </NcButton>
@ -692,26 +694,26 @@ const projectDelete = () => {
maxHeight: '70vh', maxHeight: '70vh',
overflow: 'overlay', overflow: 'overlay',
}" }"
@click="isBasesOptionsOpen[base!.id!] = false" @click="isBasesOptionsOpen[source!.id!] = false"
> >
<!-- ERD View --> <!-- ERD View -->
<NcMenuItem key="erd" v-e="['c:source:erd']" @click="openErdView(base)"> <NcMenuItem key="erd" v-e="['c:source:erd']" @click="openErdView(source)">
<GeneralIcon icon="erd" /> <GeneralIcon icon="erd" />
{{ $t('title.relations') }} {{ $t('title.relations') }}
</NcMenuItem> </NcMenuItem>
<DashboardTreeViewBaseOptions v-if="showBaseOption" v-model:project="project" :base="base" /> <DashboardTreeViewBaseOptions v-if="showBaseOption" v-model:base="base" :source="source" />
</NcMenu> </NcMenu>
</template> </template>
</NcDropdown> </NcDropdown>
<NcButton <NcButton
v-if="isUIAllowed('tableCreate', { roles: projectRole })" v-if="isUIAllowed('tableCreate', { roles: baseRole })"
v-e="['c:source:add-table']" v-e="['c:source:add-table']"
type="text" type="text"
size="xxsmall" size="xxsmall"
class="nc-sidebar-node-btn" class="nc-sidebar-node-btn"
@click.stop="openTableCreateDialog(baseIndex)" @click.stop="openTableCreateDialog(sourceIndex)"
> >
<GeneralIcon icon="plus" class="text-xl leading-5" style="-webkit-text-stroke: 0.15px" /> <GeneralIcon icon="plus" class="text-xl leading-5" style="-webkit-text-stroke: 0.15px" />
</NcButton> </NcButton>
@ -720,10 +722,10 @@ const projectDelete = () => {
</template> </template>
<div <div
ref="menuRefs" ref="menuRefs"
:key="`sortable-${base.id}-${base.id && base.id in keys ? keys[base.id] : '0'}`" :key="`sortable-${source.id}-${source.id && source.id in keys ? keys[source.id] : '0'}`"
:nc-base="base.id" :nc-source="source.id"
> >
<DashboardTreeViewTableList :project="project" :base-index="baseIndex" /> <DashboardTreeViewTableList :base="base" :source-index="sourceIndex" />
</div> </div>
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
@ -736,14 +738,13 @@ const projectDelete = () => {
</div> </div>
<template v-if="!isSharedBase" #overlay> <template v-if="!isSharedBase" #overlay>
<NcMenu class="!py-0 rounded text-sm"> <NcMenu class="!py-0 rounded text-sm">
<template v-if="contextMenuTarget.type === 'project' && project.type === 'database'"></template> <template v-if="contextMenuTarget.type === 'base' && base.type === 'database'"></template>
<template v-else-if="contextMenuTarget.type === 'base'"></template> <template v-else-if="contextMenuTarget.type === 'source'"></template>
<template v-else-if="contextMenuTarget.type === 'table'"> <template v-else-if="contextMenuTarget.type === 'table'">
v-e="['c:table:rename']" <NcMenuItem v-if="isUIAllowed('tableRename')" v-e="['c:table:rename']" @click="openRenameTableDialog(contextMenuTarget.value, true)">
<NcMenuItem v-if="isUIAllowed('tableRename')" @click="openRenameTableDialog(contextMenuTarget.value, true)"> <div class="nc-base-option-item">
<div class="nc-project-option-item">
<GeneralIcon icon="edit" class="text-gray-700" /> <GeneralIcon icon="edit" class="text-gray-700" />
{{ $t('general.rename') }} {{ $t('general.rename') }}
</div> </div>
@ -754,14 +755,14 @@ const projectDelete = () => {
v-e="['c:table:duplicate']" v-e="['c:table:duplicate']"
@click="duplicateTable(contextMenuTarget.value)" @click="duplicateTable(contextMenuTarget.value)"
> >
<div class="nc-project-option-item"> <div class="nc-base-option-item">
<GeneralIcon icon="duplicate" class="text-gray-700" /> <GeneralIcon icon="duplicate" class="text-gray-700" />
{{ $t('general.duplicate') }} {{ $t('general.duplicate') }}
</div> </div>
</NcMenuItem> </NcMenuItem>
<NcDivider /> <NcDivider />
<NcMenuItem v-if="isUIAllowed('table-delete')" class="!hover:bg-red-50" @click="tableDelete"> <NcMenuItem v-if="isUIAllowed('table-delete')" class="!hover:bg-red-50" @click="tableDelete">
<div class="nc-project-option-item text-red-600"> <div class="nc-base-option-item text-red-600">
<GeneralIcon icon="delete" /> <GeneralIcon icon="delete" />
{{ $t('general.delete') }} {{ $t('general.delete') }}
</div> </div>
@ -770,7 +771,7 @@ const projectDelete = () => {
<template v-else> <template v-else>
<NcMenuItem @click="reloadTables"> <NcMenuItem @click="reloadTables">
<div class="nc-project-option-item"> <div class="nc-base-option-item">
{{ $t('general.reload') }} {{ $t('general.reload') }}
</div> </div>
</NcMenuItem> </NcMenuItem>
@ -779,21 +780,21 @@ const projectDelete = () => {
</template> </template>
</NcDropdown> </NcDropdown>
<DlgTableDelete <DlgTableDelete
v-if="contextMenuTarget.value?.id && project?.id" v-if="contextMenuTarget.value?.id && base?.id"
v-model:visible="isTableDeleteDialogVisible" v-model:visible="isTableDeleteDialogVisible"
:table-id="contextMenuTarget.value?.id" :table-id="contextMenuTarget.value?.id"
:project-id="project?.id" :base-id="base?.id"
/> />
<DlgProjectDelete v-model:visible="isProjectDeleteDialogVisible" :project-id="project?.id" /> <DlgProjectDelete v-model:visible="isProjectDeleteDialogVisible" :base-id="base?.id" />
<DlgProjectDuplicate <DlgProjectDuplicate
v-if="selectedProjectToDuplicate" v-if="selectedProjectToDuplicate"
v-model="isDuplicateDlgOpen" v-model="isDuplicateDlgOpen"
:project="selectedProjectToDuplicate" :base="selectedProjectToDuplicate"
:on-ok="DlgProjectDuplicateOnOk" :on-ok="DlgProjectDuplicateOnOk"
/> />
<GeneralModal v-model:visible="isErdModalOpen" size="large"> <GeneralModal v-model:visible="isErdModalOpen" size="large">
<div class="h-[80vh]"> <div class="h-[80vh]">
<LazyDashboardSettingsErd :base-id="activeBaseId" /> <LazyDashboardSettingsErd :source-id="activeBaseId" />
</div> </div>
</GeneralModal> </GeneralModal>
</template> </template>
@ -811,7 +812,7 @@ const projectDelete = () => {
@apply !px-0 !pb-0 !pt-0.25; @apply !px-0 !pb-0 !pt-0.25;
} }
:deep(.ant-collapse-header:hover .nc-sidebar-base-node-btns) { :deep(.ant-collapse-header:hover .nc-sidebar-source-node-btns) {
@apply visible; @apply visible;
} }
</style> </style>

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

@ -1,17 +1,17 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ProjectType } from 'nocodb-sdk' import type { BaseType } from 'nocodb-sdk'
import { ProjectInj, ProjectRoleInj } from '#imports' import { ProjectInj, ProjectRoleInj } from '#imports'
const props = defineProps<{ const props = defineProps<{
projectRole: string | string[] baseRole: string | string[]
project: ProjectType base: BaseType
}>() }>()
const projectRole = toRef(props, 'projectRole') const baseRole = toRef(props, 'baseRole')
const project = toRef(props, 'project') const base = toRef(props, 'base')
provide(ProjectRoleInj, projectRole) provide(ProjectRoleInj, baseRole)
provide(ProjectInj, project) provide(ProjectInj, base)
</script> </script>
<template> <template>

54
packages/nc-gui/components/dashboard/TreeView/TableList.vue

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ProjectType, TableType } from 'nocodb-sdk' import type { BaseType, TableType } from 'nocodb-sdk'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import Sortable from 'sortablejs' import Sortable from 'sortablejs'
import TableNode from './TableNode.vue' import TableNode from './TableNode.vue'
@ -8,23 +8,23 @@ import { toRef } from '#imports'
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
project: ProjectType base: BaseType
baseIndex?: number sourceIndex?: number
}>(), }>(),
{ {
baseIndex: 0, sourceIndex: 0,
}, },
) )
const project = toRef(props, 'project') const base = toRef(props, 'base')
const baseIndex = toRef(props, 'baseIndex') const sourceIndex = toRef(props, 'sourceIndex')
const base = computed(() => project.value?.bases?.[baseIndex.value]) const source = computed(() => base.value?.sources?.[sourceIndex.value])
const { isMobileMode } = useGlobal() const { isMobileMode } = useGlobal()
const { projectTables } = storeToRefs(useTablesStore()) const { baseTables } = storeToRefs(useTablesStore())
const tables = computed(() => projectTables.value.get(project.value.id!) ?? []) const tables = computed(() => baseTables.value.get(base.value.id!) ?? [])
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
@ -44,14 +44,14 @@ const sortables: Record<string, Sortable> = {}
// todo: replace with vuedraggable // todo: replace with vuedraggable
const initSortable = (el: Element) => { const initSortable = (el: Element) => {
const base_id = el.getAttribute('nc-base') const source_id = el.getAttribute('nc-source')
if (!base_id) return if (!source_id) return
if (isMobileMode.value) return if (isMobileMode.value) return
if (sortables[base_id]) sortables[base_id].destroy() if (sortables[source_id]) sortables[source_id].destroy()
Sortable.create(el as HTMLLIElement, { Sortable.create(el as HTMLLIElement, {
onEnd: async (evt) => { onEnd: async (evt) => {
const offset = tables.value.findIndex((table) => table.base_id === base_id) const offset = tables.value.findIndex((table) => table.source_id === source_id)
const { newIndex = 0, oldIndex = 0 } = evt const { newIndex = 0, oldIndex = 0 } = evt
@ -87,10 +87,10 @@ const initSortable = (el: Element) => {
tables.value?.splice(newIndex + offset, 0, ...tables.value?.splice(oldIndex + offset, 1)) tables.value?.splice(newIndex + offset, 0, ...tables.value?.splice(oldIndex + offset, 1))
// force re-render the list // force re-render the list
if (keys.value[base_id]) { if (keys.value[source_id]) {
keys.value[base_id] = keys.value[base_id] + 1 keys.value[source_id] = keys.value[source_id] + 1
} else { } else {
keys.value[base_id] = 1 keys.value[source_id] = 1
} }
// update the item order // update the item order
@ -106,7 +106,7 @@ const initSortable = (el: Element) => {
id: dragEl.dataset.id, id: dragEl.dataset.id,
title: dragEl.dataset.title, title: dragEl.dataset.title,
type: dragEl.dataset.type, type: dragEl.dataset.type,
baseId: dragEl.dataset.baseId, sourceId: dragEl.dataset.sourceId,
}), }),
) )
}, },
@ -125,28 +125,28 @@ watchEffect(() => {
}) })
const availableTables = computed(() => { const availableTables = computed(() => {
return tables.value.filter((table) => table.base_id === project.value?.bases?.[baseIndex.value].id) return tables.value.filter((table) => table.source_id === base.value?.sources?.[sourceIndex.value].id)
}) })
</script> </script>
<template> <template>
<div class="border-none sortable-list"> <div class="border-none sortable-list">
<template v-if="project"> <template v-if="base">
<div <div
v-if="availableTables.length === 0" v-if="availableTables.length === 0"
class="py-0.5 text-gray-500" class="py-0.5 text-gray-500"
:class="{ :class="{
'ml-13.55': baseIndex === 0, 'ml-13.55': sourceIndex === 0,
'ml-19.25': baseIndex !== 0, 'ml-19.25': sourceIndex !== 0,
}" }"
> >
{{ $t('general.empty') }} {{ $t('general.empty') }}
</div> </div>
<div <div
v-if="project.bases?.[baseIndex] && project!.bases[baseIndex].enabled" v-if="base.sources?.[sourceIndex] && base!.sources[sourceIndex].enabled"
ref="menuRefs" ref="menuRefs"
:key="`sortable-${base?.id}-${base?.id && base?.id in keys ? keys[base?.id] : '0'}`" :key="`sortable-${source?.id}-${source?.id && source?.id in keys ? keys[source?.id] : '0'}`"
:nc-base="base?.id" :nc-source="source?.id"
> >
<TableNode <TableNode
v-for="table of availableTables" v-for="table of availableTables"
@ -155,10 +155,10 @@ const availableTables = computed(() => {
:data-order="table.order" :data-order="table.order"
:data-id="table.id" :data-id="table.id"
:table="table" :table="table"
:project="project" :base="base"
:base-index="baseIndex" :source-index="sourceIndex"
:data-title="table.title" :data-title="table.title"
:data-base-id="base?.id" :data-source-id="source?.id"
:data-type="table.type" :data-type="table.type"
> >
</TableNode> </TableNode>

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

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ProjectType, TableType } from 'nocodb-sdk' import type { BaseType, TableType } from 'nocodb-sdk'
import { toRef } from '@vue/reactivity' import { toRef } from '@vue/reactivity'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
@ -9,19 +9,19 @@ import { ProjectRoleInj, TreeViewInj, useRoles, useTabs } from '#imports'
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
project: ProjectType base: BaseType
table: TableType table: TableType
baseIndex: number sourceIndex: number
}>(), }>(),
{ baseIndex: 0 }, { sourceIndex: 0 },
) )
const project = toRef(props, 'project') const base = toRef(props, 'base')
const table = toRef(props, 'table') const table = toRef(props, 'table')
const baseIndex = toRef(props, 'baseIndex') const sourceIndex = toRef(props, 'sourceIndex')
const { openTable: _openTable } = useTableNew({ const { openTable: _openTable } = useTableNew({
projectId: project.value.id!, baseId: base.value.id!,
}) })
const route = useRoute() const route = useRoute()
@ -36,10 +36,10 @@ const { updateTab } = tabStore
const { $e, $api } = useNuxtApp() const { $e, $api } = useNuxtApp()
useTableNew({ useTableNew({
projectId: project.value.id!, baseId: base.value.id!,
}) })
const projectRole = inject(ProjectRoleInj) const baseRole = inject(ProjectRoleInj)
provide(SidebarTableInj, table) provide(SidebarTableInj, table)
const { setMenuContext, openRenameTableDialog, duplicateTable } = inject(TreeViewInj)! const { setMenuContext, openRenameTableDialog, duplicateTable } = inject(TreeViewInj)!
@ -49,8 +49,8 @@ const { activeView } = storeToRefs(useViewsStore())
const { isLeftSidebarOpen } = storeToRefs(useSidebarStore()) const { isLeftSidebarOpen } = storeToRefs(useSidebarStore())
// todo: temp // todo: temp
const { projectTables } = storeToRefs(useTablesStore()) const { baseTables } = storeToRefs(useTablesStore())
const tables = computed(() => projectTables.value.get(project.value.id!) ?? []) const tables = computed(() => baseTables.value.get(base.value.id!) ?? [])
const openedTableId = computed(() => route.params.viewId) const openedTableId = computed(() => route.params.viewId)
@ -78,11 +78,11 @@ const setIcon = async (icon: string, table: TableType) => {
// Todo: temp // Todo: temp
const { isSharedBase } = useProject() const { isSharedBase } = useBase()
// const isMultiBase = computed(() => project.bases && project.bases.length > 1) // const isMultiBase = computed(() => base.sources && base.sources.length > 1)
const canUserEditEmote = computed(() => { const canUserEditEmote = computed(() => {
return isUIAllowed('tableIconEdit', { roles: projectRole?.value }) return isUIAllowed('tableIconEdit', { roles: baseRole?.value })
}) })
const isExpanded = ref(false) const isExpanded = ref(false)
@ -146,15 +146,15 @@ const isTableOpened = computed(() => {
:data-order="table.order" :data-order="table.order"
:data-id="table.id" :data-id="table.id"
:data-table-id="table.id" :data-table-id="table.id"
:class="[`nc-project-tree-tbl nc-project-tree-tbl-${table.title}`]" :class="[`nc-base-tree-tbl nc-base-tree-tbl-${table.title}`]"
:data-active="openedTableId === table.id" :data-active="openedTableId === table.id"
> >
<GeneralTooltip <GeneralTooltip
class="nc-tree-item-inner nc-sidebar-node pl-11 pr-0.75 mb-0.25 rounded-md h-7.1 w-full group cursor-pointer hover:bg-gray-200" class="nc-tree-item-inner nc-sidebar-node pl-11 pr-0.75 mb-0.25 rounded-md h-7.1 w-full group cursor-pointer hover:bg-gray-200"
:class="{ :class="{
'hover:bg-gray-200': openedTableId !== table.id, 'hover:bg-gray-200': openedTableId !== table.id,
'pl-12 xs:(pl-14)': baseIndex !== 0, 'pl-12 xs:(pl-14)': sourceIndex !== 0,
'pl-6.5': baseIndex === 0, 'pl-6.5': sourceIndex === 0,
'!bg-primary-selected': isTableOpened, '!bg-primary-selected': isTableOpened,
}" }"
modifier-key="Alt" modifier-key="Alt"
@ -178,7 +178,7 @@ const isTableOpened = computed(() => {
> >
<GeneralIcon <GeneralIcon
icon="triangleFill" icon="triangleFill"
class="nc-sidebar-base-node-btns group-hover:visible invisible cursor-pointer transform transition-transform duration-500 h-1.5 w-1.5 !text-gray-600 rotate-90" class="nc-sidebar-source-node-btns group-hover:visible invisible cursor-pointer transform transition-transform duration-500 h-1.5 w-1.5 !text-gray-600 rotate-90"
:class="{ '!rotate-180': isExpanded }" :class="{ '!rotate-180': isExpanded }"
/> />
</NcButton> </NcButton>
@ -204,12 +204,12 @@ const isTableOpened = computed(() => {
<template #title> <template #title>
{{ $t('general.changeIcon') }} {{ $t('general.changeIcon') }}
</template> </template>
<component
:is="iconMap.table" <MdiTable
v-if="table.type === 'table'" v-if="table.type === 'table'"
class="flex w-5 !text-gray-500 text-sm" class="flex w-5 !text-gray-500 text-sm"
:class="{ :class="{
'group-hover:text-gray-500': isUIAllowed('tableSort', { roles: projectRole }), 'group-hover:text-gray-500': isUIAllowed('tableSort', { roles: baseRole }),
'!text-black': openedTableId === table.id, '!text-black': openedTableId === table.id,
}" }"
/> />
@ -217,7 +217,7 @@ const isTableOpened = computed(() => {
v-else v-else
class="flex w-5 !text-gray-500 text-sm" class="flex w-5 !text-gray-500 text-sm"
:class="{ :class="{
'group-hover:text-gray-500': isUIAllowed('tableSort', { roles: projectRole }), 'group-hover:text-gray-500': isUIAllowed('tableSort', { roles: baseRole }),
'!text-black': openedTableId === table.id, '!text-black': openedTableId === table.id,
}" }"
/> />
@ -243,7 +243,7 @@ const isTableOpened = computed(() => {
<NcDropdown <NcDropdown
v-if=" v-if="
!isSharedBase && !isSharedBase &&
(isUIAllowed('tableRename', { roles: projectRole }) || isUIAllowed('tableDelete', { roles: projectRole })) (isUIAllowed('tableRename', { roles: baseRole }) || isUIAllowed('tableDelete', { roles: baseRole }))
" "
v-e="['c:table:option']" v-e="['c:table:option']"
:trigger="['click']" :trigger="['click']"
@ -258,10 +258,10 @@ const isTableOpened = computed(() => {
<template #overlay> <template #overlay>
<NcMenu> <NcMenu>
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('tableRename', { roles: projectRole })"
v-e="['c:table:rename']" v-e="['c:table:rename']"
v-if="isUIAllowed('tableRename', { roles: baseRole })"
:data-testid="`sidebar-table-rename-${table.title}`" :data-testid="`sidebar-table-rename-${table.title}`"
@click="openRenameTableDialog(table, project.bases[baseIndex].id)" @click="openRenameTableDialog(table, base.sources[sourceIndex].id)"
> >
<GeneralIcon icon="edit" class="text-gray-700" /> <GeneralIcon icon="edit" class="text-gray-700" />
{{ $t('general.rename') }} {{ $t('general.rename') }}
@ -270,8 +270,8 @@ const isTableOpened = computed(() => {
<NcMenuItem <NcMenuItem
v-if=" v-if="
isUIAllowed('tableDuplicate') && isUIAllowed('tableDuplicate') &&
project.bases?.[baseIndex] && base.sources?.[sourceIndex] &&
(project.bases[baseIndex].is_meta || project.bases[baseIndex].is_local) (base.sources[sourceIndex].is_meta || base.sources[sourceIndex].is_local)
" "
v-e="['c:table:duplicate']" v-e="['c:table:duplicate']"
:data-testid="`sidebar-table-duplicate-${table.title}`" :data-testid="`sidebar-table-duplicate-${table.title}`"
@ -282,7 +282,7 @@ const isTableOpened = computed(() => {
</NcMenuItem> </NcMenuItem>
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('tableDelete', { roles: projectRole })" v-if="isUIAllowed('tableDelete', { roles: baseRole })"
v-e="['c:table:delete']" v-e="['c:table:delete']"
:data-testid="`sidebar-table-delete-${table.title}`" :data-testid="`sidebar-table-delete-${table.title}`"
class="!text-red-500 !hover:bg-red-50" class="!text-red-500 !hover:bg-red-50"
@ -310,13 +310,13 @@ const isTableOpened = computed(() => {
</div> </div>
</div> </div>
<DlgTableDelete <DlgTableDelete
v-if="table.id && project?.id" v-if="table.id && base?.id"
v-model:visible="isTableDeleteDialogVisible" v-model:visible="isTableDeleteDialogVisible"
:table-id="table.id" :table-id="table.id"
:project-id="project.id" :base-id="base.id"
/> />
</GeneralTooltip> </GeneralTooltip>
<DashboardTreeViewViewsList v-if="isExpanded" :table-id="table.id" :project-id="project.id" /> <DashboardTreeViewViewsList v-if="isExpanded" :table-id="table.id" :base-id="base.id" />
</div> </div>
</template> </template>

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

@ -28,7 +28,7 @@ interface Emits {
} }
const emits = defineEmits<Emits>() const emits = defineEmits<Emits>()
const project = inject(ProjectInj)! const base = inject(ProjectInj)!
const table = inject(SidebarTableInj)! const table = inject(SidebarTableInj)!
const { isLeftSidebarOpen } = storeToRefs(useSidebarStore()) const { isLeftSidebarOpen } = storeToRefs(useSidebarStore())
@ -39,10 +39,10 @@ const { $e } = useNuxtApp()
const { t } = useI18n() const { t } = useI18n()
const isDefaultBase = computed(() => { const isDefaultBase = computed(() => {
const base = project.value?.bases?.find((b) => b.id === table.value.base_id) const source = base.value?.sources?.find((b) => b.id === table.value.source_id)
if (!base) return false if (!source) return false
return _isDefaultBase(base) return _isDefaultBase(source)
}) })
const { viewsByTable, activeView, recentViews } = storeToRefs(useViewsStore()) const { viewsByTable, activeView, recentViews } = storeToRefs(useViewsStore())
@ -197,7 +197,7 @@ async function changeView(view: ViewType) {
await navigateToView({ await navigateToView({
view, view,
tableId: table.value.id!, tableId: table.value.id!,
projectId: project.value.id!, baseId: base.value.id!,
hardReload: view.type === ViewTypes.FORM && selected.value[0] === view.id, hardReload: view.type === ViewTypes.FORM && selected.value[0] === view.id,
}) })
@ -217,7 +217,7 @@ async function onRename(view: ViewType, originalTitle?: string, undo = false) {
navigateToView({ navigateToView({
view, view,
tableId: table.value.id!, tableId: table.value.id!,
projectId: project.value.id!, baseId: base.value.id!,
hardReload: view.type === ViewTypes.FORM && selected.value[0] === view.id, hardReload: view.type === ViewTypes.FORM && selected.value[0] === view.id,
}) })
@ -272,12 +272,12 @@ function openDeleteDialog(view: ViewType) {
emits('deleted') emits('deleted')
removeFromRecentViews({ viewId: view.id, tableId: view.fk_model_id, projectId: project.value.id }) removeFromRecentViews({ viewId: view.id, tableId: view.fk_model_id, baseId: base.value.id })
refreshCommandPalette() refreshCommandPalette()
if (activeView.value?.id === view.id) { if (activeView.value?.id === view.id) {
navigateToTable({ navigateToTable({
tableId: table.value.id!, tableId: table.value.id!,
projectId: project.value.id!, baseId: base.value.id!,
}) })
} }
@ -355,7 +355,7 @@ function onOpenModal({
navigateToView({ navigateToView({
view, view,
tableId: table.value.id!, tableId: table.value.id!,
projectId: project.value.id!, baseId: base.value.id!,
hardReload: view.type === ViewTypes.FORM && selected.value[0] === view.id, hardReload: view.type === ViewTypes.FORM && selected.value[0] === view.id,
}) })

8
packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue

@ -45,17 +45,17 @@ const { isMobileMode } = useGlobal()
const { isUIAllowed } = useRoles() const { isUIAllowed } = useRoles()
const project = inject(ProjectInj, ref()) const base = inject(ProjectInj, ref())
const activeView = inject(ActiveViewInj, ref()) const activeView = inject(ActiveViewInj, ref())
const isLocked = inject(IsLockedInj, ref(false)) const isLocked = inject(IsLockedInj, ref(false))
const isDefaultBase = computed(() => { const isDefaultBase = computed(() => {
const base = project.value?.bases?.find((b) => b.id === vModel.value.base_id) const source = base.value?.sources?.find((b) => b.id === vModel.value.source_id)
if (!base) return false if (!source) return false
return _isDefaultBase(base) return _isDefaultBase(source)
}) })
const isDropdownOpen = ref(false) const isDropdownOpen = ref(false)

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

@ -18,8 +18,8 @@ import {
storeToRefs, storeToRefs,
useDialog, useDialog,
useNuxtApp, useNuxtApp,
useProject, useBase,
useProjects, useBases,
useRoles, useRoles,
useTablesStore, useTablesStore,
useTabs, useTabs,
@ -37,23 +37,23 @@ const router = useRouter()
const route = router.currentRoute const route = router.currentRoute
const projectsStore = useProjects() const basesStore = useBases()
const { createProject: _createProject } = projectsStore const { createProject: _createProject } = basesStore
const { projects, projectsList, activeProjectId } = storeToRefs(projectsStore) const { bases, basesList, activeProjectId } = storeToRefs(basesStore)
const { isWorkspaceLoading } = storeToRefs(useWorkspace()) const { isWorkspaceLoading } = storeToRefs(useWorkspace())
const { openTable } = useTablesStore() const { openTable } = useTablesStore()
const projectCreateDlg = ref(false) const baseCreateDlg = ref(false)
const projectStore = useProject() const baseStore = useBase()
const { loadTables } = projectStore const { loadTables } = baseStore
const { tables } = storeToRefs(projectStore) const { tables } = storeToRefs(baseStore)
const { t } = useI18n() const { t } = useI18n()
@ -61,15 +61,15 @@ const { activeTable: _activeTable } = storeToRefs(useTablesStore())
const { refreshCommandPalette } = useCommandPalette() const { refreshCommandPalette } = useCommandPalette()
const contextMenuTarget = reactive<{ type?: 'project' | 'base' | 'table' | 'main' | 'layout'; value?: any }>({}) const contextMenuTarget = reactive<{ type?: 'base' | 'source' | 'table' | 'main' | 'layout'; value?: any }>({})
const setMenuContext = (type: 'project' | 'base' | 'table' | 'main' | 'layout', value?: any) => { const setMenuContext = (type: 'base' | 'source' | 'table' | 'main' | 'layout', value?: any) => {
contextMenuTarget.type = type contextMenuTarget.type = type
contextMenuTarget.value = value contextMenuTarget.value = value
} }
function openRenameTableDialog(table: TableType, rightClick = false) { function openRenameTableDialog(table: TableType, rightClick = false) {
if (!table || !table.base_id) return if (!table || !table.source_id) return
$e('c:table:rename') $e('c:table:rename')
@ -78,7 +78,7 @@ function openRenameTableDialog(table: TableType, rightClick = false) {
const { close } = useDialog(resolveComponent('DlgTableRename'), { const { close } = useDialog(resolveComponent('DlgTableRename'), {
'modelValue': isOpen, 'modelValue': isOpen,
'tableMeta': table, 'tableMeta': table,
'baseId': table.base_id, // || bases.value[0].id, 'sourceId': table.source_id, // || sources.value[0].id,
'onUpdate:modelValue': closeDialog, 'onUpdate:modelValue': closeDialog,
}) })
@ -89,8 +89,8 @@ function openRenameTableDialog(table: TableType, rightClick = false) {
} }
} }
function openTableCreateDialog(baseId?: string, projectId?: string) { function openTableCreateDialog(sourceId?: string, baseId?: string) {
if (!baseId && !(projectId || projectsList.value[0].id)) return if (!sourceId && !(baseId || basesList.value[0].id)) return
$e('c:table:create:navdraw') $e('c:table:create:navdraw')
@ -98,8 +98,8 @@ function openTableCreateDialog(baseId?: string, projectId?: string) {
const { close } = useDialog(resolveComponent('DlgTableCreate'), { const { close } = useDialog(resolveComponent('DlgTableCreate'), {
'modelValue': isOpen, 'modelValue': isOpen,
'baseId': baseId, // || bases.value[0].id, 'sourceId': sourceId, // || sources.value[0].id,
'projectId': projectId || projectsList.value[0].id, 'baseId': baseId || basesList.value[0].id,
'onUpdate:modelValue': closeDialog, 'onUpdate:modelValue': closeDialog,
}) })
@ -111,7 +111,7 @@ function openTableCreateDialog(baseId?: string, projectId?: string) {
} }
const duplicateTable = async (table: TableType) => { const duplicateTable = async (table: TableType) => {
if (!table || !table.id || !table.project_id) return if (!table || !table.id || !table.base_id) return
const isOpen = ref(true) const isOpen = ref(true)
@ -182,17 +182,17 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
// prevent the key `T` is inputted to table title input // prevent the key `T` is inputted to table title input
e.preventDefault() e.preventDefault()
$e('c:shortcut', { key: 'ALT + T' }) $e('c:shortcut', { key: 'ALT + T' })
const projectId = activeProjectId.value const baseId = activeProjectId.value
const project = projectId ? projects.value.get(projectId) : undefined const base = baseId ? bases.value.get(baseId) : undefined
if (!project) return if (!base) return
if (projectId) openTableCreateDialog(project.bases?.[0].id, projectId) if (baseId) openTableCreateDialog(base.sources?.[0].id, baseId)
} }
break break
} }
// ALT + L - only show active project // ALT + L - only show active base
case 76: { case 76: {
if (route.value.params.projectId) { if (route.value.params.baseId) {
router.push({ router.push({
query: { query: {
...route.value.query, ...route.value.query,
@ -205,7 +205,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
// ALT + D // ALT + D
case 68: { case 68: {
e.stopPropagation() e.stopPropagation()
projectCreateDlg.value = true baseCreateDlg.value = true
break break
} }
} }
@ -213,7 +213,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
}) })
const handleContext = (e: MouseEvent) => { const handleContext = (e: MouseEvent) => {
if (!document.querySelector('.base-context, .table-context')?.contains(e.target as Node)) { if (!document.querySelector('.source-context, .table-context')?.contains(e.target as Node)) {
setMenuContext('main') setMenuContext('main')
} }
} }
@ -253,7 +253,7 @@ watch(
watch( watch(
activeProjectId, activeProjectId,
() => { () => {
const activeProjectDom = document.querySelector(`.nc-treeview [data-project-id="${activeProjectId.value}"]`) const activeProjectDom = document.querySelector(`.nc-treeview [data-base-id="${activeProjectId.value}"]`)
if (!activeProjectDom) return if (!activeProjectDom) return
if (isElementInvisible(activeProjectDom)) { if (isElementInvisible(activeProjectDom)) {
@ -271,15 +271,15 @@ watch(
<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 class="text-gray-500 font-medium pl-3.5 mb-1">{{ $t('objects.projects') }}</div> <div class="text-gray-500 font-medium pl-3.5 mb-1">{{ $t('objects.projects') }}</div>
<div 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="basesList?.length">
<ProjectWrapper v-for="project of projectsList" :key="project.id" :project-role="project.project_role" :project="project"> <ProjectWrapper v-for="base of basesList" :key="base.id" :base-role="base.project_role" :base="base">
<DashboardTreeViewProjectNode /> <DashboardTreeViewProjectNode />
</ProjectWrapper> </ProjectWrapper>
</template> </template>
<WorkspaceEmptyPlaceholder v-else-if="!isWorkspaceLoading" /> <WorkspaceEmptyPlaceholder v-else-if="!isWorkspaceLoading" />
</div> </div>
<WorkspaceCreateProjectDlg v-model="projectCreateDlg" /> <WorkspaceCreateProjectDlg v-model="baseCreateDlg" />
</div> </div>
</template> </template>

10
packages/nc-gui/components/dashboard/settings/AuditTab.vue

@ -1,14 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { Tooltip as ATooltip, Empty } from 'ant-design-vue' import { Tooltip as ATooltip, Empty } from 'ant-design-vue'
import type { AuditType } from 'nocodb-sdk' import type { AuditType } from 'nocodb-sdk'
import { ProjectIdInj, h, iconMap, onMounted, storeToRefs, timeAgo, useGlobal, useI18n, useNuxtApp, useProject } from '#imports' import { ProjectIdInj, h, iconMap, onMounted, storeToRefs, timeAgo, useGlobal, useI18n, useNuxtApp, useBase } from '#imports'
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const { project } = storeToRefs(useProject()) const { base } = storeToRefs(useBase())
const _projectId = inject(ProjectIdInj, undefined) const _projectId = inject(ProjectIdInj, undefined)
const projectId = computed(() => _projectId.value ?? project.value?.id) const baseId = computed(() => _projectId.value ?? base.value?.id)
const { t } = useI18n() const { t } = useI18n()
@ -26,11 +26,11 @@ const { appInfo } = useGlobal()
async function loadAudits(page = currentPage.value, limit = currentLimit.value) { async function loadAudits(page = currentPage.value, limit = currentLimit.value) {
try { try {
if (!project.value?.id) return if (!base.value?.id) return
isLoading.value = true isLoading.value = true
const { list, pageInfo } = await $api.project.auditList(projectId.value, { const { list, pageInfo } = await $api.base.auditList(baseId.value, {
offset: limit * (page - 1), offset: limit * (page - 1),
limit, limit,
}) })

16
packages/nc-gui/components/dashboard/settings/BaseAudit.vue

@ -1,17 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { Tooltip as ATooltip, Empty } from 'ant-design-vue' import { Tooltip as ATooltip, Empty } from 'ant-design-vue'
import type { AuditType } from 'nocodb-sdk' import type { AuditType } from 'nocodb-sdk'
import { h, iconMap, onMounted, storeToRefs, timeAgo, useGlobal, useI18n, useNuxtApp, useProject } from '#imports' import { h, iconMap, onMounted, storeToRefs, timeAgo, useGlobal, useI18n, useNuxtApp, useBase } from '#imports'
interface Props { interface Props {
baseId: string sourceId: string
} }
const props = defineProps<Props>() const props = defineProps<Props>()
const projectStore = useProject() const baseStore = useBase()
const { project } = storeToRefs(projectStore) const { base } = storeToRefs(baseStore)
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
@ -31,14 +31,14 @@ const { appInfo } = useGlobal()
async function loadAudits(page = currentPage.value, limit = currentLimit.value) { async function loadAudits(page = currentPage.value, limit = currentLimit.value) {
try { try {
if (!props.baseId) return if (!props.sourceId) return
isLoading.value = true isLoading.value = true
const { list, pageInfo } = await $api.project.auditList(project.value.id!, { const { list, pageInfo } = await $api.base.auditList(base.value.id!, {
offset: limit * (page - 1), offset: limit * (page - 1),
limit, limit,
baseId: props.baseId, sourceId: props.sourceId,
}) })
audits.value = list audits.value = list
@ -106,7 +106,7 @@ const columns = [
<div class="flex flex-col gap-4 w-full"> <div class="flex flex-col gap-4 w-full">
<div v-if="!appInfo.auditEnabled" class="text-red-500">Audit logs are currently disabled by administrators.</div> <div v-if="!appInfo.auditEnabled" class="text-red-500">Audit logs are currently disabled by administrators.</div>
<div class="flex flex-row justify-between items-center"> <div class="flex flex-row justify-between items-center">
<h6 class="mb-4 first-letter:capital font-bold">Audit : {{ project.title }}</h6> <h6 class="mb-4 first-letter:capital font-bold">Audit : {{ base.title }}</h6>
<a-button class="self-start !rounded-md" @click="loadAudits"> <a-button class="self-start !rounded-md" @click="loadAudits">
<!-- Reload --> <!-- Reload -->
<div class="flex items-center gap-2 text-gray-600 font-light"> <div class="flex items-center gap-2 text-gray-600 font-light">

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

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import Draggable from 'vuedraggable' import Draggable from 'vuedraggable'
import type { BaseType } from 'nocodb-sdk' import type { SourceType } from 'nocodb-sdk'
import { ClientType, DataSourcesSubTab, storeToRefs, useCommandPalette, useNuxtApp, useProject } from '#imports' import { ClientType, DataSourcesSubTab, storeToRefs, useCommandPalette, useNuxtApp, useBase } from '#imports'
interface Props { interface Props {
state: string state: string
@ -20,16 +20,16 @@ const { $api, $e } = useNuxtApp()
const { t } = useI18n() const { t } = useI18n()
const { loadProject } = useProjects() const { loadProject } = useBases()
const projectStore = useProject() const baseStore = useBase()
const { project } = storeToRefs(projectStore) const { base } = storeToRefs(baseStore)
const { projectPageTab } = storeToRefs(useConfigStore()) const { projectPageTab } = storeToRefs(useConfigStore())
const { refreshCommandPalette } = useCommandPalette() const { refreshCommandPalette } = useCommandPalette()
const sources = ref<BaseType[]>([]) const sources = ref<SourceType[]>([])
const activeBaseId = ref('') const activeBaseId = ref('')
@ -42,16 +42,16 @@ const forceAwakened = ref(false)
const dataSourcesAwakened = ref(false) const dataSourcesAwakened = ref(false)
const isDeleteBaseModalOpen = ref(false) const isDeleteBaseModalOpen = ref(false)
const toBeDeletedBase = ref<BaseType | undefined>() const toBeDeletedBase = ref<SourceType | undefined>()
async function loadBases(changed?: boolean) { async function loadBases(changed?: boolean) {
try { try {
if (changed) refreshCommandPalette() if (changed) refreshCommandPalette()
await until(() => !!project.value.id).toBeTruthy() await until(() => !!base.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.source.list(base.value.id as string)
if (baseList.list && baseList.list.length) { if (baseList.list && baseList.list.length) {
sources.value = baseList.list sources.value = baseList.list
} }
@ -63,46 +63,46 @@ async function loadBases(changed?: boolean) {
} }
} }
const baseAction = (baseId?: string, action?: string) => { const baseAction = (sourceId?: string, action?: string) => {
if (!baseId) return if (!sourceId) return
activeBaseId.value = baseId activeBaseId.value = sourceId
vState.value = action || '' vState.value = action || ''
} }
const openDeleteBase = (base: BaseType) => { const openDeleteBase = (source: SourceType) => {
$e('c:base:delete') $e('c:source:delete')
isDeleteBaseModalOpen.value = true isDeleteBaseModalOpen.value = true
toBeDeletedBase.value = base toBeDeletedBase.value = source
} }
const deleteBase = async () => { const deleteBase = async () => {
if (!toBeDeletedBase.value) return if (!toBeDeletedBase.value) return
try { try {
await $api.base.delete(toBeDeletedBase.value.project_id as string, toBeDeletedBase.value.id as string) await $api.source.delete(toBeDeletedBase.value.base_id as string, toBeDeletedBase.value.id as string)
$e('a:base:delete') $e('a:source:delete')
sources.value.splice(sources.value.indexOf(toBeDeletedBase.value), 1) sources.value.splice(sources.value.indexOf(toBeDeletedBase.value), 1)
await loadProject(project.value.id as string, true) await loadProject(base.value.id as string, true)
} catch (e: any) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} }
} }
const toggleBase = async (base: BaseType, state: boolean) => { const toggleBase = async (source: BaseType, state: boolean) => {
try { try {
if (!state && sources.value.filter((src) => src.enabled).length < 2) { if (!state && sources.value.filter((src) => src.enabled).length < 2) {
message.info('There should be at least one enabled base!') message.info('There should be at least one enabled source!')
return return
} }
base.enabled = state source.enabled = state
await $api.base.update(base.project_id as string, base.id as string, { await $api.source.update(source.base_id as string, source.id as string, {
id: base.id, id: source.id,
project_id: base.project_id, base_id: source.base_id,
enabled: base.enabled, enabled: source.enabled,
}) })
await loadProject(project.value.id as string, true) await loadProject(base.value.id as string, true)
} catch (e: any) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} }
@ -112,24 +112,24 @@ const moveBase = async (e: any) => {
try { try {
if (e.oldIndex === e.newIndex) return if (e.oldIndex === e.newIndex) return
// sources list is mutated so we have to get the new index and mirror it to backend // sources list is mutated so we have to get the new index and mirror it to backend
const base = sources.value[e.newIndex] const source = sources.value[e.newIndex]
if (base) { if (source) {
if (!base.order) { if (!source.order) {
// empty update call to reorder bases (migration) // empty update call to reorder sources (migration)
await $api.base.update(base.project_id as string, base.id as string, { await $api.source.update(source.base_id as string, source.id as string, {
id: base.id, id: source.id,
project_id: base.project_id, base_id: source.base_id,
}) })
message.info(t('info.basesMigrated')) message.info(t('info.basesMigrated'))
} else { } else {
await $api.base.update(base.project_id as string, base.id as string, { await $api.source.update(source.base_id as string, source.id as string, {
id: base.id, id: source.id,
project_id: base.project_id, base_id: source.base_id,
order: e.newIndex + 1, order: e.newIndex + 1,
}) })
} }
} }
await loadProject(project.value.id as string, true) await loadProject(base.value.id as string, true)
await loadBases() await loadBases()
} catch (e: any) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
@ -399,32 +399,32 @@ const isEditBaseModalOpen = computed({
</div> </div>
</div> </div>
</template> </template>
<template #item="{ element: base, index }"> <template #item="{ element: source, index }">
<div v-if="index !== 0" class="ds-table-row border-gray-200"> <div v-if="index !== 0" class="ds-table-row border-gray-200">
<div class="ds-table-col ds-table-enabled"> <div class="ds-table-col ds-table-enabled">
<div class="flex items-center gap-1 cursor-pointer"> <div class="flex items-center gap-1 cursor-pointer">
<a-tooltip> <a-tooltip>
<template #title> <template #title>
<template v-if="base.enabled">{{ $t('activity.hideInUI') }}</template> <template v-if="source.enabled">{{ $t('activity.hideInUI') }}</template>
<template v-else>{{ $t('activity.showInUI') }}</template> <template v-else>{{ $t('activity.showInUI') }}</template>
</template> </template>
<a-switch :checked="base.enabled ? true : false" @change="toggleBase(base, $event)" /> <a-switch :checked="source.enabled ? true : false" @change="toggleBase(source, $event)" />
</a-tooltip> </a-tooltip>
</div> </div>
</div> </div>
<div class="ds-table-col ds-table-name font-medium"> <div class="ds-table-col ds-table-name font-medium">
<GeneralIcon v-if="sources.length > 2" icon="dragVertical" small class="ds-table-handle" /> <GeneralIcon v-if="sources.length > 2" icon="dragVertical" small class="ds-table-handle" />
<div v-if="base.is_meta || base.is_local">-</div> <div v-if="source.is_meta || source.is_local">-</div>
<div v-else class="flex items-center gap-1"> <div v-else class="flex items-center gap-1">
{{ base.is_meta || base.is_local ? $t('general.base') : base.alias }} {{ source.is_meta || source.is_local ? $t('general.base') : source.alias }}
</div> </div>
</div> </div>
<div class="ds-table-col ds-table-type"> <div class="ds-table-col ds-table-type">
<GeneralIcon v-if="sources.length > 2" icon="dragVertical" small class="ds-table-handle" /> <GeneralIcon v-if="sources.length > 2" icon="dragVertical" small class="ds-table-handle" />
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<GeneralBaseLogo :base-type="base.type" /> <GeneralBaseLogo :source-type="source.type" />
<span class="text-gray-700 capitalize">{{ base.type }}</span> <span class="text-gray-700 capitalize">{{ source.type }}</span>
</div> </div>
</div> </div>
@ -433,7 +433,7 @@ const isEditBaseModalOpen = computed({
<a-button <a-button
class="nc-action-btn cursor-pointer outline-0" class="nc-action-btn cursor-pointer outline-0"
type="text" type="text"
@click="baseAction(base.id, DataSourcesSubTab.ERD)" @click="baseAction(source.id, DataSourcesSubTab.ERD)"
> >
<div class="flex items-center gap-2 text-gray-600"> <div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="erd" class="group-hover:text-accent" /> <GeneralIcon icon="erd" class="group-hover:text-accent" />
@ -443,7 +443,7 @@ const isEditBaseModalOpen = computed({
<a-button <a-button
type="text" type="text"
class="nc-action-btn cursor-pointer outline-0" class="nc-action-btn cursor-pointer outline-0"
@click="baseAction(base.id, DataSourcesSubTab.UIAcl)" @click="baseAction(source.id, DataSourcesSubTab.UIAcl)"
> >
<div class="flex items-center gap-2 text-gray-600"> <div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="acl" class="group-hover:text-accent" /> <GeneralIcon icon="acl" class="group-hover:text-accent" />
@ -451,10 +451,10 @@ const isEditBaseModalOpen = computed({
</div> </div>
</a-button> </a-button>
<a-button <a-button
v-if="!base.is_meta && !base.is_local" v-if="!source.is_meta && !source.is_local"
type="text" type="text"
class="nc-action-btn cursor-pointer outline-0" class="nc-action-btn cursor-pointer outline-0"
@click="baseAction(base.id, DataSourcesSubTab.Metadata)" @click="baseAction(source.id, DataSourcesSubTab.Metadata)"
> >
<div class="flex items-center gap-2 text-gray-600"> <div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="sync" class="group-hover:text-accent" /> <GeneralIcon icon="sync" class="group-hover:text-accent" />
@ -465,18 +465,18 @@ const isEditBaseModalOpen = computed({
</div> </div>
<div class="ds-table-col ds-table-crud justify-end gap-x-1"> <div class="ds-table-col ds-table-crud justify-end gap-x-1">
<a-button <a-button
v-if="!base.is_meta && !base.is_local" v-if="!source.is_meta && !source.is_local"
class="nc-action-btn cursor-pointer outline-0 !w-8 !px-1 !rounded-lg mt-0.5" class="nc-action-btn cursor-pointer outline-0 !w-8 !px-1 !rounded-lg mt-0.5"
type="text" type="text"
@click="baseAction(base.id, DataSourcesSubTab.Edit)" @click="baseAction(source.id, DataSourcesSubTab.Edit)"
> >
<GeneralIcon icon="edit" class="text-gray-600 -mt-0.5" /> <GeneralIcon icon="edit" class="text-gray-600 -mt-0.5" />
</a-button> </a-button>
<a-button <a-button
v-if="!base.is_meta && !base.is_local" v-if="!source.is_meta && !source.is_local"
class="nc-action-btn cursor-pointer outline-0 !w-8 !px-1 !rounded-lg mt-0.5" class="nc-action-btn cursor-pointer outline-0 !w-8 !px-1 !rounded-lg mt-0.5"
type="text" type="text"
@click="openDeleteBase(base)" @click="openDeleteBase(source)"
> >
<GeneralIcon icon="delete" class="text-red-500 -mt-0.5" /> <GeneralIcon icon="delete" class="text-red-500 -mt-0.5" />
</a-button> </a-button>
@ -490,44 +490,44 @@ const isEditBaseModalOpen = computed({
<div class="py-6 px-8"> <div class="py-6 px-8">
<LazyDashboardSettingsDataSourcesCreateBase <LazyDashboardSettingsDataSourcesCreateBase
:connection-type="clientType" :connection-type="clientType"
@base-created="loadBases(true)" @source-created="loadBases(true)"
@close="isNewBaseModalOpen = false" @close="isNewBaseModalOpen = false"
/> />
</div> </div>
</GeneralModal> </GeneralModal>
<GeneralModal v-model:visible="isErdModalOpen" size="large"> <GeneralModal v-model:visible="isErdModalOpen" size="large">
<div class="h-[80vh]"> <div class="h-[80vh]">
<LazyDashboardSettingsErd :base-id="activeBaseId" /> <LazyDashboardSettingsErd :source-id="activeBaseId" />
</div> </div>
</GeneralModal> </GeneralModal>
<GeneralModal v-model:visible="isMetaDataModal" size="medium"> <GeneralModal v-model:visible="isMetaDataModal" size="medium">
<div class="p-6"> <div class="p-6">
<LazyDashboardSettingsMetadata :base-id="activeBaseId" @base-synced="loadBases(true)" /> <LazyDashboardSettingsMetadata :source-id="activeBaseId" @source-synced="loadBases(true)" />
</div> </div>
</GeneralModal> </GeneralModal>
<GeneralModal v-model:visible="isUIAclModalOpen" class="!w-[60rem]"> <GeneralModal v-model:visible="isUIAclModalOpen" class="!w-[60rem]">
<div class="p-6"> <div class="p-6">
<LazyDashboardSettingsUIAcl :base-id="activeBaseId" /> <LazyDashboardSettingsUIAcl :source-id="activeBaseId" />
</div> </div>
</GeneralModal> </GeneralModal>
<GeneralModal v-model:visible="isEditBaseModalOpen" closable :mask-closable="false" size="medium"> <GeneralModal v-model:visible="isEditBaseModalOpen" closable :mask-closable="false" size="medium">
<div class="p-6"> <div class="p-6">
<LazyDashboardSettingsDataSourcesEditBase <LazyDashboardSettingsDataSourcesEditBase
:base-id="activeBaseId" :source-id="activeBaseId"
@base-updated="loadBases(true)" @source-updated="loadBases(true)"
@close="isEditBaseModalOpen = false" @close="isEditBaseModalOpen = false"
/> />
</div> </div>
</GeneralModal> </GeneralModal>
<GeneralModal v-model:visible="isBaseAuditModalOpen" class="!w-[70rem]"> <GeneralModal v-model:visible="isBaseAuditModalOpen" class="!w-[70rem]">
<div class="p-6"> <div class="p-6">
<LazyDashboardSettingsBaseAudit :base-id="activeBaseId" @close="isBaseAuditModalOpen = false" /> <LazyDashboardSettingsBaseAudit :source-id="activeBaseId" @close="isBaseAuditModalOpen = false" />
</div> </div>
</GeneralModal> </GeneralModal>
<GeneralDeleteModal v-model:visible="isDeleteBaseModalOpen" :entity-name="$t('general.base')" :on-delete="deleteBase"> <GeneralDeleteModal v-model:visible="isDeleteBaseModalOpen" :entity-name="$t('general.datasource')" :on-delete="deleteBase">
<template #entity-preview> <template #entity-preview>
<div v-if="toBeDeletedBase" class="flex flex-row items-center py-2 px-3.25 bg-gray-50 rounded-lg text-gray-700 mb-4"> <div v-if="toBeDeletedBase" class="flex flex-row items-center py-2 px-3.25 bg-gray-50 rounded-lg text-gray-700 mb-4">
<GeneralBaseLogo :base-type="toBeDeletedBase.type" /> <GeneralBaseLogo :source-type="toBeDeletedBase.type" />
<div <div
class="capitalize text-ellipsis overflow-hidden select-none w-full pl-3" class="capitalize text-ellipsis overflow-hidden select-none w-full pl-3"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }" :style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"

4
packages/nc-gui/components/dashboard/settings/Erd.vue

@ -1,11 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
const props = defineProps<{ const props = defineProps<{
baseId: string sourceId: string
}>() }>()
</script> </script>
<template> <template>
<div class="w-full h-full !p-0"> <div class="w-full h-full !p-0">
<ErdView :base-id="props.baseId" /> <ErdView :source-id="props.sourceId" />
</div> </div>
</template> </template>

20
packages/nc-gui/components/dashboard/settings/Metadata.vue

@ -1,17 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { Empty, extractSdkResponseErrorMsg, h, iconMap, message, storeToRefs, useI18n, useNuxtApp, useProject } from '#imports' import { Empty, extractSdkResponseErrorMsg, h, iconMap, message, storeToRefs, useI18n, useNuxtApp, useBase } from '#imports'
const props = defineProps<{ const props = defineProps<{
baseId: string sourceId: string
}>() }>()
const emit = defineEmits(['baseSynced']) const emit = defineEmits(['baseSynced'])
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const projectStore = useProject() const baseStore = useBase()
const { loadTables } = projectStore const { loadTables } = baseStore
const { project } = storeToRefs(projectStore) const { base } = storeToRefs(baseStore)
const { t } = useI18n() const { t } = useI18n()
@ -23,11 +23,11 @@ const metadiff = ref<any[]>([])
async function loadMetaDiff() { async function loadMetaDiff() {
try { try {
if (!project.value?.id) return if (!base.value?.id) return
isLoading.value = true isLoading.value = true
isDifferent.value = false isDifferent.value = false
metadiff.value = await $api.base.metaDiffGet(project.value?.id, props.baseId) metadiff.value = await $api.source.metaDiffGet(base.value?.id, props.sourceId)
for (const model of metadiff.value) { for (const model of metadiff.value) {
if (model.detectedChanges?.length > 0) { if (model.detectedChanges?.length > 0) {
model.syncState = model.detectedChanges.map((el: any) => el?.msg).join(', ') model.syncState = model.detectedChanges.map((el: any) => el?.msg).join(', ')
@ -45,10 +45,10 @@ const { $poller } = useNuxtApp()
async function syncMetaDiff() { async function syncMetaDiff() {
try { try {
if (!project.value?.id || !isDifferent.value) return if (!base.value?.id || !isDifferent.value) return
isLoading.value = true isLoading.value = true
const jobData = await $api.base.metaDiffSync(project.value?.id, props.baseId) const jobData = await $api.source.metaDiffSync(base.value?.id, props.sourceId)
$poller.subscribe( $poller.subscribe(
{ id: jobData.id }, { id: jobData.id },
@ -165,7 +165,7 @@ const columns = [
<div v-if="column.key === 'table_name'"> <div v-if="column.key === 'table_name'">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<div class="min-w-5 flex items-center justify-center"> <div class="min-w-5 flex items-center justify-center">
<GeneralTableIcon :meta="record" class="text-gray-500" /> <GeneralTableIcon :meta="record" class="text-gray-500"></GeneralTableIcon>
</div> </div>
<span class="overflow-ellipsis min-w-0 shrink-1">{{ record.title || record.table_name }}</span> <span class="overflow-ellipsis min-w-0 shrink-1">{{ record.title || record.table_name }}</span>
</div> </div>

26
packages/nc-gui/components/dashboard/settings/Misc.vue

@ -1,16 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import type { CheckboxChangeEvent } from 'ant-design-vue/es/checkbox/interface' import type { CheckboxChangeEvent } from 'ant-design-vue/es/checkbox/interface'
import { onMounted } from '@vue/runtime-core' import { onMounted } from '@vue/runtime-core'
import { ProjectIdInj, storeToRefs, useGlobal, useProject, watch } from '#imports' import { ProjectIdInj, storeToRefs, useGlobal, useBase, watch } from '#imports'
const { includeM2M, showNull } = useGlobal() const { includeM2M, showNull } = useGlobal()
const projectStore = useProject() const baseStore = useBase()
const projectsStore = useProjects() const basesStore = useBases()
const { loadTables, hasEmptyOrNullFilters } = projectStore const { loadTables, hasEmptyOrNullFilters } = baseStore
const { project } = storeToRefs(projectStore) const { base } = storeToRefs(baseStore)
const _projectId = inject(ProjectIdInj, undefined) const _projectId = inject(ProjectIdInj, undefined)
const projectId = computed(() => _projectId?.value ?? project.value?.id) const baseId = computed(() => _projectId?.value ?? base.value?.id)
const { t } = useI18n() const { t } = useI18n()
@ -19,15 +19,15 @@ watch(includeM2M, async () => await loadTables())
const showNullAndEmptyInFilter = ref() const showNullAndEmptyInFilter = ref()
onMounted(async () => { onMounted(async () => {
await projectsStore.loadProject(projectId.value!, true) await basesStore.loadProject(baseId.value!, true)
showNullAndEmptyInFilter.value = projectsStore.getProjectMeta(projectId.value!)?.showNullAndEmptyInFilter showNullAndEmptyInFilter.value = basesStore.getProjectMeta(baseId.value!)?.showNullAndEmptyInFilter
}) })
async function showNullAndEmptyInFilterOnChange(evt: CheckboxChangeEvent) { async function showNullAndEmptyInFilterOnChange(evt: CheckboxChangeEvent) {
const project = projectsStore.projects.get(projectId.value!) const base = basesStore.bases.get(baseId.value!)
if (!project) throw new Error(`Project ${projectId.value} not found`) if (!base) throw new Error(`Base ${baseId.value} not found`)
const meta = projectsStore.getProjectMeta(projectId.value!) ?? {} const meta = basesStore.getProjectMeta(baseId.value!) ?? {}
// users cannot hide null & empty option if there is existing null / empty filters // users cannot hide null & empty option if there is existing null / empty filters
if (!evt.target.checked) { if (!evt.target.checked) {
@ -41,9 +41,9 @@ async function showNullAndEmptyInFilterOnChange(evt: CheckboxChangeEvent) {
showNullAndEmptyInFilter: showNullAndEmptyInFilter.value, showNullAndEmptyInFilter: showNullAndEmptyInFilter.value,
} }
// update local state // update local state
project.meta = newProjectMeta base.meta = newProjectMeta
// update db // update db
await projectsStore.updateProject(projectId.value!, { await basesStore.updateProject(baseId.value!, {
meta: JSON.stringify(newProjectMeta), meta: JSON.stringify(newProjectMeta),
}) })
} }

16
packages/nc-gui/components/dashboard/settings/Modal.vue

@ -7,7 +7,7 @@ interface Props {
modelValue?: boolean modelValue?: boolean
openKey?: string openKey?: string
dataSourcesState?: string dataSourcesState?: string
projectId?: string baseId?: string
} }
interface SubTabGroup { interface SubTabGroup {
@ -38,9 +38,9 @@ const vOpenKey = useVModel(props, 'openKey', emits)
const vDataState = useVModel(props, 'dataSourcesState', emits) const vDataState = useVModel(props, 'dataSourcesState', emits)
const projectId = toRef(props, 'projectId') const baseId = toRef(props, 'baseId')
provide(ProjectIdInj, projectId) provide(ProjectIdInj, baseId)
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
@ -108,8 +108,8 @@ const tabsInfo: TabGroup = {
// $e('c:settings:audit') // $e('c:settings:audit')
// }, // },
// }, // },
projectSettings: { baseSettings: {
// Project Settings // Base Settings
title: t('labels.projectSettings'), title: t('labels.projectSettings'),
icon: iconMap.settings, icon: iconMap.settings,
subTabs: { subTabs: {
@ -121,7 +121,7 @@ const tabsInfo: TabGroup = {
}, },
}, },
onClick: () => { onClick: () => {
$e('c:settings:project-settings') $e('c:settings:base-settings')
}, },
}, },
} }
@ -257,14 +257,14 @@ watch(
v-model:reload="dataSourcesReload" v-model:reload="dataSourcesReload"
class="px-2 pb-2" class="px-2 pb-2"
:data-testid="`nc-settings-subtab-${selectedSubTab.key}`" :data-testid="`nc-settings-subtab-${selectedSubTab.key}`"
:project-id="projectId" :base-id="baseId"
@awaken="handleAwaken" @awaken="handleAwaken"
/> />
<component <component
:is="selectedSubTab?.body" :is="selectedSubTab?.body"
v-else v-else
class="px-2 py-6" class="px-2 py-6"
:project-id="projectId" :base-id="baseId"
:data-testid="`nc-settings-subtab-${selectedSubTab.key}`" :data-testid="`nc-settings-subtab-${selectedSubTab.key}`"
/> />
</div> </div>

27
packages/nc-gui/components/dashboard/settings/UIAcl.vue

@ -13,21 +13,21 @@ import {
useGlobal, useGlobal,
useI18n, useI18n,
useNuxtApp, useNuxtApp,
useProject, useBase,
} from '#imports' } from '#imports'
const props = defineProps<{ const props = defineProps<{
baseId: string sourceId: string
}>() }>()
const { t } = useI18n() const { t } = useI18n()
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
const { project } = storeToRefs(useProject()) const { base } = storeToRefs(useBase())
const _projectId = inject(ProjectIdInj, ref()) const _projectId = inject(ProjectIdInj, ref())
const projectId = computed(() => _projectId.value ?? project.value?.id) const baseId = computed(() => _projectId.value ?? base.value?.id)
const { includeM2M } = useGlobal() const { includeM2M } = useGlobal()
@ -42,7 +42,7 @@ const searchInput = ref('')
const filteredTables = computed(() => const filteredTables = computed(() =>
tables.value.filter( tables.value.filter(
(el) => (el) =>
el?.base_id === props.baseId && el?.source_id === props.sourceId &&
((typeof el?._ptn === 'string' && el._ptn.toLowerCase().includes(searchInput.value.toLowerCase())) || ((typeof el?._ptn === 'string' && el._ptn.toLowerCase().includes(searchInput.value.toLowerCase())) ||
(typeof el?.title === 'string' && el.title.toLowerCase().includes(searchInput.value.toLowerCase()))), (typeof el?.title === 'string' && el.title.toLowerCase().includes(searchInput.value.toLowerCase()))),
), ),
@ -50,11 +50,11 @@ const filteredTables = computed(() =>
async function loadTableList() { async function loadTableList() {
try { try {
if (!projectId.value) return if (!baseId.value) return
isLoading.value = true isLoading.value = true
tables.value = await $api.project.modelVisibilityList(projectId.value, { tables.value = await $api.base.modelVisibilityList(baseId.value, {
includeM2M: includeM2M.value, includeM2M: includeM2M.value,
}) })
} catch (e) { } catch (e) {
@ -66,10 +66,10 @@ async function loadTableList() {
async function saveUIAcl() { async function saveUIAcl() {
try { try {
if (!projectId.value) return if (!baseId.value) return
await $api.project.modelVisibilitySet( await $api.base.modelVisibilitySet(
projectId.value, baseId.value,
tables.value.filter((t) => t.edited), tables.value.filter((t) => t.edited),
) )
// Updated UI ACL for tables successfully // Updated UI ACL for tables successfully
@ -123,7 +123,7 @@ const columns = [
<template> <template>
<div class="flex flex-row w-full items-center justify-center"> <div class="flex flex-row w-full items-center justify-center">
<div class="flex flex-col w-[900px]"> <div class="flex flex-col w-[900px]">
<span class="mb-4 first-letter:capital font-bold"> UI ACL : {{ project.title }} </span> <span class="mb-4 first-letter:capital font-bold"> UI ACL : {{ base.title }} </span>
<div class="flex flex-row items-center w-full mb-4 gap-2 justify-between"> <div class="flex flex-row items-center w-full mb-4 gap-2 justify-between">
<a-input v-model:value="searchInput" :placeholder="$t('placeholder.searchModels')" class="nc-acl-search !w-[400px]"> <a-input v-model:value="searchInput" :placeholder="$t('placeholder.searchModels')" class="nc-acl-search !w-[400px]">
<template #prefix> <template #prefix>
@ -171,7 +171,10 @@ const columns = [
<div v-if="column.name === 'table_name'"> <div v-if="column.name === 'table_name'">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<div class="min-w-5 flex items-center justify-center"> <div class="min-w-5 flex items-center justify-center">
<GeneralTableIcon :meta="{ meta: record.table_meta, type: record.ptype }" class="text-gray-500" /> <GeneralTableIcon
:meta="{ meta: record.table_meta, type: record.ptype }"
class="text-gray-500"
></GeneralTableIcon>
</div> </div>
<GeneralTruncateText> <GeneralTruncateText>
<span class="overflow-ellipsis min-w-0 shrink-1">{{ record._ptn }}</span> <span class="overflow-ellipsis min-w-0 shrink-1">{{ record._ptn }}</span>

14
packages/nc-gui/components/dashboard/settings/UIAclTabs.vue

@ -1,22 +1,22 @@
<script lang="ts" setup> <script lang="ts" setup>
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
const { project } = storeToRefs(useProject()) const { base } = storeToRefs(useBase())
</script> </script>
<template> <template>
<div v-if="!project || !project.bases"></div> <div v-if="!base || !base.sources"></div>
<template v-else-if="project.bases.length === 1"> <template v-else-if="base.sources.length === 1">
<DashboardSettingsUIAcl :base-id="project.bases[0].id" class="mt-6" /> <DashboardSettingsUIAcl :source-id="base.sources[0].id" class="mt-6" />
</template> </template>
<a-tabs v-else class="w-full"> <a-tabs v-else class="w-full">
<a-tab-pane v-for="base of project.bases" :key="base.id"> <a-tab-pane v-for="source of base.sources" :key="source.id">
<template #tab> <template #tab>
<div class="tab-title" data-testid="proj-view-tab__all-tables"> <div class="tab-title" data-testid="proj-view-tab__all-tables">
<div class="capitalize">{{ base.alias ?? 'Default' }}</div> <div class="capitalize">{{ source.alias ?? 'Default' }}</div>
</div> </div>
</template> </template>
<DashboardSettingsUIAcl :base-id="base.id" class="mt-6" /> <DashboardSettingsUIAcl :source-id="source.id" class="mt-6" />
</a-tab-pane> </a-tab-pane>
</a-tabs> </a-tabs>
</template> </template>

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

@ -17,7 +17,7 @@ import {
iconMap, iconMap,
nextTick, nextTick,
onMounted, onMounted,
projectTitleValidator, baseTitleValidator,
readFile, readFile,
ref, ref,
storeToRefs, storeToRefs,
@ -29,18 +29,18 @@ import {
const props = defineProps<{ connectionType?: ClientType }>() const props = defineProps<{ connectionType?: ClientType }>()
const emit = defineEmits(['baseCreated', 'close']) const emit = defineEmits(['sourceCreated', 'close'])
const connectionType = computed(() => props.connectionType ?? ClientType.MYSQL) const connectionType = computed(() => props.connectionType ?? ClientType.MYSQL)
const projectStore = useProject() const baseStore = useBase()
const { loadProject } = useProjects() const { loadProject } = useBases()
const { project } = storeToRefs(projectStore) const { base } = storeToRefs(baseStore)
const { loadProjectTables } = useTablesStore() const { loadProjectTables } = useTablesStore()
const _projectId = inject(ProjectIdInj, undefined) const _projectId = inject(ProjectIdInj, undefined)
const projectId = computed(() => _projectId?.value ?? project.value?.id) const baseId = computed(() => _projectId?.value ?? base.value?.id)
const useForm = Form.useForm const useForm = Form.useForm
@ -56,7 +56,7 @@ const { $e } = useNuxtApp()
const { t } = useI18n() const { t } = useI18n()
const creatingBase = ref(false) const creatingSource = ref(false)
const formState = ref<ProjectCreateForm>({ const formState = ref<ProjectCreateForm>({
title: '', title: '',
@ -122,9 +122,9 @@ const validators = computed(() => {
'title': [ 'title': [
{ {
required: true, required: true,
message: 'Base name is required', message: 'Source name is required',
}, },
projectTitleValidator, baseTitleValidator,
], ],
'extraParameters': [extraParameterValidator], 'extraParameters': [extraParameterValidator],
'dataSource.client': [fieldRequiredValidator()], 'dataSource.client': [fieldRequiredValidator()],
@ -234,7 +234,7 @@ const focusInvalidInput = () => {
const { $poller } = useNuxtApp() const { $poller } = useNuxtApp()
const createBase = async () => { const createSource = async () => {
try { try {
await validate() await validate()
} catch (e) { } catch (e) {
@ -243,15 +243,15 @@ const createBase = async () => {
} }
try { try {
if (!projectId.value) return if (!baseId.value) return
creatingBase.value = true creatingSource.value = true
const connection = getConnectionConfig() const connection = getConnectionConfig()
const config = { ...formState.value.dataSource, connection } const config = { ...formState.value.dataSource, connection }
const jobData = await api.base.create(projectId.value, { const jobData = await api.source.create(baseId.value, {
alias: formState.value.title, alias: formState.value.title,
type: formState.value.dataSource.client, type: formState.value.dataSource.client,
config, config,
@ -276,17 +276,17 @@ const createBase = async () => {
if (data.status === JobStatus.COMPLETED) { if (data.status === JobStatus.COMPLETED) {
$e('a:base:create:extdb') $e('a:base:create:extdb')
if (projectId.value) { if (baseId.value) {
await loadProject(projectId.value, true) await loadProject(baseId.value, true)
await loadProjectTables(projectId.value, true) await loadProjectTables(baseId.value, true)
} }
emit('baseCreated') emit('sourceCreated')
emit('close') emit('close')
creatingBase.value = false creatingSource.value = false
} else if (status === JobStatus.FAILED) { } else if (status === JobStatus.FAILED) {
message.error('Failed to create base') message.error('Failed to create base')
creatingBase.value = false creatingSource.value = false
} }
} }
}, },
@ -304,7 +304,7 @@ const testConnection = async () => {
return return
} }
$e('a:base:create:extdb:test-connection', []) $e('a:source:create:extdb:test-connection', [])
try { try {
testingConnection.value = true testingConnection.value = true
@ -403,8 +403,8 @@ watch(
</script> </script>
<template> <template>
<div class="create-base bg-white relative flex flex-col justify-center gap-2 w-full"> <div class="create-source 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-xl font-bold self-start mb-4 flex items-center gap-2">
{{ $t('title.newBase') }} {{ $t('title.newBase') }}
<DashboardSettingsDataSourcesInfo /> <DashboardSettingsDataSourcesInfo />
<span class="flex-grow"></span> <span class="flex-grow"></span>
@ -413,7 +413,7 @@ watch(
<a-form <a-form
ref="form" ref="form"
:model="formState" :model="formState"
name="external-project-create-form" name="external-base-create-form"
layout="horizontal" layout="horizontal"
no-style no-style
:label-col="{ span: 8 }" :label-col="{ span: 8 }"
@ -424,7 +424,7 @@ watch(
maxHeight: '60vh', maxHeight: '60vh',
}" }"
> >
<a-form-item label="Base Name" v-bind="validateInfos.title"> <a-form-item label="Source Name" v-bind="validateInfos.title">
<a-input v-model:value="formState.title" class="nc-extdb-proj-name" /> <a-input v-model:value="formState.title" class="nc-extdb-proj-name" />
</a-form-item> </a-form-item>
@ -628,13 +628,12 @@ watch(
</NcButton> </NcButton>
<NcButton <NcButton
v-e="['a:source:create']"
size="small" size="small"
type="primary" type="primary"
:disabled="!testSuccess" :disabled="!testSuccess"
:loading="creatingBase" :loading="creatingBase"
class="nc-extdb-btn-submit !rounded-md" class="nc-extdb-btn-submit !rounded-md"
@click="createBase" @click="createSource"
> >
{{ $t('general.submit') }} {{ $t('general.submit') }}
</NcButton> </NcButton>
@ -688,7 +687,7 @@ watch(
@apply !min-h-0; @apply !min-h-0;
} }
.create-base { .create-source {
:deep(.ant-input-affix-wrapper), :deep(.ant-input-affix-wrapper),
:deep(.ant-input), :deep(.ant-input),
:deep(.ant-select) { :deep(.ant-select) {

42
packages/nc-gui/components/dashboard/settings/data-sources/EditBase.vue

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { BaseType } from 'nocodb-sdk' import type { SourceType } from 'nocodb-sdk'
import { Form, message } from 'ant-design-vue' import { Form, message } from 'ant-design-vue'
import type { SelectHandler } from 'ant-design-vue/es/vc-select/Select' import type { SelectHandler } from 'ant-design-vue/es/vc-select/Select'
import type { DefaultConnection, ProjectCreateForm, SQLiteConnection } from '#imports' import type { DefaultConnection, ProjectCreateForm, SQLiteConnection } from '#imports'
@ -16,7 +16,7 @@ import {
getTestDatabaseName, getTestDatabaseName,
iconMap, iconMap,
onMounted, onMounted,
projectTitleValidator, baseTitleValidator,
readFile, readFile,
ref, ref,
storeToRefs, storeToRefs,
@ -27,17 +27,17 @@ import {
} from '#imports' } from '#imports'
const props = defineProps<{ const props = defineProps<{
baseId: string sourceId: string
}>() }>()
const emit = defineEmits(['baseUpdated', 'close']) const emit = defineEmits(['baseUpdated', 'close'])
const projectStore = useProject() const baseStore = useBase()
const projectsStore = useProjects() const basesStore = useBases()
const { project } = storeToRefs(projectStore) const { base } = storeToRefs(baseStore)
const _projectId = inject(ProjectIdInj, undefined) const _projectId = inject(ProjectIdInj, undefined)
const projectId = computed(() => _projectId?.value ?? project.value?.id) const baseId = computed(() => _projectId?.value ?? base.value?.id)
const useForm = Form.useForm const useForm = Form.useForm
@ -79,7 +79,7 @@ const customFormState = ref<ProjectCreateForm>({
const validators = computed(() => { const validators = computed(() => {
return { return {
'title': [projectTitleValidator], 'title': [baseTitleValidator],
'extraParameters': [extraParameterValidator], 'extraParameters': [extraParameterValidator],
'dataSource.client': [fieldRequiredValidator()], 'dataSource.client': [fieldRequiredValidator()],
...(formState.value.dataSource.client === ClientType.SQLITE ...(formState.value.dataSource.client === ClientType.SQLITE
@ -214,13 +214,13 @@ const editBase = async () => {
} }
try { try {
if (!project.value?.id) return if (!base.value?.id) return
const connection = getConnectionConfig() const connection = getConnectionConfig()
const config = { ...formState.value.dataSource, connection } const config = { ...formState.value.dataSource, connection }
await api.base.update(project.value?.id, props.baseId, { await api.source.update(base.value?.id, props.sourceId, {
alias: formState.value.title, alias: formState.value.title,
type: formState.value.dataSource.client, type: formState.value.dataSource.client,
config, config,
@ -228,9 +228,9 @@ const editBase = async () => {
inflection_table: formState.value.inflection.inflectionTable, inflection_table: formState.value.inflection.inflectionTable,
}) })
$e('a:base:edit:extdb') $e('a:source:edit:extdb')
await projectsStore.loadProject(projectId.value!, true) await basesStore.loadProject(baseId.value!, true)
emit('baseUpdated') emit('baseUpdated')
emit('close') emit('close')
} catch (e: any) { } catch (e: any) {
@ -246,7 +246,7 @@ const testConnection = async () => {
return return
} }
$e('a:base:edit:extdb:test-connection', []) $e('a:source:edit:extdb:test-connection', [])
try { try {
testingConnection.value = true testingConnection.value = true
@ -315,12 +315,12 @@ watch(
{ deep: true }, { deep: true },
) )
// load base config // load source config
onMounted(async () => { onMounted(async () => {
if (project.value?.id) { if (base.value?.id) {
const definedParameters = ['host', 'port', 'user', 'password', 'database'] const definedParameters = ['host', 'port', 'user', 'password', 'database']
const activeBase = (await api.base.read(project.value?.id, props.baseId)) as BaseType const activeBase = (await api.source.read(base.value?.id, props.sourceId)) as SourceType
const tempParameters = Object.entries(activeBase.config.connection) const tempParameters = Object.entries(activeBase.config.connection)
.filter(([key]) => !definedParameters.includes(key)) .filter(([key]) => !definedParameters.includes(key))
@ -342,13 +342,13 @@ onMounted(async () => {
</script> </script>
<template> <template>
<div class="edit-base bg-white relative flex flex-col justify-start gap-2 w-full p-2"> <div class="edit-source bg-white relative flex flex-col justify-start gap-2 w-full p-2">
<h1 class="prose-2xl font-bold self-start">{{ $t('activity.editBase') }}</h1> <h1 class="prose-2xl font-bold self-start">{{ $t('activity.editSource') }}</h1>
<a-form <a-form
ref="form" ref="form"
:model="formState" :model="formState"
name="external-project-create-form" name="external-base-create-form"
layout="horizontal" layout="horizontal"
no-style no-style
:label-col="{ span: 8 }" :label-col="{ span: 8 }"
@ -359,7 +359,7 @@ onMounted(async () => {
maxHeight: '60vh', maxHeight: '60vh',
}" }"
> >
<a-form-item label="Base Name" v-bind="validateInfos.title"> <a-form-item label="Source Name" v-bind="validateInfos.title">
<a-input v-model:value="formState.title" class="nc-extdb-proj-name" /> <a-input v-model:value="formState.title" class="nc-extdb-proj-name" />
</a-form-item> </a-form-item>
@ -625,7 +625,7 @@ onMounted(async () => {
@apply !min-h-0; @apply !min-h-0;
} }
.edit-base { .edit-source {
:deep(.ant-input-affix-wrapper), :deep(.ant-input-affix-wrapper),
:deep(.ant-input), :deep(.ant-input),
:deep(.ant-select) { :deep(.ant-select) {

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

@ -13,13 +13,13 @@ import {
ref, ref,
storeToRefs, storeToRefs,
useNuxtApp, useNuxtApp,
useProject, useBase,
watch, watch,
} from '#imports' } from '#imports'
const { modelValue, baseId } = defineProps<{ const { modelValue, sourceId } = defineProps<{
modelValue: boolean modelValue: boolean
baseId: string sourceId: string
}>() }>()
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
@ -30,13 +30,13 @@ const baseURL = $api.instance.defaults.baseURL
const { $state, $poller } = useNuxtApp() const { $state, $poller } = useNuxtApp()
const projectStore = useProject() const baseStore = useBase()
const { refreshCommandPalette } = useCommandPalette() const { refreshCommandPalette } = useCommandPalette()
const { loadTables } = projectStore const { loadTables } = baseStore
const { project } = storeToRefs(projectStore) const { base } = storeToRefs(baseStore)
const showGoToDashboardButton = ref(false) const showGoToDashboardButton = ref(false)
@ -128,14 +128,14 @@ async function createOrUpdate() {
const { id, ...payload } = syncSource.value const { id, ...payload } = syncSource.value
if (id !== '') { if (id !== '') {
await $fetch(`/api/v1/db/meta/syncs/${id}`, { await $fetch(`/api/v1/meta/syncs/${id}`, {
baseURL, baseURL,
method: 'PATCH', method: 'PATCH',
headers: { 'xc-auth': $state.token.value as string }, headers: { 'xc-auth': $state.token.value as string },
body: payload, body: payload,
}) })
} else { } else {
syncSource.value = await $fetch(`/api/v1/db/meta/projects/${project.value.id}/syncs/${baseId}`, { syncSource.value = await $fetch(`/api/v1/meta/bases/${base.value.id}/syncs/${sourceId}`, {
baseURL, baseURL,
method: 'POST', method: 'POST',
headers: { 'xc-auth': $state.token.value as string }, headers: { 'xc-auth': $state.token.value as string },
@ -187,7 +187,7 @@ async function listenForUpdates() {
} }
async function loadSyncSrc() { async function loadSyncSrc() {
const data: any = await $fetch(`/api/v1/db/meta/projects/${project.value.id}/syncs/${baseId}`, { const data: any = await $fetch(`/api/v1/meta/bases/${base.value.id}/syncs/${sourceId}`, {
baseURL, baseURL,
method: 'GET', method: 'GET',
headers: { 'xc-auth': $state.token.value as string }, headers: { 'xc-auth': $state.token.value as string },
@ -229,7 +229,7 @@ async function loadSyncSrc() {
async function sync() { async function sync() {
try { try {
await $fetch(`/api/v1/db/meta/syncs/${syncSource.value.id}/trigger`, { await $fetch(`/api/v1/meta/syncs/${syncSource.value.id}/trigger`, {
baseURL, baseURL,
method: 'POST', method: 'POST',
headers: { 'xc-auth': $state.token.value as string }, headers: { 'xc-auth': $state.token.value as string },
@ -248,7 +248,7 @@ async function abort() {
"This is a highly experimental feature and only marks job as not started, please don't abort the job unless you are sure job is stuck.", "This is a highly experimental feature and only marks job as not started, please don't abort the job unless you are sure job is stuck.",
onOk: async () => { onOk: async () => {
try { try {
await $fetch(`/api/v1/db/meta/syncs/${syncSource.value.id}/abort`, { await $fetch(`/api/v1/meta/syncs/${syncSource.value.id}/abort`, {
baseURL, baseURL,
method: 'POST', method: 'POST',
headers: { 'xc-auth': $state.token.value as string }, headers: { 'xc-auth': $state.token.value as string },

24
packages/nc-gui/components/dlg/ProjectDelete.vue

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
const props = defineProps<{ const props = defineProps<{
visible: boolean visible: boolean
projectId: string baseId: string
}>() }>()
const emits = defineEmits(['update:visible']) const emits = defineEmits(['update:visible'])
@ -10,22 +10,22 @@ const visible = useVModel(props, 'visible', emits)
const { closeTab } = useTabs() const { closeTab } = useTabs()
const projectsStore = useProjects() const basesStore = useBases()
const { deleteProject, navigateToFirstProjectOrHome } = projectsStore const { deleteProject, navigateToFirstProjectOrHome } = basesStore
const { projects } = storeToRefs(projectsStore) const { bases } = storeToRefs(basesStore)
const { removeFromRecentViews } = useViewsStore() const { removeFromRecentViews } = useViewsStore()
const { refreshCommandPalette } = useCommandPalette() const { refreshCommandPalette } = useCommandPalette()
const project = computed(() => projects.value.get(props.projectId)) const base = computed(() => bases.value.get(props.baseId))
const isLoading = ref(false) const isLoading = ref(false)
const onDelete = async () => { const onDelete = async () => {
if (!project.value) return if (!base.value) return
const toBeDeletedProject = JSON.parse(JSON.stringify(project.value)) const toBeDeletedProject = JSON.parse(JSON.stringify(base.value))
isLoading.value = true isLoading.value = true
try { try {
@ -36,14 +36,14 @@ const onDelete = async () => {
visible.value = false visible.value = false
if (toBeDeletedProject.id === projectsStore.activeProjectId) { if (toBeDeletedProject.id === basesStore.activeProjectId) {
await navigateToFirstProjectOrHome() await navigateToFirstProjectOrHome()
} }
} catch (e: any) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} finally { } finally {
isLoading.value = false isLoading.value = false
removeFromRecentViews({ projectId: toBeDeletedProject.id! }) removeFromRecentViews({ baseId: toBeDeletedProject.id! })
} }
} }
</script> </script>
@ -51,13 +51,13 @@ const onDelete = async () => {
<template> <template>
<GeneralDeleteModal v-model:visible="visible" :entity-name="$t('objects.project')" :on-delete="onDelete"> <GeneralDeleteModal v-model:visible="visible" :entity-name="$t('objects.project')" :on-delete="onDelete">
<template #entity-preview> <template #entity-preview>
<div v-if="project" class="flex flex-row items-center py-2 px-2.25 bg-gray-50 rounded-lg text-gray-700 mb-4"> <div v-if="base" class="flex flex-row items-center py-2 px-2.25 bg-gray-50 rounded-lg text-gray-700 mb-4">
<GeneralProjectIcon :type="project.type" class="nc-view-icon px-1.5"></GeneralProjectIcon> <GeneralProjectIcon :type="base.type" class="nc-view-icon px-1.5"></GeneralProjectIcon>
<div <div
class="capitalize text-ellipsis overflow-hidden select-none w-full pl-1.75" class="capitalize text-ellipsis overflow-hidden select-none w-full pl-1.75"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }" :style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
> >
{{ project.title }} {{ base.title }}
</div> </div>
</div> </div>
</template> </template>

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

@ -1,11 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import tinycolor from 'tinycolor2' import tinycolor from 'tinycolor2'
import type { ProjectType } from 'nocodb-sdk' import type { BaseType } from 'nocodb-sdk'
import { useVModel } from '#imports' import { useVModel } from '#imports'
const props = defineProps<{ const props = defineProps<{
modelValue: boolean modelValue: boolean
project: ProjectType base: BaseType
onOk: (jobData: { name: string; id: string }) => Promise<void> onOk: (jobData: { name: string; id: string }) => Promise<void>
}>() }>()
@ -35,17 +35,17 @@ const isLoading = ref(false)
const _duplicate = async () => { const _duplicate = async () => {
try { try {
isLoading.value = true isLoading.value = true
// pick a random color from array and assign to project // pick a random color from array and assign to base
const color = projectThemeColors[Math.floor(Math.random() * 1000) % projectThemeColors.length] const color = baseThemeColors[Math.floor(Math.random() * 1000) % baseThemeColors.length]
const tcolor = tinycolor(color) const tcolor = tinycolor(color)
const complement = tcolor.complement() const complement = tcolor.complement()
const jobData = await api.project.duplicate(props.project.id as string, { const jobData = await api.base.duplicate(props.base.id as string, {
options: optionsToExclude.value, options: optionsToExclude.value,
project: { base: {
fk_workspace_id: props.project.fk_workspace_id, fk_workspace_id: props.base.fk_workspace_id,
type: props.project.type, type: props.base.type,
color, color,
meta: JSON.stringify({ meta: JSON.stringify({
theme: { theme: {
@ -75,7 +75,7 @@ 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="base" v-model:visible="dialogShow" class="!w-[30rem]" wrap-class-name="nc-modal-base-duplicate">
<div> <div>
<div class="prose-xl font-bold self-center" @dblclick="isEaster = !isEaster"> <div class="prose-xl font-bold self-center" @dblclick="isEaster = !isEaster">
{{ $t('general.duplicate') }} {{ $t('objects.project') }} {{ $t('general.duplicate') }} {{ $t('objects.project') }}

12
packages/nc-gui/components/dlg/QuickImport.vue

@ -30,7 +30,7 @@ import {
storeToRefs, storeToRefs,
useGlobal, useGlobal,
useI18n, useI18n,
useProject, useBase,
useVModel, useVModel,
} from '#imports' } from '#imports'
@ -41,11 +41,11 @@ import { useNuxtApp } from '#app'
interface Props { interface Props {
modelValue: boolean modelValue: boolean
importType: 'csv' | 'json' | 'excel' importType: 'csv' | 'json' | 'excel'
baseId: string sourceId: string
importDataOnly?: boolean importDataOnly?: boolean
} }
const { importType, importDataOnly = false, baseId, ...rest } = defineProps<Props>() const { importType, importDataOnly = false, sourceId, ...rest } = defineProps<Props>()
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
@ -63,7 +63,7 @@ const { t } = useI18n()
const progressMsg = ref('Parsing Data ...') const progressMsg = ref('Parsing Data ...')
const { tables } = storeToRefs(useProject()) const { tables } = storeToRefs(useBase())
const activeKey = ref('uploadTab') const activeKey = ref('uploadTab')
@ -546,13 +546,13 @@ async function parseAndExtractData(val: UploadFile[] | ArrayBuffer | string) {
<LazyTemplateEditor <LazyTemplateEditor
v-if="templateEditorModal" v-if="templateEditorModal"
ref="templateEditorRef" ref="templateEditorRef"
:project-template="templateData" :base-template="templateData"
:import-data="importData" :import-data="importData"
:import-columns="importColumns" :import-columns="importColumns"
:import-data-only="importDataOnly" :import-data-only="importDataOnly"
:quick-import-type="importType" :quick-import-type="importType"
:max-rows-to-parse="importState.parserConfig.maxRowsToParse" :max-rows-to-parse="importState.parserConfig.maxRowsToParse"
:base-id="baseId" :source-id="sourceId"
:import-worker="importWorker" :import-worker="importWorker"
class="nc-quick-import-template-editor" class="nc-quick-import-template-editor"
@import="handleImport" @import="handleImport"

32
packages/nc-gui/components/dlg/TableCreate.vue

@ -6,7 +6,7 @@ import {
nextTick, nextTick,
onMounted, onMounted,
ref, ref,
useProject, useBase,
useTableNew, useTableNew,
useTablesStore, useTablesStore,
useTabs, useTabs,
@ -16,8 +16,8 @@ import {
const props = defineProps<{ const props = defineProps<{
modelValue: boolean modelValue: boolean
sourceId: string
baseId: string baseId: string
projectId: string
}>() }>()
const emit = defineEmits(['update:modelValue', 'create']) const emit = defineEmits(['update:modelValue', 'create'])
@ -30,30 +30,30 @@ const inputEl = ref<HTMLInputElement>()
const { addTab } = useTabs() const { addTab } = useTabs()
const { isMysql, isMssql, isPg } = useProject() const { isMysql, isMssql, isPg } = useBase()
const { loadProjectTables, addTable } = useTablesStore() const { loadProjectTables, addTable } = useTablesStore()
const { table, createTable, generateUniqueTitle, tables, project } = useTableNew({ const { table, createTable, generateUniqueTitle, tables, base } = useTableNew({
async onTableCreate(table) { async onTableCreate(table) {
// await loadProject(props.projectId) // await loadProject(props.baseId)
await addTab({ await addTab({
id: table.id as string, id: table.id as string,
title: table.title, title: table.title,
type: TabType.TABLE, type: TabType.TABLE,
projectId: props.projectId, baseId: props.baseId,
// baseId: props.baseId, // sourceId: props.sourceId,
}) })
addTable(props.projectId, table) addTable(props.baseId, table)
await loadProjectTables(props.projectId, true) await loadProjectTables(props.baseId, true)
emit('create', table) emit('create', table)
dialogShow.value = false dialogShow.value = false
}, },
sourceId: props.sourceId,
baseId: props.baseId, baseId: props.baseId,
projectId: props.projectId,
}) })
const useForm = Form.useForm const useForm = Form.useForm
@ -66,7 +66,7 @@ const validators = computed(() => {
validator: (_: any, value: any) => { validator: (_: any, value: any) => {
// validate duplicate alias // validate duplicate alias
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if ((tables.value || []).some((t) => t.title === (value || '') && t.base_id === props.baseId)) { if ((tables.value || []).some((t) => t.title === (value || '') && t.source_id === props.sourceId)) {
return reject(new Error('Duplicate table alias')) return reject(new Error('Duplicate table alias'))
} }
return resolve(true) return resolve(true)
@ -77,15 +77,15 @@ const validators = computed(() => {
validator: (rule: any, value: any) => { validator: (rule: any, value: any) => {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
let tableNameLengthLimit = 255 let tableNameLengthLimit = 255
if (isMysql(props.baseId)) { if (isMysql(props.sourceId)) {
tableNameLengthLimit = 64 tableNameLengthLimit = 64
} else if (isPg(props.baseId)) { } else if (isPg(props.sourceId)) {
tableNameLengthLimit = 63 tableNameLengthLimit = 63
} else if (isMssql(props.baseId)) { } else if (isMssql(props.sourceId)) {
tableNameLengthLimit = 128 tableNameLengthLimit = 128
} }
const projectPrefix = project?.value?.prefix || '' const basePrefix = base?.value?.prefix || ''
if ((projectPrefix + value).length > tableNameLengthLimit) { if ((basePrefix + value).length > tableNameLengthLimit) {
return reject(new Error(`Table name exceeds ${tableNameLengthLimit} characters`)) return reject(new Error(`Table name exceeds ${tableNameLengthLimit} characters`))
} }
resolve() resolve()

20
packages/nc-gui/components/dlg/TableDelete.vue

@ -5,7 +5,7 @@ import { UITypes, isSystemColumn } from 'nocodb-sdk'
const props = defineProps<{ const props = defineProps<{
visible: boolean visible: boolean
tableId: string tableId: string
projectId: string baseId: string
}>() }>()
const emits = defineEmits(['update:visible']) const emits = defineEmits(['update:visible'])
@ -17,14 +17,14 @@ const { closeTab } = useTabs()
const { getMeta, removeMeta } = useMetas() const { getMeta, removeMeta } = useMetas()
const { loadTables, projectUrl, isXcdbBase } = useProject() const { loadTables, baseUrl, isXcdbBase } = useBase()
const { refreshCommandPalette } = useCommandPalette() const { refreshCommandPalette } = useCommandPalette()
const { removeFromRecentViews } = useViewsStore() const { removeFromRecentViews } = useViewsStore()
const { projectTables, activeTable } = storeToRefs(useTablesStore()) const { baseTables, activeTable } = storeToRefs(useTablesStore())
const { openTable } = useTablesStore() const { openTable } = useTablesStore()
const tables = computed(() => projectTables.value.get(props.projectId) ?? []) const tables = computed(() => baseTables.value.get(props.baseId) ?? [])
const table = computed(() => tables.value.find((t) => t.id === props.tableId)) const table = computed(() => tables.value.find((t) => t.id === props.tableId))
@ -43,7 +43,7 @@ const onDelete = async () => {
const meta = (await getMeta(toBeDeletedTable.id as string, true)) as TableType const meta = (await getMeta(toBeDeletedTable.id as string, true)) as TableType
const relationColumns = meta?.columns?.filter((c) => c.uidt === UITypes.LinkToAnotherRecord && !isSystemColumn(c)) const relationColumns = meta?.columns?.filter((c) => c.uidt === UITypes.LinkToAnotherRecord && !isSystemColumn(c))
if (relationColumns?.length && !isXcdbBase(toBeDeletedTable.base_id)) { if (relationColumns?.length && !isXcdbBase(toBeDeletedTable.source_id)) {
const refColMsgs = await Promise.all( const refColMsgs = await Promise.all(
relationColumns.map(async (c, i) => { relationColumns.map(async (c, i) => {
const refMeta = (await getMeta((c?.colOptions as LinkToAnotherRecordType)?.fk_related_model_id as string)) as TableType const refMeta = (await getMeta((c?.colOptions as LinkToAnotherRecordType)?.fk_related_model_id as string)) as TableType
@ -71,7 +71,7 @@ const onDelete = async () => {
await loadTables() await loadTables()
// Remove from recent views // Remove from recent views
removeFromRecentViews({ projectId: props.projectId, tableId: toBeDeletedTable.id as string }) removeFromRecentViews({ baseId: props.baseId, tableId: toBeDeletedTable.id as string })
removeMeta(toBeDeletedTable.id as string) removeMeta(toBeDeletedTable.id as string)
refreshCommandPalette() refreshCommandPalette()
@ -79,11 +79,11 @@ const onDelete = async () => {
$e('a:table:delete') $e('a:table:delete')
if (oldActiveTableId === toBeDeletedTable.id) { if (oldActiveTableId === toBeDeletedTable.id) {
// Navigate to project if no tables left or open first table // Navigate to base if no tables left or open first table
if (tables.value.length === 0) { if (tables.value.length === 0) {
await navigateTo( await navigateTo(
projectUrl({ baseUrl({
id: props.projectId, id: props.baseId,
type: 'database', type: 'database',
}), }),
) )
@ -110,7 +110,7 @@ const onDelete = async () => {
<GeneralDeleteModal v-model:visible="visible" :entity-name="$t('objects.table')" :on-delete="onDelete"> <GeneralDeleteModal v-model:visible="visible" :entity-name="$t('objects.table')" :on-delete="onDelete">
<template #entity-preview> <template #entity-preview>
<div v-if="table" class="flex flex-row items-center py-2.25 px-2.5 bg-gray-50 rounded-lg text-gray-700 mb-4"> <div v-if="table" class="flex flex-row items-center py-2.25 px-2.5 bg-gray-50 rounded-lg text-gray-700 mb-4">
<GeneralTableIcon :meta="table" class="nc-view-icon" /> <GeneralTableIcon :meta="table" class="nc-view-icon"></GeneralTableIcon>
<div <div
class="capitalize text-ellipsis overflow-hidden select-none w-full pl-1.75" class="capitalize text-ellipsis overflow-hidden select-none w-full pl-1.75"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }" :style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"

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

@ -34,7 +34,7 @@ const isLoading = ref(false)
const _duplicate = async () => { const _duplicate = async () => {
try { try {
isLoading.value = true 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.base_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))

28
packages/nc-gui/components/dlg/TableRename.vue

@ -13,7 +13,7 @@ import {
useCommandPalette, useCommandPalette,
useMetas, useMetas,
useNuxtApp, useNuxtApp,
useProject, useBase,
useTablesStore, useTablesStore,
useTabs, useTabs,
useUndoRedo, useUndoRedo,
@ -25,10 +25,10 @@ import {
interface Props { interface Props {
modelValue?: boolean modelValue?: boolean
tableMeta: TableType tableMeta: TableType
baseId: string sourceId: string
} }
const { tableMeta, baseId, ...props } = defineProps<Props>() const { tableMeta, sourceId, ...props } = defineProps<Props>()
const emit = defineEmits(['update:modelValue', 'updated']) const emit = defineEmits(['update:modelValue', 'updated'])
@ -42,9 +42,9 @@ const { updateTab } = useTabs()
const { loadProjectTables } = useTablesStore() const { loadProjectTables } = useTablesStore()
const projectStore = useProject() const baseStore = useBase()
const { loadTables, isMysql, isMssql, isPg } = projectStore const { loadTables, isMysql, isMssql, isPg } = baseStore
const { tables, project } = storeToRefs(projectStore) const { tables, base } = storeToRefs(baseStore)
const { refreshCommandPalette } = useCommandPalette() const { refreshCommandPalette } = useCommandPalette()
@ -68,15 +68,15 @@ const validators = computed(() => {
validator: (rule: any, value: any) => { validator: (rule: any, value: any) => {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
let tableNameLengthLimit = 255 let tableNameLengthLimit = 255
if (isMysql(baseId)) { if (isMysql(sourceId)) {
tableNameLengthLimit = 64 tableNameLengthLimit = 64
} else if (isPg(baseId)) { } else if (isPg(sourceId)) {
tableNameLengthLimit = 63 tableNameLengthLimit = 63
} else if (isMssql(baseId)) { } else if (isMssql(sourceId)) {
tableNameLengthLimit = 128 tableNameLengthLimit = 128
} }
const projectPrefix = project?.value?.prefix || '' const basePrefix = base?.value?.prefix || ''
if ((projectPrefix + value).length > tableNameLengthLimit) { if ((basePrefix + value).length > tableNameLengthLimit) {
return reject(new Error(`Table name exceeds ${tableNameLengthLimit} characters`)) return reject(new Error(`Table name exceeds ${tableNameLengthLimit} characters`))
} }
resolve() resolve()
@ -127,14 +127,14 @@ const renameTable = async (undo = false, disableTitleDiffCheck?: boolean | undef
loading.value = true loading.value = true
try { try {
await $api.dbTable.update(tableMeta.id as string, { await $api.dbTable.update(tableMeta.id as string, {
project_id: tableMeta.project_id, base_id: tableMeta.base_id,
table_name: formState.title, table_name: formState.title,
title: formState.title, title: formState.title,
}) })
dialogShow.value = false dialogShow.value = false
await loadProjectTables(tableMeta.project_id!, true) await loadProjectTables(tableMeta.base_id!, true)
if (!undo) { if (!undo) {
addUndo({ addUndo({
@ -168,7 +168,7 @@ const renameTable = async (undo = false, disableTitleDiffCheck?: boolean | undef
$e('a:table:rename') $e('a:table:rename')
useTitle(`${project.value?.title}: ${newMeta?.title}`) useTitle(`${base.value?.title}: ${newMeta?.title}`)
dialogShow.value = false dialogShow.value = false
} catch (e: any) { } catch (e: any) {

6
packages/nc-gui/components/dlg/share-and-collaborate/Collaborate.vue

@ -66,7 +66,7 @@ watch(
class="!rounded-md !ml-0.5" class="!rounded-md !ml-0.5"
validate-trigger="onBlur" validate-trigger="onBlur"
placeholder="Add people by email..." placeholder="Add people by email..."
data-testid="docs-share-dlg-share-project-collaborate-emails" data-testid="docs-share-dlg-share-base-collaborate-emails"
/> />
</a-form-item> </a-form-item>
</div> </div>
@ -77,9 +77,9 @@ watch(
v-model:value="invitationUsersData.role" v-model:value="invitationUsersData.role"
class="!rounded-md !bg-white" class="!rounded-md !bg-white"
dropdown-class-name="nc-dropdown-user-role !rounded-md" dropdown-class-name="nc-dropdown-user-role !rounded-md"
data-testid="docs-share-dlg-share-project-collaborate-role" data-testid="docs-share-dlg-share-base-collaborate-role"
> >
<a-select-option v-for="(role, index) in projectRoles" :key="index" :value="role" class="nc-role-option"> <a-select-option v-for="(role, index) in baseRoles" :key="index" :value="role" class="nc-role-option">
<div <div
class="flex flex-row h-full justify-start items-center" class="flex flex-row h-full justify-start items-center"
:data-testid="`nc-share-invite-user-role-option-${role}`" :data-testid="`nc-share-invite-user-role-option-${role}`"

4
packages/nc-gui/components/dlg/share-and-collaborate/ManageUsers.vue

@ -73,7 +73,7 @@ const rolesTypes = [
<template> <template>
<div class="flex flex-col mx-4 h-112"> <div class="flex flex-col mx-4 h-112">
<div class="flex mt-2.5 mb-2.5 border-b-1 border-gray-50 pb-1.5" :style="{ fontWeight: 500 }">Manage Members</div> <div class="flex mt-2.5 mb-2.5 border-b-1 border-gray-50 pb-1.5" :style="{ fontWeight: 500 }">Manage Members</div>
<div class="flex mt-2.5 mb-2.5 text-xs font-bold">Project Owner</div> <div class="flex mt-2.5 mb-2.5 text-xs font-bold">Base Owner</div>
<div v-if="owner" class="flex flex-row px-2 py-2 items-center gap-x-2 border-1 border-gray-100 rounded-md"> <div v-if="owner" class="flex flex-row px-2 py-2 items-center gap-x-2 border-1 border-gray-100 rounded-md">
<a-avatar></a-avatar> <a-avatar></a-avatar>
<div class="flex flex-col justify-center"> <div class="flex flex-col justify-center">
@ -91,7 +91,7 @@ const rolesTypes = [
</div> </div>
</div> </div>
<div class="flex flex-col mb-2 pr-0.5 h-96 overflow-y-auto users-list border-b-1 border-gray-100"> <div class="flex flex-col mb-2 pr-0.5 h-96 overflow-y-auto users-list border-b-1 border-gray-100">
<div v-if="nonOwners.length === 0" class="text-xs mt-2">No users have access to this project</div> <div v-if="nonOwners.length === 0" class="text-xs mt-2">No users have access to this base</div>
<div <div
v-for="user of nonOwners" v-for="user of nonOwners"
:key="user.id" :key="user.id"

38
packages/nc-gui/components/dlg/share-and-collaborate/ShareBase.vue

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { extractSdkResponseErrorMsg, message, onMounted, storeToRefs, useDashboard, useNuxtApp, useProject } from '#imports' import { extractSdkResponseErrorMsg, message, onMounted, storeToRefs, useDashboard, useNuxtApp, useBase } from '#imports'
interface ShareBase { interface ShareBase {
uuid?: string uuid?: string
@ -16,19 +16,19 @@ const { dashboardUrl } = useDashboard()
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
const base = ref<null | ShareBase>(null) const sharedBase = ref<null | ShareBase>(null)
const { project } = storeToRefs(useProject()) const { base } = storeToRefs(useBase())
const url = computed(() => (base.value && base.value.uuid ? `${dashboardUrl.value}#/base/${base.value.uuid}` : '')) const url = computed(() => (sharedBase.value && sharedBase.value.uuid ? `${dashboardUrl.value}#/base/${sharedBase.value.uuid}` : ''))
const loadBase = async () => { const loadBase = async () => {
try { try {
if (!project.value.id) return if (!base.value.id) return
const res = await $api.project.sharedBaseGet(project.value.id) const res = await $api.base.sharedBaseGet(base.value.id)
base.value = { sharedBase.value = {
uuid: res.uuid, uuid: res.uuid,
url: res.url, url: res.url,
role: res.roles, role: res.roles,
@ -40,14 +40,14 @@ const loadBase = async () => {
const createShareBase = async (role = ShareBaseRole.Viewer) => { const createShareBase = async (role = ShareBaseRole.Viewer) => {
try { try {
if (!project.value.id) return if (!base.value.id) return
const res = await $api.project.sharedBaseUpdate(project.value.id, { const res = await $api.base.sharedBaseUpdate(base.value.id, {
roles: role, roles: role,
}) })
base.value = res ?? {} sharedBase.value = res ?? {}
base.value!.role = role sharedBase.value!.role = role
} catch (e: any) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} }
@ -57,10 +57,10 @@ const createShareBase = async (role = ShareBaseRole.Viewer) => {
const disableSharedBase = async () => { const disableSharedBase = async () => {
try { try {
if (!project.value.id) return if (!base.value.id) return
await $api.project.sharedBaseDisable(project.value.id) await $api.base.sharedBaseDisable(base.value.id)
base.value = null sharedBase.value = null
} catch (e: any) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} }
@ -69,12 +69,12 @@ const disableSharedBase = async () => {
} }
onMounted(() => { onMounted(() => {
if (!base.value) { if (!sharedBase.value) {
loadBase() loadBase()
} }
}) })
const isSharedBaseEnabled = computed(() => !!base.value?.uuid) const isSharedBaseEnabled = computed(() => !!sharedBase.value?.uuid)
const isToggleBaseLoading = ref(false) const isToggleBaseLoading = ref(false)
const isRoleToggleLoading = ref(false) const isRoleToggleLoading = ref(false)
@ -96,12 +96,12 @@ const toggleSharedBase = async () => {
} }
const onRoleToggle = async () => { const onRoleToggle = async () => {
if (!base.value) return if (!sharedBase.value) return
if (isRoleToggleLoading.value) return if (isRoleToggleLoading.value) return
isRoleToggleLoading.value = true isRoleToggleLoading.value = true
try { try {
if (base.value.role === ShareBaseRole.Viewer) { if (sharedBase.value.role === ShareBaseRole.Viewer) {
await createShareBase(ShareBaseRole.Editor) await createShareBase(ShareBaseRole.Editor)
} else { } else {
await createShareBase(ShareBaseRole.Viewer) await createShareBase(ShareBaseRole.Viewer)
@ -134,7 +134,7 @@ const onRoleToggle = async () => {
<a-switch <a-switch
v-e="['c:share:base:role:toggle']" v-e="['c:share:base:role:toggle']"
:loading="isRoleToggleLoading" :loading="isRoleToggleLoading"
:checked="base?.role === ShareBaseRole.Editor" :checked="sharedBase?.role === ShareBaseRole.Editor"
class="ml-2" class="ml-2"
@click="onRoleToggle" @click="onRoleToggle"
/> />

2
packages/nc-gui/components/dlg/share-and-collaborate/SharePage.vue

@ -371,7 +371,7 @@ const isPublicShareDisabled = computed(() => {
data-testid="nc-modal-share-view__theme-picker" data-testid="nc-modal-share-view__theme-picker"
class="!p-0 !bg-inherit" class="!p-0 !bg-inherit"
:model-value="activeView?.meta?.theme?.primaryColor" :model-value="activeView?.meta?.theme?.primaryColor"
:colors="projectThemeColors" :colors="baseThemeColors"
:row-size="9" :row-size="9"
:advanced="false" :advanced="false"
@input="onChangeTheme" @input="onChangeTheme"

6
packages/nc-gui/components/dlg/share-and-collaborate/ShareProject.vue

@ -5,14 +5,14 @@ import ShareBase from './ShareBase.vue'
const { formStatus } = storeToRefs(useShare()) const { formStatus } = storeToRefs(useShare())
onMounted(async () => { onMounted(async () => {
formStatus.value = 'project-collaborate' formStatus.value = 'base-collaborate'
}) })
</script> </script>
<template> <template>
<div class="flex flex-col mx-4 mb-4 mt-2"> <div class="flex flex-col mx-4 mb-4 mt-2">
<a-tabs v-model:activeKey="formStatus"> <a-tabs v-model:activeKey="formStatus">
<a-tab-pane key="project-collaborate"> <a-tab-pane key="base-collaborate">
<template #tab> <template #tab>
<div class="flex flex-row items-center text-xs px-2"> <div class="flex flex-row items-center text-xs px-2">
<MdiAccountPlusOutline class="mr-1" /> <MdiAccountPlusOutline class="mr-1" />
@ -25,7 +25,7 @@ onMounted(async () => {
<template #tab> <template #tab>
<div class="flex flex-row items-center text-xs px-2"> <div class="flex flex-row items-center text-xs px-2">
<MdiEarth class="mr-1" /> <MdiEarth class="mr-1" />
<div data-testid="db-share-base">Share Base</div> <div data-testid="db-share-base">Share Source</div>
</div> </div>
</template> </template>
<ShareBase /> <ShareBase />

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

@ -10,9 +10,9 @@ const { isViewToolbar } = defineProps<{
const { copy } = useCopy() const { copy } = useCopy()
const { dashboardUrl } = useDashboard() const { dashboardUrl } = useDashboard()
const projectStore = useProject() const baseStore = useBase()
const { project } = storeToRefs(projectStore) const { base } = storeToRefs(baseStore)
const { navigateToProjectPage } = projectStore const { navigateToProjectPage } = baseStore
const { activeView } = storeToRefs(useViewsStore()) const { activeView } = storeToRefs(useViewsStore())
let view: Ref<ViewType | undefined> let view: Ref<ViewType | undefined>
@ -29,7 +29,7 @@ const { formStatus, showShareModal, invitationUsersData, isInvitationLinkCopied
const { resetData } = useShare() const { resetData } = useShare()
// const { inviteUser } = useManageUsers() // const { inviteUser } = useManageUsers()
// const expandedSharedType = ref<'none' | 'project' | 'view'>('view') // const expandedSharedType = ref<'none' | 'base' | 'view'>('view')
const isOpeningManageAccess = ref(false) const isOpeningManageAccess = ref(false)
const inviteUrl = computed(() => const inviteUrl = computed(() =>
@ -89,17 +89,17 @@ watch(showShareModal, (val) => {
:class="{ active: showShareModal }" :class="{ active: showShareModal }"
wrap-class-name="nc-modal-share-collaborate" wrap-class-name="nc-modal-share-collaborate"
:closable="false" :closable="false"
:mask-closable="formStatus === 'project-collaborateSaving' ? false : true" :mask-closable="formStatus === 'base-collaborateSaving' ? false : true"
:ok-button-props="{ hidden: true } as any" :ok-button-props="{ hidden: true } as any"
:cancel-button-props="{ hidden: true } as any" :cancel-button-props="{ hidden: true } as any"
:footer="null" :footer="null"
:width="formStatus === 'manageCollaborators' ? '60rem' : '40rem'" :width="formStatus === 'manageCollaborators' ? '60rem' : '40rem'"
> >
<div v-if="formStatus === 'project-collaborateSaving'" class="flex flex-row w-full px-5 justify-between items-center py-1"> <div v-if="formStatus === 'base-collaborateSaving'" class="flex flex-row w-full px-5 justify-between items-center py-1">
<div class="flex text-base font-bold">Adding Members</div> <div class="flex text-base font-bold">Adding Members</div>
<a-spin :indicator="indicator" /> <a-spin :indicator="indicator" />
</div> </div>
<template v-else-if="formStatus === 'project-collaborateSaved'"> <template v-else-if="formStatus === 'base-collaborateSaved'">
<div class="flex flex-col py-1.5"> <div class="flex flex-col py-1.5">
<div class="flex flex-row w-full px-5 justify-between items-center py-0.5"> <div class="flex flex-row w-full px-5 justify-between items-center py-0.5">
<div class="flex text-base font-medium">Members added</div> <div class="flex text-base font-medium">Members added</div>
@ -154,14 +154,14 @@ 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">
<GeneralProjectIcon :type="project.type" class="nc-view-icon group-hover" /> <GeneralProjectIcon :type="base.type" class="nc-view-icon group-hover" />
<div>{{ $t('activity.shareBase.label') }}</div> <div>{{ $t('activity.shareBase.label') }}</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"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap' }" :style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap' }"
> >
{{ project.title }} {{ base.title }}
</div> </div>
</div> </div>
<LazyDlgShareAndCollaborateShareBase /> <LazyDlgShareAndCollaborateShareBase />
@ -179,7 +179,7 @@ watch(showShareModal, (val) => {
> >
<!-- <a-button <!-- <a-button
v-if="formStatus === 'project-collaborate'" v-if="formStatus === 'base-collaborate'"
data-testid="docs-share-btn" data-testid="docs-share-btn"
class="!border-0 !rounded-md" class="!border-0 !rounded-md"
type="primary" type="primary"

16
packages/nc-gui/components/erd/View.vue

@ -1,11 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import type { BaseType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk' import type { SourceType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { isLinksOrLTAR } from 'nocodb-sdk' import { isLinksOrLTAR } from 'nocodb-sdk'
import type { ERDConfig } from './utils' import type { ERDConfig } from './utils'
import { reactive, ref, storeToRefs, useMetas, useProject, watch } from '#imports' import { reactive, ref, storeToRefs, useMetas, useBase, watch } from '#imports'
const props = defineProps({ const props = defineProps({
baseId: { sourceId: {
type: String, type: String,
default: '', default: '',
}, },
@ -19,7 +19,7 @@ const props = defineProps({
}, },
}) })
const { bases, tables: projectTables } = storeToRefs(useProject()) const { sources, tables: baseTables } = storeToRefs(useBase())
const { metas, getMeta } = useMetas() const { metas, getMeta } = useMetas()
@ -51,7 +51,7 @@ const populateTables = async () => {
let localTables: TableType[] = [] let localTables: TableType[] = []
if (props.table) { if (props.table) {
// if table is provided only get the table and its related tables // if table is provided only get the table and its related tables
localTables = projectTables.value.filter( localTables = baseTables.value.filter(
(t) => (t) =>
t.id === props.table?.id || t.id === props.table?.id ||
metas.value[props.table!.id!].columns?.find((column) => { metas.value[props.table!.id!].columns?.find((column) => {
@ -59,7 +59,7 @@ const populateTables = async () => {
}), }),
) )
} else { } else {
localTables = projectTables.value localTables = baseTables.value
} }
await loadMetaOfTablesNotInMetas(localTables) await loadMetaOfTablesNotInMetas(localTables)
@ -81,7 +81,7 @@ const toggleFullScreen = () => {
config.isFullScreen = !config.isFullScreen config.isFullScreen = !config.isFullScreen
} }
watch([metas, projectTables], populateTables, { watch([metas, baseTables], populateTables, {
flush: 'post', flush: 'post',
immediate: true, immediate: true,
}) })
@ -93,7 +93,7 @@ watch(config, populateTables, {
const filteredTables = computed(() => const filteredTables = computed(() =>
tables.value.filter((t) => tables.value.filter((t) =>
props?.baseId ? t.base_id === props.baseId : t.base_id === bases.value?.filter((base: BaseType) => base.enabled)[0].id, props?.sourceId ? t.source_id === props.sourceId : t.source_id === sources.value?.filter((source: SourceType) => source.enabled)[0].id,
), ),
) )

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

@ -12,11 +12,11 @@ const toggleDialog = inject(ToggleDialogInj, () => {})
<div <div
v-if="isUIAllowed('settingsPage')" v-if="isUIAllowed('settingsPage')"
class="flex items-center w-full pl-3 hover:(text-primary bg-primary bg-opacity-5)" class="flex items-center w-full pl-3 hover:(text-primary bg-primary bg-opacity-5)"
@click="toggleDialog(true, undefined, undefined, projectId)" @click="toggleDialog(true, undefined, undefined, baseId)"
> >
<div> <div>
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<component :is="iconMap.users" class="mr-1 nc-new-base" /> <component :is="iconMap.users" class="mr-1 nc-new-source" />
<div>{{ t('title.teamAndSettings') }}</div> <div>{{ t('title.teamAndSettings') }}</div>
</div> </div>
</div> </div>

10
packages/nc-gui/components/general/HelpAndSupport.vue

@ -1,16 +1,16 @@
<script lang="ts" setup> <script lang="ts" setup>
import { iconMap, ref, storeToRefs, useGlobal, useProject, useRoute } from '#imports' import { iconMap, ref, storeToRefs, useGlobal, useBase, useRoute } from '#imports'
const showDrawer = ref(false) const showDrawer = ref(false)
const { appInfo } = useGlobal() const { appInfo } = useGlobal()
const { project } = storeToRefs(useProject()) const { base } = storeToRefs(useBase())
const route = useRoute() const route = useRoute()
const openSwaggerLink = () => { const openSwaggerLink = () => {
openLink(`./api/v1/db/meta/projects/${route.params.projectId}/swagger`, appInfo.value.ncSiteUrl) openLink(`./api/v1/meta/bases/${route.params.baseId}/swagger`, appInfo.value.ncSiteUrl)
} }
</script> </script>
@ -40,7 +40,7 @@ const openSwaggerLink = () => {
<LazyGeneralSocialCard class="!w-full nc-social-card"> <LazyGeneralSocialCard class="!w-full nc-social-card">
<template #before> <template #before>
<a-list-item v-if="project"> <a-list-item v-if="base">
<nuxt-link <nuxt-link
v-e="['a:navbar:user:swagger']" v-e="['a:navbar:user:swagger']"
no-prefetch no-prefetch
@ -52,7 +52,7 @@ const openSwaggerLink = () => {
<div class="ml-3 flex items-center text-sm"> <div class="ml-3 flex items-center text-sm">
<LogosSwagger /> <LogosSwagger />
<!-- Swagger Documentation --> <!-- Swagger Documentation -->
<span class="ml-3">{{ project.title }} : {{ $t('title.swaggerDocumentation') }}</span> <span class="ml-3">{{ base.title }} : {{ $t('title.swaggerDocumentation') }}</span>
</div> </div>
</nuxt-link> </nuxt-link>
</a-list-item> </a-list-item>

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

@ -1,11 +1,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, iconMap, navigateTo, storeToRefs, useGlobal, useProject, useRoute, useSidebar } from '#imports' import { computed, iconMap, navigateTo, storeToRefs, useGlobal, useBase, useRoute, useSidebar } from '#imports'
const { signOut, signedIn, user, currentVersion } = useGlobal() const { signOut, signedIn, user, currentVersion } = useGlobal()
const { isOpen } = useSidebar('nc-mini-sidebar', { isOpen: true }) const { isOpen } = useSidebar('nc-mini-sidebar', { isOpen: true })
const { project } = storeToRefs(useProject()) const { base } = storeToRefs(useBase())
const route = useRoute() const route = useRoute()
@ -80,9 +80,9 @@ const logout = async () => {
<a-menu-item class="active:(ring ring-accent ring-opacity-100)"> <a-menu-item class="active:(ring ring-accent ring-opacity-100)">
<div <div
v-e="['c:project:create:xcdb']" v-e="['c:base:create:xcdb']"
class="group flex items-center gap-2 py-2 hover:text-primary" class="group flex items-center gap-2 py-2 hover:text-primary"
@click="navigateTo('/project/create')" @click="navigateTo('/base/create')"
> >
<component :is="iconMap.plus" class="text-lg group-hover:text-accent" /> <component :is="iconMap.plus" class="text-lg group-hover:text-accent" />
{{ $t('activity.createProject') }} {{ $t('activity.createProject') }}
@ -91,9 +91,9 @@ const logout = async () => {
<a-menu-item class="rounded-b active:(ring ring-accent)"> <a-menu-item class="rounded-b active:(ring ring-accent)">
<div <div
v-e="['c:project:create:extdb']" v-e="['c:base:create:extdb']"
class="group flex items-center gap-2 py-2 hover:text-primary" class="group flex items-center gap-2 py-2 hover:text-primary"
@click="navigateTo('/project/create-external')" @click="navigateTo('/base/create-external')"
> >
<component :is="iconMap.database" class="text-lg group-hover:text-accent" /> <component :is="iconMap.database" class="text-lg group-hover:text-accent" />
<div v-html="$t('activity.createProjectExtended.extDB')" /> <div v-html="$t('activity.createProjectExtended.extDB')" />
@ -105,12 +105,12 @@ const logout = async () => {
</a-dropdown> </a-dropdown>
<a-tooltip placement="right"> <a-tooltip placement="right">
<template v-if="project" #title>{{ project.title }}</template> <template v-if="base" #title>{{ base.title }}</template>
<div <div
:class="[route.name.includes('nc-projectId') ? 'active' : 'pointer-events-none !text-gray-400']" :class="[route.name.includes('nc-baseId') ? 'active' : 'pointer-events-none !text-gray-400']"
class="nc-mini-sidebar-item" class="nc-mini-sidebar-item"
@click="navigateTo(`/${route.params.projectType}/${route.params.projectId}`)" @click="navigateTo(`/${route.params.baseType}/${route.params.baseId}`)"
> >
<component :is="iconMap.database" class="cursor-pointer transform hover:scale-105 text-2xl" /> <component :is="iconMap.database" class="cursor-pointer transform hover:scale-105 text-2xl" />
</div> </div>

10
packages/nc-gui/components/general/ProjectIcon.vue

@ -7,19 +7,19 @@ const { hoverable } = defineProps<{
<template> <template>
<GeneralIcon <GeneralIcon
icon="project" icon="ncDatabase"
class="text-[#2824FB] nc-project-icon" class="text-[#2824FB] base"
:class="{ :class="{
'nc-project-icon-hoverable': hoverable, 'nc-base-icon-hoverable': hoverable,
}" }"
/> />
</template> </template>
<style scoped> <style scoped>
.nc-project-icon { .nc-base-icon {
@apply text-xl; @apply text-xl;
} }
.nc-project-icon-hoverable { .nc-base-icon-hoverable {
@apply cursor-pointer !hover:bg-gray-200 !hover:bg-opacity-50; @apply cursor-pointer !hover:bg-gray-200 !hover:bg-opacity-50;
} }
</style> </style>

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

@ -13,7 +13,7 @@ const { isMobileMode } = useGlobal()
const { visibility, showShareModal } = storeToRefs(useShare()) const { visibility, showShareModal } = storeToRefs(useShare())
const { activeTable } = storeToRefs(useTablesStore()) const { activeTable } = storeToRefs(useTablesStore())
const { project } = storeToRefs(useProject()) const { base } = storeToRefs(useBase())
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
@ -38,9 +38,9 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<template> <template>
<div <div
v-if="isUIAllowed('projectShare') && visibility !== 'hidden' && (activeTable || project)" v-if="isUIAllowed('baseShare') && visibility !== 'hidden' && (activeTable || base)"
class="flex flex-col justify-center h-full" class="flex flex-col justify-center h-full"
data-testid="share-project-button" data-testid="share-base-button"
:data-sharetype="visibility" :data-sharetype="visibility"
> >
<NcButton <NcButton

1
packages/nc-gui/components/general/TableIcon.vue

@ -16,6 +16,7 @@ const { meta: tableMeta } = defineProps<{
:emoji="tableMeta.meta?.icon" :emoji="tableMeta.meta?.icon"
readonly readonly
/> />
<component :is="iconMap.eye" v-else-if="tableMeta?.type === 'view'" class="w-5 mx-0.75" /> <component :is="iconMap.eye" v-else-if="tableMeta?.type === 'view'" class="w-5 mx-0.75" />
<component :is="iconMap.table" v-else class="w-5 mx-0.5" /> <component :is="iconMap.table" v-else class="w-5 mx-0.5" />
</template> </template>

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

@ -1,27 +1,12 @@
<script lang="ts" setup> <script lang="ts" setup>
const props = withDefaults( const props = defineProps<{
defineProps<{
size?: 'small' | 'medium' | 'base' | 'large' | 'xlarge' size?: 'small' | 'medium' | 'base' | 'large' | 'xlarge'
name?: string name?: string
email?: string }>()
}>(),
{
email: '',
},
)
const { user } = useGlobal() const { user } = useGlobal()
const emailProp = toRef(props, 'email') const backgroundColor = computed(() => (user.value?.id ? stringToColour(user.value?.id) : '#FFFFFF'))
const backgroundColor = computed(() => {
// in comments we need to generate user icon from email
if (emailProp.value.length) {
return stringToColour(emailProp.value)
}
return user.value?.email ? stringToColour(user.value?.email) : '#FFFFFF'
})
const size = computed(() => props.size || 'medium') const size = computed(() => props.size || 'medium')
@ -46,7 +31,7 @@ const usernameInitials = computed(() => {
<template> <template>
<div <div
class="flex nc-user-avatar font-bold" class="flex nc-user-avatar"
:class="{ :class="{
'min-w-4 min-h-4': size === 'small', 'min-w-4 min-h-4': size === 'small',
'min-w-6 min-h-6': size === 'medium', 'min-w-6 min-h-6': size === 'medium',

4
packages/nc-gui/components/general/language/Menu.vue

@ -25,7 +25,7 @@ async function changeLanguage(lang: string) {
<a <a
href="https://docs.nocodb.com/engineering/translation/#how-to-contribute--for-community-members" href="https://docs.nocodb.com/engineering/translation/#how-to-contribute--for-community-members"
target="_blank" target="_blank"
class="caption nc-project-menu-item py-2 text-primary underline hover:opacity-75" class="caption nc-base-menu-item py-2 text-primary underline hover:opacity-75"
> >
{{ $t('activity.translate') }} {{ $t('activity.translate') }}
</a> </a>
@ -39,7 +39,7 @@ async function changeLanguage(lang: string) {
:value="key" :value="key"
@click="changeLanguage(key)" @click="changeLanguage(key)"
> >
<div :class="key === locale ? '!font-semibold !text-primary' : ''" class="nc-project-menu-item capitalize"> <div :class="key === locale ? '!font-semibold !text-primary' : ''" class="nc-base-menu-item capitalize">
{{ Language[key] || lang }} {{ Language[key] || lang }}
</div> </div>
</a-menu-item> </a-menu-item>

4
packages/nc-gui/components/notification/Item/ProjectEvent.vue

@ -24,14 +24,14 @@ const action = computed(() => {
const onClick = () => { const onClick = () => {
if (item.value.type === AppEvents.PROJECT_DELETE) return if (item.value.type === AppEvents.PROJECT_DELETE) return
navigateToProject({ projectId: item.value.body.id }) navigateToProject({ baseId: item.value.body.id })
} }
</script> </script>
<template> <template>
<NotificationItemWrapper :item="item" @click="onClick"> <NotificationItemWrapper :item="item" @click="onClick">
<div class="text-xs gap-2"> <div class="text-xs gap-2">
Project Base
<GeneralProjectIcon style="vertical-align: middle" :type="item.body.type" /> <strong>{{ item.body.title }}</strong> <GeneralProjectIcon style="vertical-align: middle" :type="item.body.type" /> <strong>{{ item.body.title }}</strong>
{{ action }} successfully {{ action }} successfully
</div> </div>

4
packages/nc-gui/components/notification/Item/ProjectInvite.vue

@ -10,10 +10,10 @@ const item = toRef(props, 'item')
</script> </script>
<template> <template>
<NotificationItemWrapper :item="item" @click="navigateToProject({ projectId: item.body.id })"> <NotificationItemWrapper :item="item" @click="navigateToProject({ baseId: item.body.id })">
<div class="text-xs"> <div class="text-xs">
<strong>{{ item.body.invited_by }}</strong> has invited you to collaborate on <strong>{{ item.body.invited_by }}</strong> has invited you to collaborate on
<!-- <GeneralProjectIcon style="vertical-align: middle" :type="item.body.type" /> <strong>{{ item.body.title }}</strong> project. --> <!-- <GeneralProjectIcon style="vertical-align: middle" :type="item.body.type" /> <strong>{{ item.body.title }}</strong> base. -->
</div> </div>
</NotificationItemWrapper> </NotificationItemWrapper>
</template> </template>

2
packages/nc-gui/components/notification/Item/SharedViewEvent.vue

@ -23,7 +23,7 @@ const action = computed(() => {
const onClick = () => { const onClick = () => {
if (item.value.type === AppEvents.VIEW_DELETE) return if (item.value.type === AppEvents.VIEW_DELETE) return
navigateToProject({ projectId: item.value.body.id }) navigateToProject({ baseId: item.value.body.id })
} }
</script> </script>

2
packages/nc-gui/components/notification/Item/TableEvent.vue

@ -23,7 +23,7 @@ const action = computed(() => {
const onClick = () => { const onClick = () => {
if (item.value.type === AppEvents.TABLE_DELETE) return if (item.value.type === AppEvents.TABLE_DELETE) return
navigateToProject({ projectId: item.value.body.id }) navigateToProject({ baseId: item.value.body.id })
} }
</script> </script>

2
packages/nc-gui/components/notification/Item/ViewEvent.vue

@ -23,7 +23,7 @@ const action = computed(() => {
const onClick = () => { const onClick = () => {
if (item.value.type === AppEvents.VIEW_DELETE) return if (item.value.type === AppEvents.VIEW_DELETE) return
navigateToProject({ projectId: item.value.body.id }) navigateToProject({ baseId: item.value.body.id })
} }
</script> </script>

4
packages/nc-gui/components/profile/overview/contributionActivity.vue

@ -19,7 +19,7 @@
</div> </div>
</a-timeline-item> </a-timeline-item>
<a-timeline-item> <a-timeline-item>
<div>Answered 2 discussions in 1 project</div> <div>Answered 2 discussions in 1 base</div>
<div class="text-primary">slair.xyz / project_8</div> <div class="text-primary">slair.xyz / project_8</div>
<div class="prose-xs"> <div class="prose-xs">
<div class="flex items-center mr-4"> <div class="flex items-center mr-4">
@ -49,7 +49,7 @@
</div> </div>
</a-timeline-item> </a-timeline-item>
<a-timeline-item> <a-timeline-item>
<div>Answered 2 discussions in 1 project</div> <div>Answered 2 discussions in 1 base</div>
<div class="text-primary">slair.xyz / project_8</div> <div class="text-primary">slair.xyz / project_8</div>
<div class="prose-xs"> <div class="prose-xs">
<div class="flex items-center mr-4"> <div class="flex items-center mr-4">

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

@ -4,11 +4,11 @@ import { OrderedProjectRoles, ProjectRoles, WorkspaceRolesToProjectRoles } from
import InfiniteLoading from 'v3-infinite-loading' import InfiniteLoading from 'v3-infinite-loading'
import { isEeUI, storeToRefs, stringToColour, timeAgo } from '#imports' import { isEeUI, storeToRefs, stringToColour, timeAgo } from '#imports'
const projectsStore = useProjects() const basesStore = useBases()
const { getProjectUsers, createProjectUser, updateProjectUser, removeProjectUser } = projectsStore const { getProjectUsers, createProjectUser, updateProjectUser, removeProjectUser } = basesStore
const { activeProjectId } = storeToRefs(projectsStore) const { activeProjectId } = storeToRefs(basesStore)
const { projectRoles } = useRoles() const { baseRoles } = useRoles()
const collaborators = ref< const collaborators = ref<
{ {
@ -33,7 +33,7 @@ const loadCollaborators = async () => {
currentPage.value += 1 currentPage.value += 1
const { users, totalRows } = await getProjectUsers({ const { users, totalRows } = await getProjectUsers({
projectId: activeProjectId.value!, baseId: activeProjectId.value!,
page: currentPage.value, page: currentPage.value,
...(!userSearchText.value ? {} : ({ searchText: userSearchText.value } as any)), ...(!userSearchText.value ? {} : ({ searchText: userSearchText.value } as any)),
limit: 20, limit: 20,
@ -44,7 +44,7 @@ const loadCollaborators = async () => {
...collaborators.value, ...collaborators.value,
...users.map((user: any) => ({ ...users.map((user: any) => ({
...user, ...user,
project_roles: user.roles, base_roles: user.roles,
roles: roles:
user.roles ?? user.roles ??
(user.workspace_roles (user.workspace_roles
@ -98,7 +98,7 @@ const updateCollaborator = async (collab: any, roles: ProjectRoles) => {
} else { } else {
collab.roles = ProjectRoles.NO_ACCESS collab.roles = ProjectRoles.NO_ACCESS
} }
} else if (collab.project_roles) { } else if (collab.base_roles) {
collab.roles = roles collab.roles = roles
await updateProjectUser(activeProjectId.value!, collab) await updateProjectUser(activeProjectId.value!, collab)
} else { } else {
@ -140,7 +140,7 @@ onMounted(async () => {
try { try {
await loadCollaborators() await loadCollaborators()
const currentRoleIndex = OrderedProjectRoles.findIndex( const currentRoleIndex = OrderedProjectRoles.findIndex(
(role) => projectRoles.value && Object.keys(projectRoles.value).includes(role), (role) => baseRoles.value && Object.keys(baseRoles.value).includes(role),
) )
if (currentRoleIndex !== -1) { if (currentRoleIndex !== -1) {
accessibleRoles.value = OrderedProjectRoles.slice(currentRoleIndex + 1) accessibleRoles.value = OrderedProjectRoles.slice(currentRoleIndex + 1)

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

@ -1,11 +1,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { BaseType, TableType } from 'nocodb-sdk' import type { SourceType, TableType } from 'nocodb-sdk'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import NcTooltip from '~/components/nc/Tooltip.vue' import NcTooltip from '~/components/nc/Tooltip.vue'
const { activeTables } = storeToRefs(useTablesStore()) const { activeTables } = storeToRefs(useTablesStore())
const { openTable } = useTablesStore() const { openTable } = useTablesStore()
const { openedProject } = storeToRefs(useProjects()) const { openedProject } = storeToRefs(useBases())
const isNewBaseModalOpen = ref(false) const isNewBaseModalOpen = ref(false)
@ -18,16 +18,16 @@ const { $e } = useNuxtApp()
const isImportModalOpen = ref(false) const isImportModalOpen = ref(false)
const defaultBase = computed(() => { const defaultBase = computed(() => {
return openedProject.value?.bases?.[0] return openedProject.value?.sources?.[0]
}) })
const bases = computed(() => { const sources = computed(() => {
// Convert array of bases to map of bases // Convert array of sources to map of sources
const baseMap = new Map<string, BaseType>() const baseMap = new Map<string, SourceType>()
openedProject.value?.bases?.forEach((base) => { openedProject.value?.sources?.forEach((source) => {
baseMap.set(base.id!, base) baseMap.set(source.id!, source)
}) })
return baseMap return baseMap
@ -37,17 +37,17 @@ function openTableCreateDialog(baseIndex?: number | undefined) {
$e('c:table:create:navdraw') $e('c:table:create:navdraw')
const isOpen = ref(true) const isOpen = ref(true)
let baseId = openedProject.value!.bases?.[0].id let sourceId = openedProject.value!.sources?.[0].id
if (typeof baseIndex === 'number') { if (typeof baseIndex === 'number') {
baseId = openedProject.value!.bases?.[baseIndex].id sourceId = openedProject.value!.sources?.[baseIndex].id
} }
if (!baseId || !openedProject.value?.id) return if (!sourceId || !openedProject.value?.id) return
const { close } = useDialog(resolveComponent('DlgTableCreate'), { const { close } = useDialog(resolveComponent('DlgTableCreate'), {
'modelValue': isOpen, 'modelValue': isOpen,
baseId, // || bases.value[0].id, sourceId, // || sources.value[0].id,
'projectId': openedProject.value.id, 'baseId': openedProject.value.id,
'onCreate': closeDialog, 'onCreate': closeDialog,
'onUpdate:modelValue': () => closeDialog(), 'onUpdate:modelValue': () => closeDialog(),
}) })
@ -82,7 +82,7 @@ const onCreateBaseClick = () => {
<div <div
v-if="isUIAllowed('tableCreate')" v-if="isUIAllowed('tableCreate')"
role="button" role="button"
class="nc-project-view-all-table-btn" class="nc-base-view-all-table-btn"
data-testid="proj-view-btn__add-new-table" data-testid="proj-view-btn__add-new-table"
@click="openTableCreateDialog()" @click="openTableCreateDialog()"
> >
@ -93,14 +93,14 @@ const onCreateBaseClick = () => {
v-if="isUIAllowed('tableCreate')" v-if="isUIAllowed('tableCreate')"
v-e="['c:table:import']" v-e="['c:table:import']"
role="button" role="button"
class="nc-project-view-all-table-btn" class="nc-base-view-all-table-btn"
data-testid="proj-view-btn__import-data" data-testid="proj-view-btn__import-data"
@click="isImportModalOpen = true" @click="isImportModalOpen = true"
> >
<GeneralIcon icon="download" /> <GeneralIcon icon="download" />
<div class="label">{{ $t('activity.import') }} {{ $t('general.data') }}</div> <div class="label">{{ $t('activity.import') }} {{ $t('general.data') }}</div>
</div> </div>
<component :is="isDataSourceLimitReached ? NcTooltip : 'div'" v-if="isUIAllowed('baseCreate')"> <component :is="isDataSourceLimitReached ? NcTooltip : 'div'" v-if="isUIAllowed('sourceCreate')">
<template #title> <template #title>
<div> <div>
{{ $t('tooltip.reachedSourceLimit') }} {{ $t('tooltip.reachedSourceLimit') }}
@ -109,7 +109,7 @@ const onCreateBaseClick = () => {
<div <div
v-e="['c:table:create-source']" v-e="['c:table:create-source']"
role="button" role="button"
class="nc-project-view-all-table-btn" class="nc-base-view-all-table-btn"
data-testid="proj-view-btn__create-source" data-testid="proj-view-btn__create-source"
:class="{ :class="{
disabled: isDataSourceLimitReached, disabled: isDataSourceLimitReached,
@ -127,14 +127,14 @@ const onCreateBaseClick = () => {
<div class="w-1/5">{{ $t('labels.createdOn') }}</div> <div class="w-1/5">{{ $t('labels.createdOn') }}</div>
</div> </div>
<div <div
class="nc-project-view-all-table-list nc-scrollbar-md" class="nc-base-view-all-table-list nc-scrollbar-md"
:style="{ :style="{
height: 'calc(100vh - var(--topbar-height) - 18rem)', height: 'calc(100vh - var(--topbar-height) - 18rem)',
}" }"
> >
<div <div
v-for="table in [...activeTables].sort( v-for="table in [...activeTables].sort(
(a, b) => a.base_id!.localeCompare(b.base_id!) * 20 (a, b) => a.source_id!.localeCompare(b.source_id!) * 20
)" )"
:key="table.id" :key="table.id"
class="py-4 flex flex-row w-full cursor-pointer hover:bg-gray-100 border-b-1 border-gray-100 px-2.25" class="py-4 flex flex-row w-full cursor-pointer hover:bg-gray-100 border-b-1 border-gray-100 px-2.25"
@ -146,10 +146,10 @@ const onCreateBaseClick = () => {
{{ table?.title }} {{ table?.title }}
</div> </div>
<div class="w-1/5 text-gray-600" data-testid="proj-view-list__item-type"> <div class="w-1/5 text-gray-600" data-testid="proj-view-list__item-type">
<div v-if="table.base_id === defaultBase?.id" class="ml-0.75">-</div> <div v-if="table.source_id === defaultBase?.id" class="ml-0.75">-</div>
<div v-else> <div v-else>
<GeneralBaseLogo :base-type="bases.get(table.base_id!)?.type" class="w-4 mr-1" /> <GeneralBaseLogo :source-type="sources.get(table.source_id!)?.type" class="w-4 mr-1" />
{{ bases.get(table.base_id!)?.alias }} {{ sources.get(table.source_id!)?.alias }}
</div> </div>
</div> </div>
<div class="w-1/5 text-gray-400 ml-0.25" data-testid="proj-view-list__item-created-at"> <div class="w-1/5 text-gray-400 ml-0.25" data-testid="proj-view-list__item-created-at">
@ -157,7 +157,7 @@ const onCreateBaseClick = () => {
</div> </div>
</div> </div>
</div> </div>
<ProjectImportModal v-if="defaultBase" v-model:visible="isImportModalOpen" :base="defaultBase" /> <ProjectImportModal v-if="defaultBase" v-model:visible="isImportModalOpen" :source="defaultBase" />
<GeneralModal v-model:visible="isNewBaseModalOpen" size="medium"> <GeneralModal v-model:visible="isNewBaseModalOpen" size="medium">
<div class="py-6 px-8"> <div class="py-6 px-8">
<LazyDashboardSettingsDataSourcesCreateBase @close="isNewBaseModalOpen = false" /> <LazyDashboardSettingsDataSourcesCreateBase @close="isNewBaseModalOpen = false" />
@ -167,7 +167,7 @@ const onCreateBaseClick = () => {
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.nc-project-view-all-table-btn { .nc-base-view-all-table-btn {
@apply flex flex-col gap-y-6 p-4 bg-gray-100 rounded-xl w-56 cursor-pointer text-gray-600 hover:(bg-gray-200 text-black); @apply flex flex-col gap-y-6 p-4 bg-gray-100 rounded-xl w-56 cursor-pointer text-gray-600 hover:(bg-gray-200 text-black);
.nc-icon { .nc-icon {
@ -179,7 +179,7 @@ const onCreateBaseClick = () => {
} }
} }
.nc-project-view-all-table-btn.disabled { .nc-base-view-all-table-btn.disabled {
@apply bg-gray-50 text-gray-400 hover:(bg-gray-50 text-gray-400) cursor-not-allowed; @apply bg-gray-50 text-gray-400 hover:(bg-gray-50 text-gray-400) cursor-not-allowed;
} }
</style> </style>

28
packages/nc-gui/components/project/ImportModal.vue

@ -1,21 +1,21 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { BaseType } from 'nocodb-sdk' import type { SourceType } from 'nocodb-sdk'
const props = defineProps<{ const props = defineProps<{
visible: boolean visible: boolean
base: BaseType source: SourceType
}>() }>()
const emits = defineEmits(['update:visible']) const emits = defineEmits(['update:visible'])
const base = toRef(props, 'base') const source = toRef(props, 'source')
const visible = useVModel(props, 'visible', emits) const visible = useVModel(props, 'visible', emits)
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
function openAirtableImportDialog(baseId?: string) { function openAirtableImportDialog(sourceId?: string) {
if (!baseId) return if (!sourceId) return
$e('a:actions:import-airtable') $e('a:actions:import-airtable')
@ -23,7 +23,7 @@ function openAirtableImportDialog(baseId?: string) {
const { close } = useDialog(resolveComponent('DlgAirtableImport'), { const { close } = useDialog(resolveComponent('DlgAirtableImport'), {
'modelValue': isOpen, 'modelValue': isOpen,
'baseId': baseId, 'sourceId': sourceId,
'onUpdate:modelValue': closeDialog, 'onUpdate:modelValue': closeDialog,
}) })
@ -35,7 +35,7 @@ function openAirtableImportDialog(baseId?: string) {
} }
function openQuickImportDialog(type: 'csv' | 'excel' | 'json') { function openQuickImportDialog(type: 'csv' | 'excel' | 'json') {
if (!base.value.id) return if (!source.value.id) return
$e(`a:actions:import-${type}`) $e(`a:actions:import-${type}`)
@ -44,7 +44,7 @@ function openQuickImportDialog(type: 'csv' | 'excel' | 'json') {
const { close } = useDialog(resolveComponent('DlgQuickImport'), { const { close } = useDialog(resolveComponent('DlgQuickImport'), {
'modelValue': isOpen, 'modelValue': isOpen,
'importType': type, 'importType': type,
'baseId': base.value.id, 'sourceId': source.value.id,
'onUpdate:modelValue': closeDialog, 'onUpdate:modelValue': closeDialog,
}) })
@ -59,7 +59,7 @@ const onClick = (type: 'airtable' | 'csv' | 'excel' | 'json') => {
visible.value = false visible.value = false
if (type === 'airtable') { if (type === 'airtable') {
openAirtableImportDialog(base.value.id) openAirtableImportDialog(source.value.id)
} else { } else {
openQuickImportDialog(type) openQuickImportDialog(type)
} }
@ -71,21 +71,21 @@ const onClick = (type: 'airtable' | 'csv' | 'excel' | 'json') => {
<div class="flex flex-col px-8 pt-6 pb-9"> <div class="flex flex-col px-8 pt-6 pb-9">
<div class="text-lg font-medium mb-6">{{ $t('general.import') }}</div> <div class="text-lg font-medium mb-6">{{ $t('general.import') }}</div>
<div class="row mb-10"> <div class="row mb-10">
<div class="nc-project-view-import-sub-btn" @click="onClick('airtable')"> <div class="nc-base-view-import-sub-btn" @click="onClick('airtable')">
<GeneralIcon icon="airtable" /> <GeneralIcon icon="airtable" />
<div class="label">{{ $t('labels.airtable') }}</div> <div class="label">{{ $t('labels.airtable') }}</div>
</div> </div>
<div class="nc-project-view-import-sub-btn" @click="onClick('csv')"> <div class="nc-base-view-import-sub-btn" @click="onClick('csv')">
<GeneralIcon icon="csv" /> <GeneralIcon icon="csv" />
<div class="label">{{ $t('labels.csv') }}</div> <div class="label">{{ $t('labels.csv') }}</div>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="nc-project-view-import-sub-btn" @click="onClick('excel')"> <div class="nc-base-view-import-sub-btn" @click="onClick('excel')">
<GeneralIcon icon="excelColored" /> <GeneralIcon icon="excelColored" />
<div class="label">{{ $t('labels.excel') }}</div> <div class="label">{{ $t('labels.excel') }}</div>
</div> </div>
<div class="nc-project-view-import-sub-btn" @click="onClick('json')"> <div class="nc-base-view-import-sub-btn" @click="onClick('json')">
<GeneralIcon icon="code" /> <GeneralIcon icon="code" />
<div class="label">{{ $t('labels.json') }}</div> <div class="label">{{ $t('labels.json') }}</div>
</div> </div>
@ -98,7 +98,7 @@ const onClick = (type: 'airtable' | 'csv' | 'excel' | 'json') => {
.row { .row {
@apply flex flex-row gap-x-10; @apply flex flex-row gap-x-10;
} }
.nc-project-view-import-sub-btn { .nc-base-view-import-sub-btn {
@apply flex flex-col gap-y-6 p-16 bg-gray-50 items-center justify-center rounded-xl w-56 cursor-pointer text-gray-600 hover:(bg-gray-100 !text-black); @apply flex flex-col gap-y-6 p-16 bg-gray-50 items-center justify-center rounded-xl w-56 cursor-pointer text-gray-600 hover:(bg-gray-100 !text-black);
.nc-icon { .nc-icon {

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

@ -1,11 +1,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useTitle } from '@vueuse/core' import { useTitle } from '@vueuse/core'
import NcLayout from '~icons/nc-icons/layout' import NcLayout from '~icons/nc-icons/layout'
const { openedProject } = storeToRefs(useProjects()) const { openedProject } = storeToRefs(useBases())
const { activeTables } = storeToRefs(useTablesStore()) const { activeTables } = storeToRefs(useTablesStore())
const { activeWorkspace } = storeToRefs(useWorkspace()) const { activeWorkspace } = storeToRefs(useWorkspace())
const { navigateToProjectPage } = useProject() const { navigateToProjectPage } = useBase()
const router = useRouter() const router = useRouter()
const route = router.currentRoute const route = router.currentRoute
@ -13,12 +13,12 @@ const route = router.currentRoute
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
/* const defaultBase = computed(() => { /* const defaultBase = computed(() => {
return openedProject.value?.bases?.[0] return openedProject.value?.sources?.[0]
}) */ }) */
const { isUIAllowed } = useRoles() const { isUIAllowed } = useRoles()
const { project } = storeToRefs(useProject()) const { base } = storeToRefs(useBase())
const { projectPageTab } = storeToRefs(useConfigStore()) const { projectPageTab } = storeToRefs(useConfigStore())
@ -65,7 +65,7 @@ watch(
</script> </script>
<template> <template>
<div class="h-full nc-project-view"> <div class="h-full nc-base-view">
<div <div
class="flex flex-row pl-2 pr-2 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 }"
@ -80,7 +80,7 @@ watch(
<LazyGeneralShareProject /> <LazyGeneralShareProject />
</div> </div>
<div <div
class="flex mx-12 my-8 nc-project-view-tab" class="flex mx-12 my-8 nc-base-view-tab"
:style="{ :style="{
height: 'calc(100% - var(--topbar-height))', height: 'calc(100% - var(--topbar-height))',
}" }"
@ -104,8 +104,8 @@ watch(
</template> </template>
<ProjectAllTables /> <ProjectAllTables />
</a-tab-pane> </a-tab-pane>
<!-- <a-tab-pane v-if="defaultBase" key="erd" tab="Project ERD" force-render class="pt-4 pb-12"> <!-- <a-tab-pane v-if="defaultBase" key="erd" tab="Base ERD" force-render class="pt-4 pb-12">
<ErdView :base-id="defaultBase!.id" class="!h-full" /> <ErdView :source-id="defaultBase!.id" class="!h-full" />
</a-tab-pane> --> </a-tab-pane> -->
<a-tab-pane v-if="isUIAllowed('newUser')" key="collaborator"> <a-tab-pane v-if="isUIAllowed('newUser')" key="collaborator">
<template #tab> <template #tab>
@ -122,14 +122,14 @@ watch(
<GeneralIcon icon="database" /> <GeneralIcon icon="database" />
<div>{{ $t('labels.dataSources') }}</div> <div>{{ $t('labels.dataSources') }}</div>
<div <div
v-if="project.bases?.length" v-if="base.sources?.length"
class="tab-info" class="tab-info"
:class="{ :class="{
'bg-primary-selected': projectPageTab === 'data-source', 'bg-primary-selected': projectPageTab === 'data-source',
'bg-gray-50': projectPageTab !== 'data-source', 'bg-gray-50': projectPageTab !== 'data-source',
}" }"
> >
{{ project.bases.length - 1 }} {{ base.sources.length - 1 }}
</div> </div>
</div> </div>
</template> </template>

4
packages/nc-gui/components/shared-view/Grid.vue

@ -12,7 +12,7 @@ import {
provide, provide,
ref, ref,
useGlobal, useGlobal,
useProject, useBase,
useProvideSmartsheetStore, useProvideSmartsheetStore,
useSharedView, useSharedView,
} from '#imports' } from '#imports'
@ -21,7 +21,7 @@ const { sharedView, meta, sorts, nestedFilters } = useSharedView()
const { signedIn } = useGlobal() const { signedIn } = useGlobal()
const { loadProject } = useProject() const { loadProject } = useBase()
const { isLocked } = useProvideSmartsheetStore(sharedView, meta, true, sorts, nestedFilters) const { isLocked } = useProvideSmartsheetStore(sharedView, meta, true, sorts, nestedFilters)

10
packages/nc-gui/components/smartsheet/ApiSnippet.vue

@ -10,7 +10,7 @@ import {
useCopy, useCopy,
useGlobal, useGlobal,
useI18n, useI18n,
useProject, useBase,
useSmartsheetStoreOrThrow, useSmartsheetStoreOrThrow,
useVModel, useVModel,
useViewData, useViewData,
@ -25,8 +25,8 @@ const emits = defineEmits(['update:modelValue'])
const { t } = useI18n() const { t } = useI18n()
const projectStore = useProject() const baseStore = useBase()
const { project } = storeToRefs(projectStore) const { base } = storeToRefs(baseStore)
const { appInfo, token } = useGlobal() const { appInfo, token } = useGlobal()
@ -84,7 +84,7 @@ const selectedLangName = ref(langs[0].name)
const apiUrl = computed( const apiUrl = computed(
() => () =>
new URL( new URL(
`/api/v1/db/data/noco/${project.value.id}/${meta.value?.title}/views/${view.value?.title}`, `/api/v1/data/noco/${base.value.id}/${meta.value?.title}/views/${view.value?.title}`,
(appInfo.value && appInfo.value.ncSiteUrl) || '/', (appInfo.value && appInfo.value.ncSiteUrl) || '/',
).href, ).href,
) )
@ -118,7 +118,7 @@ const api = new Api({
api.dbViewRow.list( api.dbViewRow.list(
"noco", "noco",
${JSON.stringify(project.value.title)}, ${JSON.stringify(base.value.title)},
${JSON.stringify(meta.value?.title)}, ${JSON.stringify(meta.value?.title)},
${JSON.stringify(view.value?.title)}, ${JSON.stringify(queryParams.value, null, 4)}).then(function (data) { ${JSON.stringify(view.value?.title)}, ${JSON.stringify(queryParams.value, null, 4)}).then(function (data) {
console.log(data); console.log(data);

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

@ -47,7 +47,7 @@ import {
storeToRefs, storeToRefs,
toRef, toRef,
useDebounceFn, useDebounceFn,
useProject, useBase,
useSmartsheetRowStoreOrThrow, useSmartsheetRowStoreOrThrow,
} from '#imports' } from '#imports'
@ -97,9 +97,9 @@ const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))
const { currentRow } = useSmartsheetRowStoreOrThrow() const { currentRow } = useSmartsheetRowStoreOrThrow()
const { sqlUis } = storeToRefs(useProject()) const { sqlUis } = storeToRefs(useBase())
const sqlUi = ref(column.value?.base_id ? sqlUis.value[column.value?.base_id] : Object.values(sqlUis.value)[0]) const sqlUi = ref(column.value?.source_id ? sqlUis.value[column.value?.source_id] : Object.values(sqlUis.value)[0])
const abstractType = computed(() => column.value && sqlUi.value.getAbstractType(column.value)) const abstractType = computed(() => column.value && sqlUi.value.getAbstractType(column.value))

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

@ -212,14 +212,14 @@ watch(
<template #overlay> <template #overlay>
<a-menu class="shadow !rounded !py-0" @click="contextMenu = false"> <a-menu class="shadow !rounded !py-0" @click="contextMenu = false">
<a-menu-item v-if="contextMenuTarget" @click="deleteRow(contextMenuTarget.row)"> <a-menu-item v-if="contextMenuTarget" @click="deleteRow(contextMenuTarget.row)">
<div v-e="['a:row:delete']" class="nc-project-menu-item"> <div v-e="['a:row:delete']" class="nc-base-menu-item">
<!-- Delete Row --> <!-- Delete Row -->
{{ $t('activity.deleteRow') }} {{ $t('activity.deleteRow') }}
</div> </div>
</a-menu-item> </a-menu-item>
<!-- <a-menu-item v-if="contextMenuTarget" @click="openNewRecordFormHook.trigger()"> --> <!-- <a-menu-item v-if="contextMenuTarget" @click="openNewRecordFormHook.trigger()"> -->
<!-- <div v-e="['a:row:insert']" class="nc-project-menu-item"> --> <!-- <div v-e="['a:row:insert']" class="nc-base-menu-item"> -->
<!-- &lt;!&ndash; Insert New Row &ndash;&gt; --> <!-- &lt;!&ndash; Insert New Row &ndash;&gt; -->
<!-- {{ $t('activity.insertRow') }} --> <!-- {{ $t('activity.insertRow') }} -->
<!-- </div> --> <!-- </div> -->

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

@ -679,7 +679,7 @@ watch(
<template v-if="!isLocked && !isPublic && hasEditPermission" #overlay> <template v-if="!isLocked && !isPublic && hasEditPermission" #overlay>
<a-menu class="shadow !rounded !py-0" @click="contextMenu = false"> <a-menu class="shadow !rounded !py-0" @click="contextMenu = false">
<a-menu-item v-if="contextMenuTarget" @click="expandForm(contextMenuTarget)"> <a-menu-item v-if="contextMenuTarget" @click="expandForm(contextMenuTarget)">
<div v-e="['a:kanban:expand-record']" class="nc-project-menu-item nc-kanban-context-menu-item"> <div v-e="['a:kanban:expand-record']" class="nc-base-menu-item nc-kanban-context-menu-item">
<component :is="iconMap.expand" class="flex" /> <component :is="iconMap.expand" class="flex" />
<!-- Expand Record --> <!-- Expand Record -->
{{ $t('activity.expandRecord') }} {{ $t('activity.expandRecord') }}
@ -687,7 +687,7 @@ watch(
</a-menu-item> </a-menu-item>
<a-divider class="!m-0 !p-0" /> <a-divider class="!m-0 !p-0" />
<a-menu-item v-if="contextMenuTarget" @click="deleteRow(contextMenuTarget)"> <a-menu-item v-if="contextMenuTarget" @click="deleteRow(contextMenuTarget)">
<div v-e="['a:kanban:delete-record']" class="nc-project-menu-item nc-kanban-context-menu-item"> <div v-e="['a:kanban:delete-record']" class="nc-base-menu-item nc-kanban-context-menu-item">
<component :is="iconMap.delete" class="flex" /> <component :is="iconMap.delete" class="flex" />
<!-- Delete Record --> <!-- Delete Record -->
{{ $t('activity.deleteRecord') }} {{ $t('activity.deleteRecord') }}

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

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { UITypes } from 'nocodb-sdk' import { UITypes } from 'nocodb-sdk'
import { MetaInj, computed, useColumnCreateStoreOrThrow, useProject, useVModel } from '#imports' import { MetaInj, computed, useColumnCreateStoreOrThrow, useBase, useVModel } from '#imports'
const props = defineProps<{ const props = defineProps<{
value: any value: any
@ -16,7 +16,7 @@ const { onAlter, onDataTypeChange, validateInfos, sqlUi } = useColumnCreateStore
// todo: 2nd argument of `getDataTypeListForUiType` is missing! // todo: 2nd argument of `getDataTypeListForUiType` is missing!
const dataTypes = computed(() => sqlUi.value.getDataTypeListForUiType(vModel.value as { uidt: UITypes }, '' as any)) const dataTypes = computed(() => sqlUi.value.getDataTypeListForUiType(vModel.value as { uidt: UITypes }, '' as any))
const { isPg } = useProject() const { isPg } = useBase()
const meta = inject(MetaInj, ref()) const meta = inject(MetaInj, ref())
@ -93,7 +93,7 @@ vModel.value.au = !!vModel.value.au */
<a-input v-model:value="vModel.dtxs" class="!rounded-md" :disabled="!sqlUi.columnEditable(vModel)" @input="onAlter" /> <a-input v-model:value="vModel.dtxs" class="!rounded-md" :disabled="!sqlUi.columnEditable(vModel)" @input="onAlter" />
</a-form-item> </a-form-item>
<LazySmartsheetColumnPgBinaryOptions v-if="isPg(meta?.base_id) && vModel.dt === 'bytea'" v-model:value="vModel" /> <LazySmartsheetColumnPgBinaryOptions v-if="isPg(meta?.source_id) && vModel.dt === 'bytea'" v-model:value="vModel" />
</template> </template>
</div> </div>
</template> </template>

8
packages/nc-gui/components/smartsheet/column/EditOrAdd.vue

@ -19,7 +19,7 @@ import {
useI18n, useI18n,
useMetas, useMetas,
useNuxtApp, useNuxtApp,
useProject, useBase,
watchEffect, watchEffect,
} from '#imports' } from '#imports'
import MdiMinusIcon from '~icons/mdi/minus-circle-outline' import MdiMinusIcon from '~icons/mdi/minus-circle-outline'
@ -66,7 +66,7 @@ const isForm = inject(IsFormInj, ref(false))
const isKanban = inject(IsKanbanInj, ref(false)) const isKanban = inject(IsKanbanInj, ref(false))
const { isMysql, isMssql } = useProject() const { isMysql, isMssql } = useBase()
const reloadDataTrigger = inject(ReloadViewDataHookInj) const reloadDataTrigger = inject(ReloadViewDataHookInj)
@ -327,8 +327,8 @@ if (props.fromTableExplorer) {
v-if=" v-if="
!isVirtualCol(formState) && !isVirtualCol(formState) &&
!isAttachment(formState) && !isAttachment(formState) &&
!isMssql(meta!.base_id) && !isMssql(meta!.source_id) &&
!(isMysql(meta!.base_id) && (isJSON(formState) || isTextArea(formState))) !(isMysql(meta!.source_id) && (isJSON(formState) || isTextArea(formState)))
" "
v-model:value="formState" v-model:value="formState"
/> />

10
packages/nc-gui/components/smartsheet/column/LinkedToAnotherRecordOptions.vue

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ModelTypes, MssqlUi, SqliteUi, UITypes } from 'nocodb-sdk' import { ModelTypes, MssqlUi, SqliteUi, UITypes } from 'nocodb-sdk'
import { MetaInj, inject, ref, storeToRefs, useProject, useVModel } from '#imports' import { MetaInj, inject, ref, storeToRefs, useBase, useVModel } from '#imports'
import MdiPlusIcon from '~icons/mdi/plus-circle-outline' import MdiPlusIcon from '~icons/mdi/plus-circle-outline'
import MdiMinusIcon from '~icons/mdi/minus-circle-outline' import MdiMinusIcon from '~icons/mdi/minus-circle-outline'
@ -16,8 +16,8 @@ const meta = inject(MetaInj, ref())
const { setAdditionalValidations, validateInfos, onDataTypeChange, sqlUi, isXcdbBase } = useColumnCreateStoreOrThrow() const { setAdditionalValidations, validateInfos, onDataTypeChange, sqlUi, isXcdbBase } = useColumnCreateStoreOrThrow()
const projectStore = useProject() const baseStore = useBase()
const { tables } = storeToRefs(projectStore) const { tables } = storeToRefs(baseStore)
const { t } = useI18n() const { t } = useI18n()
@ -47,7 +47,7 @@ const refTables = computed(() => {
return [] return []
} }
return tables.value.filter((t) => t.type === ModelTypes.TABLE && t.base_id === meta.value?.base_id) return tables.value.filter((t) => t.type === ModelTypes.TABLE && t.source_id === meta.value?.source_id)
}) })
const filterOption = (value: string, option: { key: string }) => option.key.toLowerCase().includes(value.toLowerCase()) const filterOption = (value: string, option: { key: string }) => option.key.toLowerCase().includes(value.toLowerCase())
@ -80,7 +80,7 @@ const isLinks = computed(() => vModel.value.uidt === UITypes.Links)
<a-select-option v-for="table of refTables" :key="table.title" :value="table.id"> <a-select-option v-for="table of refTables" :key="table.title" :value="table.id">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="min-w-5 flex items-center justify-center"> <div class="min-w-5 flex items-center justify-center">
<GeneralTableIcon :meta="table" class="text-gray-500" /> <GeneralTableIcon :meta="table" class="text-gray-500"></GeneralTableIcon>
</div> </div>
<span class="overflow-ellipsis min-w-0 shrink-1">{{ table.title }}</span> <span class="overflow-ellipsis min-w-0 shrink-1">{{ table.title }}</span>

8
packages/nc-gui/components/smartsheet/column/LookupOptions.vue

@ -2,7 +2,7 @@
import { onMounted } from '@vue/runtime-core' import { onMounted } from '@vue/runtime-core'
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { UITypes, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk' import { UITypes, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { MetaInj, inject, ref, storeToRefs, useColumnCreateStoreOrThrow, useMetas, useProject, useVModel } from '#imports' import { MetaInj, inject, ref, storeToRefs, useColumnCreateStoreOrThrow, useMetas, useBase, useVModel } from '#imports'
const props = defineProps<{ const props = defineProps<{
value: any value: any
@ -18,8 +18,8 @@ const { t } = useI18n()
const { setAdditionalValidations, validateInfos, onDataTypeChange, isEdit } = useColumnCreateStoreOrThrow() const { setAdditionalValidations, validateInfos, onDataTypeChange, isEdit } = useColumnCreateStoreOrThrow()
const projectStore = useProject() const baseStore = useBase()
const { tables } = storeToRefs(projectStore) const { tables } = storeToRefs(baseStore)
const { metas } = useMetas() const { metas } = useMetas()
@ -37,7 +37,7 @@ const refTables = computed(() => {
} }
const _refTables = meta.value.columns const _refTables = meta.value.columns
.filter((column) => isLinksOrLTAR(column) && !column.system && column.base_id === meta.value?.base_id) .filter((column) => isLinksOrLTAR(column) && !column.system && column.source_id === meta.value?.source_id)
.map((column) => ({ .map((column) => ({
col: column.colOptions, col: column.colOptions,
column, column,

8
packages/nc-gui/components/smartsheet/column/RollupOptions.vue

@ -2,7 +2,7 @@
import { onMounted } from '@vue/runtime-core' import { onMounted } from '@vue/runtime-core'
import type { ColumnType, LinkToAnotherRecordType, TableType, UITypes } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType, TableType, UITypes } from 'nocodb-sdk'
import { isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk' import { isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { MetaInj, inject, ref, storeToRefs, useColumnCreateStoreOrThrow, useMetas, useProject, useVModel } from '#imports' import { MetaInj, inject, ref, storeToRefs, useColumnCreateStoreOrThrow, useMetas, useBase, useVModel } from '#imports'
const props = defineProps<{ const props = defineProps<{
value: any value: any
@ -16,8 +16,8 @@ const meta = inject(MetaInj, ref())
const { setAdditionalValidations, validateInfos, onDataTypeChange, isEdit } = useColumnCreateStoreOrThrow() const { setAdditionalValidations, validateInfos, onDataTypeChange, isEdit } = useColumnCreateStoreOrThrow()
const projectStore = useProject() const baseStore = useBase()
const { tables } = storeToRefs(projectStore) const { tables } = storeToRefs(baseStore)
const { metas } = useMetas() const { metas } = useMetas()
@ -55,7 +55,7 @@ const refTables = computed(() => {
isLinksOrLTAR(c) && isLinksOrLTAR(c) &&
(c.colOptions as LinkToAnotherRecordType).type !== 'bt' && (c.colOptions as LinkToAnotherRecordType).type !== 'bt' &&
!c.system && !c.system &&
c.base_id === meta.value?.base_id, c.source_id === meta.value?.source_id,
) )
.map((c) => ({ .map((c) => ({
col: c.colOptions, col: c.colOptions,

4
packages/nc-gui/components/smartsheet/column/SelectOptions.vue

@ -31,7 +31,7 @@ const vModel = useVModel(props, 'value', emit)
const { formState, setAdditionalValidations, validateInfos, isMysql } = useColumnCreateStoreOrThrow() const { formState, setAdditionalValidations, validateInfos, isMysql } = useColumnCreateStoreOrThrow()
const { project } = storeToRefs(useProject()) const { base } = storeToRefs(useBase())
const { loadMagic, optionsMagic: _optionsMagic } = useNocoEe() const { loadMagic, optionsMagic: _optionsMagic } = useNocoEe()
@ -140,7 +140,7 @@ const addNewOption = () => {
} }
const optionsMagic = async () => { const optionsMagic = async () => {
await _optionsMagic(project, formState, getNextColor, options.value, renderedOptions.value) await _optionsMagic(base, formState, getNextColor, options.value, renderedOptions.value)
} }
const syncOptions = () => { const syncOptions = () => {

10
packages/nc-gui/components/smartsheet/details/Api.vue

@ -10,7 +10,7 @@ import {
useCopy, useCopy,
useGlobal, useGlobal,
useI18n, useI18n,
useProject, useBase,
useSmartsheetStoreOrThrow, useSmartsheetStoreOrThrow,
useViewData, useViewData,
watch, watch,
@ -18,8 +18,8 @@ import {
const { t } = useI18n() const { t } = useI18n()
const projectStore = useProject() const baseStore = useBase()
const { project } = storeToRefs(projectStore) const { base } = storeToRefs(baseStore)
const { appInfo, token } = useGlobal() const { appInfo, token } = useGlobal()
@ -75,7 +75,7 @@ const selectedLangName = ref(langs[0].name)
const apiUrl = computed( const apiUrl = computed(
() => () =>
new URL( new URL(
`/api/v1/db/data/noco/${project.value.id}/${meta.value?.title}/views/${view.value?.title}`, `/api/v1/data/noco/${base.value.id}/${meta.value?.title}/views/${view.value?.title}`,
(appInfo.value && appInfo.value.ncSiteUrl) || '/', (appInfo.value && appInfo.value.ncSiteUrl) || '/',
).href, ).href,
) )
@ -109,7 +109,7 @@ const api = new Api({
api.dbViewRow.list( api.dbViewRow.list(
"noco", "noco",
${JSON.stringify(project.value.title)}, ${JSON.stringify(base.value.title)},
${JSON.stringify(meta.value?.title)}, ${JSON.stringify(meta.value?.title)},
${JSON.stringify(view.value?.title)}, ${JSON.stringify(queryParams.value, null, 4)}).then(function (data) { ${JSON.stringify(view.value?.title)}, ${JSON.stringify(queryParams.value, null, 4)}).then(function (data) {
console.log(data); console.log(data);

2
packages/nc-gui/components/smartsheet/details/Erd.vue

@ -4,6 +4,6 @@ const { activeTable } = storeToRefs(useTablesStore())
<template> <template>
<div class="flex flex-col p-4" style="height: calc(100vh - (var(--topbar-height) * 2))"> <div class="flex flex-col p-4" style="height: calc(100vh - (var(--topbar-height) * 2))">
<LazyErdView :table="activeTable" :base-id="activeTable?.base_id" :show-all-columns="false" /> <LazyErdView :table="activeTable" :source-id="activeTable?.source_id" :show-all-columns="false" />
</div> </div>
</template> </template>

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

@ -8,9 +8,7 @@ const { loadCommentsAndLogs, commentsAndLogs, saveComment, comment, updateCommen
const commentsWrapperEl = ref<HTMLDivElement>() const commentsWrapperEl = ref<HTMLDivElement>()
onMounted(async () => { await loadCommentsAndLogs()
await loadCommentsAndLogs()
})
const { user } = useGlobal() const { user } = useGlobal()
@ -93,7 +91,7 @@ const value = computed({
}) })
watch( watch(
[commentsAndLogs, tab], commentsAndLogs,
() => { () => {
setTimeout(() => { setTimeout(() => {
if (commentsWrapperEl.value) commentsWrapperEl.value.scrollTop = commentsWrapperEl.value?.scrollHeight if (commentsWrapperEl.value) commentsWrapperEl.value.scrollTop = commentsWrapperEl.value?.scrollHeight
@ -150,24 +148,23 @@ const processedAudit = (log: string) => {
'pb-2': tab !== 'comments', 'pb-2': tab !== 'comments',
}" }"
> >
<div v-if="tab === 'comments'" class="flex flex-col h-full"> <div v-if="tab === 'comments'" ref="commentsWrapperEl" class="flex flex-col h-full">
<div v-if="comments.length === 0" class="flex flex-col my-1 text-center justify-center h-full"> <div v-if="comments.length === 0" class="flex flex-col my-1 text-center justify-center h-full">
<div class="text-center text-3xl text-gray-700"> <div class="text-center text-3xl text-gray-700">
<GeneralIcon icon="commentHere" /> <GeneralIcon icon="commentHere" />
</div> </div>
<div class="font-medium text-center my-6 text-gray-500">{{ $t('activity.startCommenting') }}</div> <div class="font-medium text-center my-6 text-gray-500">{{ $t('activity.startCommenting') }}</div>
</div> </div>
<div v-else ref="commentsWrapperEl" class="flex flex-col h-full py-2 pl-2 pr-1 space-y-2 nc-scrollbar-md"> <div v-else class="flex flex-col h-full py-2 pl-2 pr-1 space-y-2 nc-scrollbar-md">
<div v-for="log of comments" :key="log.id"> <div v-for="log of comments" :key="log.id">
<div class="bg-white rounded-xl group border-1 gap-2 border-gray-200"> <div class="bg-white rounded-xl group border-1 gap-2 border-gray-200">
<div class="flex flex-col p-4 gap-3"> <div class="flex flex-col p-4 gap-3">
<div class="flex justify-between"> <div class="flex justify-between">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<GeneralUserIcon size="base" :name="log.display_name ?? log.user" :email="log.user" /> <GeneralUserIcon size="base" :name="log.display_name ?? log.user" />
<div class="flex flex-col"> <div class="flex flex-col">
<span class="truncate font-bold max-w-42"> <span class="truncate font-bold max-w-42">
{{ log.display_name ?? log.user.split('@')[0] ?? 'Shared base' }} {{ log.display_name ?? log.user.split('@')[0].slice(0, 2) ?? 'Shared source' }}
</span> </span>
<div v-if="log.id !== editLog?.id" class="text-xs text-gray-500"> <div v-if="log.id !== editLog?.id" class="text-xs text-gray-500">
{{ log.created_at !== log.updated_at ? `Edited ${timeAgo(log.updated_at)}` : timeAgo(log.created_at) }} {{ log.created_at !== log.updated_at ? `Edited ${timeAgo(log.updated_at)}` : timeAgo(log.created_at) }}
@ -240,12 +237,12 @@ const processedAudit = (log: string) => {
<div class="bg-white rounded-xl border-1 gap-3 border-gray-200"> <div class="bg-white rounded-xl border-1 gap-3 border-gray-200">
<div class="flex flex-col p-4 gap-3"> <div class="flex flex-col p-4 gap-3">
<div class="flex justify-between"> <div class="flex justify-between">
<div class="flex items-center gap-2"> <div class="flex font-bold items-center gap-2">
<GeneralUserIcon size="base" :name="log.display_name ?? log.user" :email="log.user" /> <GeneralUserIcon size="base" :name="log.display_name ?? log.user" />
<div class="flex flex-col"> <div class="flex flex-col">
<span class="truncate max-w-50 font-bold"> <span class="truncate max-w-50">
{{ log.display_name ?? log.user.split('@')[0].slice(0, 2) ?? 'Shared base' }} {{ log.display_name ?? log.user.split('@')[0].slice(0, 2) ?? 'Shared source' }}
</span> </span>
<div v-if="log.id !== editLog?.id" class="text-xs text-gray-500"> <div v-if="log.id !== editLog?.id" class="text-xs text-gray-500">
{{ timeAgo(log.created_at) }} {{ timeAgo(log.created_at) }}

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

@ -3,6 +3,7 @@ import type { TableType, ViewType } from 'nocodb-sdk'
import { isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk' import { isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import MdiChevronDown from '~icons/mdi/chevron-down' import MdiChevronDown from '~icons/mdi/chevron-down'
import TableIcon from '~icons/nc-icons/table'
import { import {
CellClickHookInj, CellClickHookInj,
@ -14,7 +15,6 @@ import {
ReloadRowDataHookInj, ReloadRowDataHookInj,
computedInject, computedInject,
createEventHook, createEventHook,
iconMap,
inject, inject,
message, message,
provide, provide,
@ -66,9 +66,6 @@ const router = useRouter()
const isPublic = inject(IsPublicInj, ref(false)) const isPublic = inject(IsPublicInj, ref(false))
// to check if a expanded form which is not yet saved exist or not
const isUnsavedFormExist = ref(false)
const { isUIAllowed } = useRoles() const { isUIAllowed } = useRoles()
const reloadTrigger = inject(ReloadRowDataHookInj, createEventHook()) const reloadTrigger = inject(ReloadRowDataHookInj, createEventHook())
@ -154,7 +151,6 @@ const onClose = () => {
const onDuplicateRow = () => { const onDuplicateRow = () => {
duplicatingRowInProgress.value = true duplicatingRowInProgress.value = true
isUnsavedFormExist.value = true
const oldRow = { ...row.value.row } const oldRow = { ...row.value.row }
delete oldRow.ncRecordId delete oldRow.ncRecordId
const newRow = Object.assign( const newRow = Object.assign(
@ -172,38 +168,25 @@ const onDuplicateRow = () => {
}, 500) }, 500)
} }
const save = async () => {
if (isNew.value) {
const data = await _save(rowState.value)
await syncLTARRefs(data)
reloadTrigger?.trigger()
} else {
await _save()
reloadTrigger?.trigger()
}
isUnsavedFormExist.value = false
}
const isPreventChangeModalOpen = ref(false)
const discardPreventModal = () => {
emits('next')
isPreventChangeModalOpen.value = false
}
const onNext = async () => { const onNext = async () => {
if (changedColumns.value.size > 0) { if (changedColumns.value.size > 0) {
isPreventChangeModalOpen.value = true await Modal.confirm({
title: 'Do you want to save the changes?',
okText: 'Save',
cancelText: 'Discard',
onOk: async () => {
await save()
emits('next')
},
onCancel: () => {
emits('next')
},
})
} else { } else {
emits('next') emits('next')
} }
} }
const saveChanges = async () => {
isUnsavedFormExist.value = false
await save()
emits('next')
}
const reloadParentRowHook = inject(ReloadRowDataHookInj, createEventHook()) const reloadParentRowHook = inject(ReloadRowDataHookInj, createEventHook())
// override reload trigger and use it to reload grid and the form itself // override reload trigger and use it to reload grid and the form itself
@ -225,10 +208,6 @@ if (isKanban.value) {
} }
} }
watch(isUnsavedFormExist, () => {
console.log(isUnsavedFormExist.value, 'HEHEH')
})
provide(IsExpandedFormOpenInj, isExpanded) provide(IsExpandedFormOpenInj, isExpanded)
const cellWrapperEl = ref() const cellWrapperEl = ref()
@ -251,6 +230,18 @@ const addNewRow = () => {
isExpanded.value = true isExpanded.value = true
}, 500) }, 500)
} }
const save = async () => {
if (isNew.value) {
const data = await _save(rowState.value)
await syncLTARRefs(data)
reloadTrigger?.trigger()
} else {
await _save()
reloadTrigger?.trigger()
}
}
// attach keyboard listeners to switch between rows // attach keyboard listeners to switch between rows
// using alt + left/right arrow keys // using alt + left/right arrow keys
useActiveKeyupListener( useActiveKeyupListener(
@ -334,9 +325,14 @@ const onConfirmDeleteRowClick = async () => {
showDeleteRowModal.value = false showDeleteRowModal.value = false
await deleteRowById(primaryKey.value) await deleteRowById(primaryKey.value)
message.success('Row deleted') message.success('Row deleted')
// if (!props.lastRow) {
// await onNext()
// } else if (!props.firstRow) {
// emits('prev')
// } else {
// }
reloadTrigger.trigger() reloadTrigger.trigger()
onClose() onClose()
showDeleteRowModal.value = false
} }
watch( watch(
@ -382,7 +378,7 @@ export default {
<div class="h-[85vh] xs:(max-h-full) max-h-215 flex flex-col p-6"> <div class="h-[85vh] xs:(max-h-full) max-h-215 flex flex-col p-6">
<div class="flex h-8 flex-shrink-0 w-full items-center nc-expanded-form-header relative mb-4 justify-between"> <div class="flex h-8 flex-shrink-0 w-full items-center nc-expanded-form-header relative mb-4 justify-between">
<template v-if="!isMobileMode"> <template v-if="!isMobileMode">
<div class="flex gap-3 w-100"> <div class="flex gap-3">
<div class="flex gap-2"> <div class="flex gap-2">
<NcButton <NcButton
v-if="props.showNextPrevIcons" v-if="props.showNextPrevIcons"
@ -403,9 +399,13 @@ export default {
<MdiChevronDown class="text-md" /> <MdiChevronDown class="text-md" />
</NcButton> </NcButton>
</div> </div>
<div v-if="displayValue" class="flex items-center truncate font-bold text-gray-800 text-xl"> <div v-if="displayValue" class="flex items-center truncate max-w-32 font-bold text-gray-800 text-xl">
{{ displayValue }} {{ displayValue }}
</div> </div>
<div class="bg-gray-100 px-2 gap-1 flex my-1 items-center rounded-lg text-gray-800 font-medium">
<TableIcon class="w-6 h-6 text-sm" />
All {{ meta.title }}
</div>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<NcDropdown v-if="!isNew"> <NcDropdown v-if="!isNew">
@ -438,7 +438,7 @@ export default {
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('dataEdit') && !isNew" v-if="isUIAllowed('dataEdit') && !isNew"
v-e="['c:row-expand:delete']" v-e="['c:row-expand:delete']"
class="!text-red-500 !hover:bg-red-50" class="!text-red-500"
@click="!isNew && onDeleteRowClick()" @click="!isNew && onDeleteRowClick()"
> >
<component :is="iconMap.delete" data-testid="nc-expanded-form-delete" class="cursor-pointer nc-delete-row" /> <component :is="iconMap.delete" data-testid="nc-expanded-form-delete" class="cursor-pointer nc-delete-row" />
@ -595,7 +595,7 @@ export default {
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('dataEdit') && !isNew" v-if="isUIAllowed('dataEdit') && !isNew"
v-e="['c:row-expand:delete']" v-e="['c:row-expand:delete']"
class="!text-red-500 !hover:bg-red-50" class="!text-red-500"
@click="!isNew && onDeleteRowClick()" @click="!isNew && onDeleteRowClick()"
> >
<div data-testid="nc-expanded-form-delete"> <div data-testid="nc-expanded-form-delete">
@ -624,7 +624,6 @@ export default {
type="primary" type="primary"
size="medium" size="medium"
class="nc-expand-form-save-btn !xs:(text-base)" class="nc-expand-form-save-btn !xs:(text-base)"
:disabled="changedColumns.size === 0 && !isUnsavedFormExist"
@click="save" @click="save"
> >
<div class="xs:px-1">Save</div> <div class="xs:px-1">Save</div>
@ -643,32 +642,16 @@ export default {
</div> </div>
</NcModal> </NcModal>
<GeneralDeleteModal v-model:visible="showDeleteRowModal" entity-name="Record" :on-delete="() => onConfirmDeleteRowClick()"> <NcModal v-model:visible="showDeleteRowModal" class="!w-[25rem] !xs-">
<template #entity-preview> <div class="">
<span> <div class="prose-xl font-bold self-center">Delete row ?</div>
<div class="flex flex-row items-center py-2.25 px-2.5 bg-gray-50 rounded-lg text-gray-700 mb-4">
<component :is="iconMap.table" class="nc-view-icon" />
<div class="capitalize text-ellipsis overflow-hidden select-none w-full pl-1.75 break-keep whitespace-nowrap">
{{ meta.title }}
</div>
</div>
</span>
</template>
</GeneralDeleteModal>
<!-- Prevent unsaved change modal -->
<NcModal v-model:visible="isPreventChangeModalOpen" size="small">
<template #header>
<div class="flex flex-row items-center gap-x-2">Do you want to save the changes ?</div>
</template>
<div class="mt-2">
<div class="flex flex-row justify-end gap-x-2 mt-6">
<NcButton type="secondary" @click="discardPreventModal">{{ $t('general.quit') }}</NcButton>
<NcButton key="submit" type="primary" label="Rename Table" loading-label="Renaming Table" @click="saveChanges"> <div class="mt-4">Are you sure you want to delete this row?</div>
{{ $t('activity.saveAndQuit') }}
</NcButton>
</div> </div>
<div class="flex flex-row gap-x-2 mt-4 pt-1.5 justify-end pt-4 gap-x-3">
<NcButton v-if="isMobileMode" type="secondary" @click="showDeleteRowModal = false">{{ $t('general.cancel') }} </NcButton>
<NcButton v-e="['a:row-expand:delete']" @click="onConfirmDeleteRowClick">{{ $t('general.confirm') }} </NcButton>
</div> </div>
</NcModal> </NcModal>
</template> </template>

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

@ -1530,7 +1530,7 @@ const handleCellClick = (event: MouseEvent, row: number, col: number) => {
<NcMenuItem <NcMenuItem
v-if="!contextMenuClosing && !contextMenuTarget && data.some((r) => r.rowMeta.selected)" v-if="!contextMenuClosing && !contextMenuTarget && data.some((r) => r.rowMeta.selected)"
v-e="['a:row:delete-bulk']" v-e="['a:row:delete-bulk']"
class="nc-project-menu-item !text-red-600 !hover:bg-red-50" class="nc-base-menu-item !text-red-600 !hover:bg-red-50"
data-testid="nc-delete-row" data-testid="nc-delete-row"
@click="deleteSelectedRows" @click="deleteSelectedRows"
> >
@ -1542,7 +1542,7 @@ const handleCellClick = (event: MouseEvent, row: number, col: number) => {
<!-- <NcMenuItem --> <!-- <NcMenuItem -->
<!-- v-if="contextMenuTarget && selectedRange.isSingleCell()" --> <!-- v-if="contextMenuTarget && selectedRange.isSingleCell()" -->
<!-- v-e="['a:row:insert']" --> <!-- v-e="['a:row:insert']" -->
<!-- class="nc-project-menu-item" --> <!-- class="nc-base-menu-item" -->
<!-- @click="addEmptyRow(contextMenuTarget.row + 1)" --> <!-- @click="addEmptyRow(contextMenuTarget.row + 1)" -->
<!-- > --> <!-- > -->
<!-- <GeneralIcon icon="plus" /> --> <!-- <GeneralIcon icon="plus" /> -->
@ -1553,7 +1553,7 @@ const handleCellClick = (event: MouseEvent, row: number, col: number) => {
<NcMenuItem <NcMenuItem
v-if="contextMenuTarget" v-if="contextMenuTarget"
v-e="['a:row:copy']" v-e="['a:row:copy']"
class="nc-project-menu-item" class="nc-base-menu-item"
data-testid="context-menu-item-copy" data-testid="context-menu-item-copy"
@click="copyValue(contextMenuTarget)" @click="copyValue(contextMenuTarget)"
> >
@ -1570,7 +1570,7 @@ const handleCellClick = (event: MouseEvent, row: number, col: number) => {
(isLinksOrLTAR(fields[contextMenuTarget.col]) || !isVirtualCol(fields[contextMenuTarget.col])) (isLinksOrLTAR(fields[contextMenuTarget.col]) || !isVirtualCol(fields[contextMenuTarget.col]))
" "
v-e="['a:row:clear']" v-e="['a:row:clear']"
class="nc-project-menu-item" class="nc-base-menu-item"
@click="clearCell(contextMenuTarget)" @click="clearCell(contextMenuTarget)"
> >
<GeneralIcon icon="close" /> <GeneralIcon icon="close" />
@ -1581,7 +1581,7 @@ const handleCellClick = (event: MouseEvent, row: number, col: number) => {
<NcMenuItem <NcMenuItem
v-else-if="contextMenuTarget" v-else-if="contextMenuTarget"
v-e="['a:row:clear-range']" v-e="['a:row:clear-range']"
class="nc-project-menu-item" class="nc-base-menu-item"
@click="clearSelectedRangeOfCells()" @click="clearSelectedRangeOfCells()"
> >
<GeneralIcon icon="closeBox" class="text-gray-500" /> <GeneralIcon icon="closeBox" class="text-gray-500" />
@ -1592,7 +1592,7 @@ const handleCellClick = (event: MouseEvent, row: number, col: number) => {
<NcMenuItem <NcMenuItem
v-if="contextMenuTarget && (selectedRange.isSingleCell() || selectedRange.isSingleRow())" v-if="contextMenuTarget && (selectedRange.isSingleCell() || selectedRange.isSingleRow())"
v-e="['a:row:delete']" v-e="['a:row:delete']"
class="nc-project-menu-item !text-red-600 !hover:bg-red-50" class="nc-base-menu-item !text-red-600 !hover:bg-red-50"
@click="confirmDeleteRow(contextMenuTarget.row)" @click="confirmDeleteRow(contextMenuTarget.row)"
> >
<GeneralIcon icon="delete" /> <GeneralIcon icon="delete" />
@ -1602,7 +1602,7 @@ const handleCellClick = (event: MouseEvent, row: number, col: number) => {
<div v-else-if="contextMenuTarget && deleteRangeOfRows"> <div v-else-if="contextMenuTarget && deleteRangeOfRows">
<NcMenuItem <NcMenuItem
v-e="['a:row:delete']" v-e="['a:row:delete']"
class="nc-project-menu-item !text-red-600 !hover:bg-red-50" class="nc-base-menu-item !text-red-600 !hover:bg-red-50"
@click="deleteSelectedRangeOfRows" @click="deleteSelectedRangeOfRows"
> >
<GeneralIcon icon="delete" class="text-gray-500 text-red-600" /> <GeneralIcon icon="delete" class="text-gray-500 text-red-600" />

6
packages/nc-gui/components/smartsheet/header/CellIcon.ts

@ -34,7 +34,7 @@ import {
isYear, isYear,
storeToRefs, storeToRefs,
toRef, toRef,
useProject, useBase,
} from '#imports' } from '#imports'
const renderIcon = (column: ColumnType, abstractType: any) => { const renderIcon = (column: ColumnType, abstractType: any) => {
@ -105,9 +105,9 @@ export default defineComponent({
const column = inject(ColumnInj, columnMeta) const column = inject(ColumnInj, columnMeta)
const { sqlUis } = storeToRefs(useProject()) const { sqlUis } = storeToRefs(useBase())
const sqlUi = ref(column.value?.base_id ? sqlUis.value[column.value?.base_id] : Object.values(sqlUis.value)[0]) const sqlUi = ref(column.value?.source_id ? sqlUis.value[column.value?.source_id] : Object.values(sqlUis.value)[0])
const abstractType = computed(() => column.value && sqlUi.value.getAbstractType(column.value)) const abstractType = computed(() => column.value && sqlUi.value.getAbstractType(column.value))

2
packages/nc-gui/components/smartsheet/header/DeleteColumnModal.vue

@ -20,7 +20,7 @@ const { getMeta } = useMetas()
const { includeM2M } = useGlobal() const { includeM2M } = useGlobal()
const { loadTables } = useProject() const { loadTables } = useBase()
const isLoading = ref(false) const isLoading = ref(false)

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

@ -31,7 +31,7 @@ const vModel = useVModel(props, 'modelValue', emits)
</div> </div>
<div class="w-full h-70vh"> <div class="w-full h-70vh">
<LazyErdView :table="activeTable" :base-id="activeTable?.base_id" /> <LazyErdView :table="activeTable" :source-id="activeTable?.source_id" />
</div> </div>
</a-modal> </a-modal>
</template> </template>

12
packages/nc-gui/components/smartsheet/toolbar/ExportSubActions.vue

@ -14,7 +14,7 @@ import {
storeToRefs, storeToRefs,
useI18n, useI18n,
useNuxtApp, useNuxtApp,
useProject, useBase,
useSmartsheetStoreOrThrow, useSmartsheetStoreOrThrow,
} from '#imports' } from '#imports'
@ -24,8 +24,8 @@ const isPublicView = inject(IsPublicInj, ref(false))
const fields = inject(FieldsInj, ref([])) const fields = inject(FieldsInj, ref([]))
const projectStore = useProject() const baseStore = useBase()
const { project } = storeToRefs(projectStore) const { base } = storeToRefs(baseStore)
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
@ -55,7 +55,7 @@ const exportFile = async (exportType: ExportTypes) => {
} else { } else {
res = await $api.dbViewRow.export( res = await $api.dbViewRow.export(
'noco', 'noco',
project.value?.id as string, base.value?.id as string,
meta.value?.id as string, meta.value?.id as string,
selectedView?.value.id as string, selectedView?.value.id as string,
exportType, exportType,
@ -100,7 +100,7 @@ const exportFile = async (exportType: ExportTypes) => {
<template> <template>
<a-menu-item> <a-menu-item>
<div v-e="['a:download:csv']" class="nc-project-menu-item" @click="exportFile(ExportTypes.CSV)"> <div v-e="['a:download:csv']" class="nc-base-menu-item" @click="exportFile(ExportTypes.CSV)">
<component :is="iconMap.csv" /> <component :is="iconMap.csv" />
<!-- Download as CSV --> <!-- Download as CSV -->
{{ $t('activity.downloadCSV') }} {{ $t('activity.downloadCSV') }}
@ -108,7 +108,7 @@ const exportFile = async (exportType: ExportTypes) => {
</a-menu-item> </a-menu-item>
<a-menu-item> <a-menu-item>
<div v-e="['a:download:excel']" class="nc-project-menu-item" @click="exportFile(ExportTypes.EXCEL)"> <div v-e="['a:download:excel']" class="nc-base-menu-item" @click="exportFile(ExportTypes.EXCEL)">
<component :is="iconMap.excel" /> <component :is="iconMap.excel" />
<!-- Download as XLSX --> <!-- Download as XLSX -->
{{ $t('activity.downloadExcel') }} {{ $t('activity.downloadExcel') }}

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

@ -26,7 +26,7 @@ import {
ref, ref,
storeToRefs, storeToRefs,
toRef, toRef,
useProject, useBase,
} from '#imports' } from '#imports'
import type { Filter } from '#imports' import type { Filter } from '#imports'
import SingleSelect from '~/components/cell/SingleSelect.vue' import SingleSelect from '~/components/cell/SingleSelect.vue'
@ -86,9 +86,9 @@ const checkTypeFunctions = {
type FilterType = keyof typeof checkTypeFunctions type FilterType = keyof typeof checkTypeFunctions
const { sqlUis } = storeToRefs(useProject()) const { sqlUis } = storeToRefs(useBase())
const sqlUi = ref(column.value?.base_id ? sqlUis.value[column.value?.base_id] : Object.values(sqlUis.value)[0]) const sqlUi = ref(column.value?.source_id ? sqlUis.value[column.value?.source_id] : Object.values(sqlUis.value)[0])
const abstractType = computed(() => column.value && sqlUi.value.getAbstractType(column.value)) const abstractType = computed(() => column.value && sqlUi.value.getAbstractType(column.value))

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

@ -14,7 +14,7 @@ import {
storeToRefs, storeToRefs,
useI18n, useI18n,
useNuxtApp, useNuxtApp,
useProject, useBase,
useRoles, useRoles,
useSharedView, useSharedView,
useSmartsheetStoreOrThrow, useSmartsheetStoreOrThrow,
@ -28,7 +28,7 @@ const isPublicView = inject(IsPublicInj, ref(false))
const isView = false const isView = false
const { project } = storeToRefs(useProject()) const { base } = storeToRefs(useBase())
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
@ -66,7 +66,7 @@ const exportFile = async (exportType: ExportTypes) => {
} else { } else {
res = await $api.dbViewRow.export( res = await $api.dbViewRow.export(
'noco', 'noco',
project?.value.id as string, base?.value.id as string,
meta.value?.id as string, meta.value?.id as string,
selectedView.value?.id as string, selectedView.value?.id as string,
exportType, exportType,

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

@ -16,7 +16,7 @@ const router = useRouter()
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const { project } = storeToRefs(useProject()) const { base } = storeToRefs(useBase())
const { isMobileMode } = useGlobal() const { isMobileMode } = useGlobal()
@ -83,7 +83,7 @@ const onDecode = async (codeValue: string) => {
const selectedColumnToScanFor = getColumnToSearchForByBarOrQrCodeColumnId(idOfSelectedColumnToScanFor.value) const selectedColumnToScanFor = getColumnToSearchForByBarOrQrCodeColumnId(idOfSelectedColumnToScanFor.value)
const whereClause = `(${selectedColumnToScanFor?.title},eq,${codeValue})` const whereClause = `(${selectedColumnToScanFor?.title},eq,${codeValue})`
const foundRowsForCode = ( const foundRowsForCode = (
await $api.dbViewRow.list(NOCO, project.value.id!, meta.value!.id!, view.value!.title!, { await $api.dbViewRow.list(NOCO, base.value.id!, meta.value!.id!, view.value!.title!, {
where: whereClause, where: whereClause,
}) })
).list ).list

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

@ -2,7 +2,7 @@
import type { GridType } from 'nocodb-sdk' import type { GridType } from 'nocodb-sdk'
import { ActiveViewInj, IsLockedInj, iconMap, inject, ref, storeToRefs, useMenuCloseOnEsc, useUndoRedo } from '#imports' import { ActiveViewInj, IsLockedInj, iconMap, inject, ref, storeToRefs, useMenuCloseOnEsc, useUndoRedo } from '#imports'
const { isSharedBase } = storeToRefs(useProject()) const { isSharedBase } = storeToRefs(useBase())
const view = inject(ActiveViewInj, ref()) const view = inject(ActiveViewInj, ref())

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

@ -8,14 +8,14 @@ import {
iconMap, iconMap,
isRtlLang, isRtlLang,
message, message,
projectThemeColors, baseThemeColors,
ref, ref,
storeToRefs, storeToRefs,
useCopy, useCopy,
useDashboard, useDashboard,
useI18n, useI18n,
useNuxtApp, useNuxtApp,
useProject, useBase,
useRoles, useRoles,
useSmartsheetStoreOrThrow, useSmartsheetStoreOrThrow,
watch, watch,
@ -34,7 +34,7 @@ const { dashboardUrl } = useDashboard()
const { isUIAllowed } = useRoles() const { isUIAllowed } = useRoles()
const { isSharedBase } = storeToRefs(useProject()) const { isSharedBase } = storeToRefs(useBase())
const { isMobileMode } = useGlobal() const { isMobileMode } = useGlobal()
@ -390,7 +390,7 @@ watch(shared, () => {
data-testid="nc-modal-share-view__theme-picker" data-testid="nc-modal-share-view__theme-picker"
class="!p-0" class="!p-0"
:model-value="shared.meta.theme?.primaryColor" :model-value="shared.meta.theme?.primaryColor"
:colors="projectThemeColors" :colors="baseThemeColors"
:row-size="9" :row-size="9"
:advanced="false" :advanced="false"
@input="onChangeTheme" @input="onChangeTheme"

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

@ -16,7 +16,7 @@ import {
useI18n, useI18n,
useMenuCloseOnEsc, useMenuCloseOnEsc,
useNuxtApp, useNuxtApp,
useProject, useBase,
useRoles, useRoles,
useSmartsheetStoreOrThrow, useSmartsheetStoreOrThrow,
} from '#imports' } from '#imports'
@ -58,11 +58,11 @@ const quickImportDialogs: Record<(typeof quickImportDialogTypes)[number], Ref<bo
const { isUIAllowed } = useRoles() const { isUIAllowed } = useRoles()
useProject() useBase()
const meta = inject(MetaInj, ref()) const meta = inject(MetaInj, ref())
const currentBaseId = computed(() => meta.value?.base_id) const currentBaseId = computed(() => meta.value?.source_id)
/* /*
const Icon = computed(() => { const Icon = computed(() => {
@ -119,7 +119,7 @@ useMenuCloseOnEsc(open)
<template v-if="isUIAllowed('csvTableImport') && !isView && !isPublicView && !isSqlView"> <template v-if="isUIAllowed('csvTableImport') && !isView && !isPublicView && !isSqlView">
<a-sub-menu key="upload"> <a-sub-menu key="upload">
<template #title> <template #title>
<div v-e="['c:navdraw:preview-as']" class="nc-project-menu-item group"> <div v-e="['c:navdraw:preview-as']" class="nc-base-menu-item group">
<UploadIcon class="w-4 h-4" /> <UploadIcon class="w-4 h-4" />
{{ $t('general.upload') }} {{ $t('general.upload') }}
<div class="flex-1" /> <div class="flex-1" />
@ -133,7 +133,7 @@ useMenuCloseOnEsc(open)
<a-menu-item v-if="isUIAllowed(`${type}TableImport`) && !isView && !isPublicView" :key="type"> <a-menu-item v-if="isUIAllowed(`${type}TableImport`) && !isView && !isPublicView" :key="type">
<div <div
v-e="[`a:upload:${type}`]" v-e="[`a:upload:${type}`]"
class="nc-project-menu-item" class="nc-base-menu-item"
:class="{ disabled: isLocked }" :class="{ disabled: isLocked }"
@click="!isLocked ? (dialog.value = true) : {}" @click="!isLocked ? (dialog.value = true) : {}"
> >
@ -146,7 +146,7 @@ useMenuCloseOnEsc(open)
</template> </template>
<a-sub-menu key="download"> <a-sub-menu key="download">
<template #title> <template #title>
<div v-e="['c:download']" class="nc-project-menu-item group"> <div v-e="['c:download']" class="nc-base-menu-item group">
<DownloadIcon class="w-4 h-4" /> <DownloadIcon class="w-4 h-4" />
{{ $t('general.download') }} {{ $t('general.download') }}
<div class="flex-1" /> <div class="flex-1" />
@ -166,7 +166,7 @@ useMenuCloseOnEsc(open)
class="scrollbar-thin-dull max-h-90vh overflow-auto !py-0" class="scrollbar-thin-dull max-h-90vh overflow-auto !py-0"
> >
<template #title> <template #title>
<div v-e="['c:navdraw:preview-as']" class="nc-project-menu-item group px-0 !py-0"> <div v-e="['c:navdraw:preview-as']" class="nc-base-menu-item group px-0 !py-0">
<LazySmartsheetToolbarLockType hide-tick :type="lockType" /> <LazySmartsheetToolbarLockType hide-tick :type="lockType" />
<component :is="iconMap.arrowRight" /> <component :is="iconMap.arrowRight" />
@ -197,7 +197,7 @@ useMenuCloseOnEsc(open)
:key="tp" :key="tp"
v-model="quickImportDialogs[tp].value" v-model="quickImportDialogs[tp].value"
:import-type="tp" :import-type="tp"
:base-id="currentBaseId" :source-id="currentBaseId"
:import-data-only="true" :import-data-only="true"
/> />
</template> </template>

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

@ -5,7 +5,7 @@ const { isMobileMode } = useGlobal()
const { openedViewsTab, activeView } = storeToRefs(useViewsStore()) const { openedViewsTab, activeView } = storeToRefs(useViewsStore())
const { project } = storeToRefs(useProject()) const { base } = storeToRefs(useBase())
const { activeTable } = storeToRefs(useTablesStore()) const { activeTable } = storeToRefs(useTablesStore())
@ -25,11 +25,11 @@ const { isLeftSidebarOpen } = storeToRefs(useSidebarStore())
<template v-if="!isMobileMode"> <template v-if="!isMobileMode">
<NcTooltip class="ml-0.75 max-w-1/4"> <NcTooltip class="ml-0.75 max-w-1/4">
<template #title> <template #title>
{{ project?.title }} {{ base?.title }}
</template> </template>
<div class="flex flex-row items-center gap-x-1.5"> <div class="flex flex-row items-center gap-x-1.5">
<GeneralProjectIcon <GeneralProjectIcon
:meta="{ type: project?.type }" :meta="{ type: base?.type }"
class="!grayscale" class="!grayscale"
:style="{ :style="{
filter: 'grayscale(100%) brightness(115%)', filter: 'grayscale(100%) brightness(115%)',
@ -37,7 +37,7 @@ const { isLeftSidebarOpen } = storeToRefs(useSidebarStore())
/> />
<div class="hidden !2xl:(flex truncate ml-1)"> <div class="hidden !2xl:(flex truncate ml-1)">
<span class="truncate text-gray-700"> <span class="truncate text-gray-700">
{{ project?.title }} {{ base?.title }}
</span> </span>
</div> </div>
</div> </div>

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

@ -89,8 +89,8 @@ const onDrop = async (event: DragEvent) => {
const data = JSON.parse(event.dataTransfer!.getData('text/json')) const data = JSON.parse(event.dataTransfer!.getData('text/json'))
// Do something with the received data // Do something with the received data
// if dragged item is not from the same base, return // if dragged item is not from the same source, return
if (data.baseId !== meta.value?.base_id) return if (data.sourceId !== meta.value?.source_id) return
// if dragged item or opened view is not a table, return // if dragged item or opened view is not a table, return
if (data.type !== 'table' || meta.value?.type !== 'table') return if (data.type !== 'table' || meta.value?.type !== 'table') return

18
packages/nc-gui/components/tabs/auth/ApiTokenManagement.vue

@ -9,7 +9,7 @@ import {
useCopy, useCopy,
useI18n, useI18n,
useNuxtApp, useNuxtApp,
useProject, useBase,
} from '#imports' } from '#imports'
interface ApiToken extends ApiTokenType { interface ApiToken extends ApiTokenType {
@ -20,8 +20,8 @@ const { t } = useI18n()
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
const projectStore = useProject() const baseStore = useBase()
const { project } = storeToRefs(projectStore) const { base } = storeToRefs(baseStore)
const { copy } = useCopy() const { copy } = useCopy()
@ -34,9 +34,9 @@ const showDeleteTokenModal = ref(false)
const selectedTokenData = ref<ApiToken>({}) const selectedTokenData = ref<ApiToken>({})
const loadApiTokens = async () => { const loadApiTokens = async () => {
if (!project.value?.id) return if (!base.value?.id) return
tokensInfo.value = (await $api.apiToken.list(project.value.id)).list tokensInfo.value = (await $api.apiToken.list(base.value.id)).list
} }
const openNewTokenModal = () => { const openNewTokenModal = () => {
@ -59,9 +59,9 @@ const copyToken = async (token: string | undefined) => {
const generateToken = async () => { const generateToken = async () => {
try { try {
if (!project.value?.id) return if (!base.value?.id) return
await $api.apiToken.create(project.value.id, selectedTokenData.value) await $api.apiToken.create(base.value.id, selectedTokenData.value)
showNewTokenModal.value = false showNewTokenModal.value = false
// Token generated successfully // Token generated successfully
message.success(t('msg.success.tokenGenerated')) message.success(t('msg.success.tokenGenerated'))
@ -76,9 +76,9 @@ const generateToken = async () => {
const deleteToken = async () => { const deleteToken = async () => {
try { try {
if (!project.value?.id || !selectedTokenData.value.token) return if (!base.value?.id || !selectedTokenData.value.token) return
await $api.apiToken.delete(project.value.id, selectedTokenData.value.token) await $api.apiToken.delete(base.value.id, selectedTokenData.value.token)
// Token deleted successfully // Token deleted successfully
message.success(t('msg.success.tokenDeleted')) message.success(t('msg.success.tokenDeleted'))

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

@ -13,7 +13,7 @@ import {
useDashboard, useDashboard,
useI18n, useI18n,
useNuxtApp, useNuxtApp,
useProject, useBase,
useRoles, useRoles,
watchDebounced, watchDebounced,
} from '#imports' } from '#imports'
@ -25,7 +25,7 @@ const { $e } = useNuxtApp()
const { api } = useApi() const { api } = useApi()
const { project } = storeToRefs(useProject()) const { base } = storeToRefs(useBase())
const { copy } = useCopy() const { copy } = useCopy()
@ -53,10 +53,10 @@ const searchText = ref<string>('')
const loadUsers = async (page = currentPage.value, limit = currentLimit.value) => { const loadUsers = async (page = currentPage.value, limit = currentLimit.value) => {
try { try {
if (!project.value?.id) return if (!base.value?.id) return
// TODO: Types of api is not correct // TODO: Types of api is not correct
const response: any = await api.auth.projectUserList(project.value?.id, { const response: any = await api.auth.baseUserList(base.value?.id, {
query: { query: {
limit, limit,
offset: (page - 1) * limit, offset: (page - 1) * limit,
@ -75,16 +75,16 @@ const loadUsers = async (page = currentPage.value, limit = currentLimit.value) =
const inviteUser = async (user: User) => { const inviteUser = async (user: User) => {
try { try {
if (!project.value?.id) return if (!base.value?.id) return
if (!user.roles) { if (!user.roles) {
// mark it as editor by default // mark it as editor by default
user.roles = 'editor' user.roles = 'editor'
} }
await api.auth.projectUserAdd(project.value.id, user as ProjectUserReqType) await api.auth.baseUserAdd(base.value.id, user as ProjectUserReqType)
// Successfully added user to project // Successfully added user to base
message.success(t('msg.success.userAddedToProject')) message.success(t('msg.success.userAddedToProject'))
await loadUsers() await loadUsers()
} catch (e: any) { } catch (e: any) {
@ -96,11 +96,11 @@ const inviteUser = async (user: User) => {
const deleteUser = async () => { const deleteUser = async () => {
try { try {
if (!project.value?.id || !selectedUser.value?.id) return if (!base.value?.id || !selectedUser.value?.id) return
await api.auth.projectUserRemove(project.value.id, selectedUser.value.id) await api.auth.baseUserRemove(base.value.id, selectedUser.value.id)
// Successfully deleted user from project // Successfully deleted user from base
message.success(t('msg.success.userDeletedFromProject')) message.success(t('msg.success.userDeletedFromProject'))
await loadUsers() await loadUsers()
@ -131,10 +131,10 @@ const onDelete = (user: User) => {
} }
const resendInvite = async (user: User) => { const resendInvite = async (user: User) => {
if (!project.value?.id) return if (!base.value?.id) return
try { try {
await api.auth.projectUserResendInvite(project.value.id, user.id) await api.auth.baseUserResendInvite(base.value.id, user.id)
// Invite email sent successfully // Invite email sent successfully
message.success(t('msg.success.inviteEmailSent')) message.success(t('msg.success.inviteEmailSent'))
@ -209,7 +209,7 @@ const isSuperAdmin = (user: { main_roles?: string }) => {
> >
<div class="flex flex-col h-full"> <div class="flex flex-col h-full">
<div class="flex flex-row justify-center mt-2 text-center w-full text-base"> <div class="flex flex-row justify-center mt-2 text-center w-full text-base">
This action will remove this user from this project This action will remove this user from this base
</div> </div>
<div class="flex mt-6 justify-end space-x-2"> <div class="flex mt-6 justify-end space-x-2">
<a-button class="!rounded-md" @click="showUserDeleteModal = false"> {{ $t('general.cancel') }}</a-button> <a-button class="!rounded-md" @click="showUserDeleteModal = false"> {{ $t('general.cancel') }}</a-button>
@ -287,7 +287,7 @@ const isSuperAdmin = (user: { main_roles?: string }) => {
</div> </div>
<div class="flex w-1/6 flex-wrap justify-end"> <div class="flex w-1/6 flex-wrap justify-end">
<template v-if="!isSuperAdmin(user)"> <template v-if="!isSuperAdmin(user)">
<a-tooltip v-if="user.project_id" placement="bottom"> <a-tooltip v-if="user.base_id" placement="bottom">
<template #title> <template #title>
<span>{{ $t('activity.editUser') }}</span> <span>{{ $t('activity.editUser') }}</span>
</template> </template>
@ -299,8 +299,8 @@ const isSuperAdmin = (user: { main_roles?: string }) => {
</a-button> </a-button>
</a-tooltip> </a-tooltip>
<!-- Add user to project --> <!-- Add user to base -->
<a-tooltip v-if="!user.project_id" placement="bottom"> <a-tooltip v-if="!user.base_id" placement="bottom">
<template #title> <template #title>
<span>{{ $t('activity.addUserToProject') }}</span> <span>{{ $t('activity.addUserToProject') }}</span>
</template> </template>
@ -312,7 +312,7 @@ const isSuperAdmin = (user: { main_roles?: string }) => {
</a-button> </a-button>
</a-tooltip> </a-tooltip>
<!-- Remove user from the project --> <!-- Remove user from the base -->
<a-tooltip v-else placement="bottom"> <a-tooltip v-else placement="bottom">
<template #title> <template #title>
<span>{{ $t('activity.deleteUser') }}</span> <span>{{ $t('activity.deleteUser') }}</span>

50
packages/nc-gui/components/tabs/auth/user-management/ShareBase.vue

@ -9,7 +9,7 @@ import {
useDashboard, useDashboard,
useI18n, useI18n,
useNuxtApp, useNuxtApp,
useProject, useBase,
} from '#imports' } from '#imports'
interface ShareBase { interface ShareBase {
@ -29,23 +29,23 @@ const { dashboardUrl } = useDashboard()
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
const base = ref<null | ShareBase>(null) const sharedBase = ref<null | ShareBase>(null)
const showEditBaseDropdown = ref(false) const showEditBaseDropdown = ref(false)
const { project } = storeToRefs(useProject()) const { base } = storeToRefs(useBase())
const { copy } = useCopy() const { copy } = useCopy()
const url = computed(() => (base.value && base.value.uuid ? `${dashboardUrl.value}#/base/${base.value.uuid}` : null)) const url = computed(() => (sharedBase.value && sharedBase.value.uuid ? `${dashboardUrl.value}#/base/${sharedBase.value.uuid}` : null))
const loadBase = async () => { const loadBase = async () => {
try { try {
if (!project.value.id) return if (!base.value.id) return
const res = await $api.project.sharedBaseGet(project.value.id) const res = await $api.base.sharedBaseGet(base.value.id)
base.value = { sharedBase.value = {
uuid: res.uuid, uuid: res.uuid,
url: res.url, url: res.url,
role: res.roles, role: res.roles,
@ -57,14 +57,14 @@ const loadBase = async () => {
const createShareBase = async (role = ShareBaseRole.Viewer) => { const createShareBase = async (role = ShareBaseRole.Viewer) => {
try { try {
if (!project.value.id) return if (!base.value.id) return
const res = await $api.project.sharedBaseUpdate(project.value.id, { const res = await $api.base.sharedBaseUpdate(base.value.id, {
roles: role, roles: role,
}) })
base.value = res ?? {} sharedBase.value = res ?? {}
base.value!.role = role sharedBase.value!.role = role
} catch (e: any) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} }
@ -74,10 +74,10 @@ const createShareBase = async (role = ShareBaseRole.Viewer) => {
const disableSharedBase = async () => { const disableSharedBase = async () => {
try { try {
if (!project.value.id) return if (!base.value.id) return
await $api.project.sharedBaseDisable(project.value.id) await $api.base.sharedBaseDisable(base.value.id)
base.value = null sharedBase.value = null
} catch (e: any) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} }
@ -87,15 +87,15 @@ const disableSharedBase = async () => {
const recreate = async () => { const recreate = async () => {
try { try {
if (!project.value.id) return if (!base.value.id) return
const sharedBase = await $api.project.sharedBaseCreate(project.value.id, { const sharedBase = await $api.base.sharedBaseCreate(base.value.id, {
roles: base.value?.role || ShareBaseRole.Viewer, roles: sharedBase.value?.role || ShareBaseRole.Viewer,
}) })
const newBase = sharedBase || {} const newBase = sharedBase || {}
base.value = { ...newBase, role: base.value?.role } sharedBase.value = { ...newBase, role: sharedBase.value?.role }
} catch (e: any) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} }
@ -145,7 +145,7 @@ style="background: transparent; border: 1px solid #ddd"></iframe>`)
} }
onMounted(() => { onMounted(() => {
if (!base.value) { if (!sharedBase.value) {
loadBase() loadBase()
} }
}) })
@ -153,14 +153,14 @@ onMounted(() => {
<template> <template>
<div class="flex flex-col gap-2 w-full" data-testid="nc-share-base-sub-modal"> <div class="flex flex-col gap-2 w-full" data-testid="nc-share-base-sub-modal">
<!-- Generate publicly shareable readonly base --> <!-- Generate publicly shareable readonly source -->
<div class="flex text-xs text-gray-500 justify-start ml-1">{{ $t('msg.info.generatePublicShareableReadonlyBase') }}</div> <div class="flex text-xs text-gray-500 justify-start ml-1">{{ $t('msg.info.generatePublicShareableReadonlyBase') }}</div>
<div class="flex flex-row justify-between mx-1"> <div class="flex flex-row justify-between mx-1">
<a-dropdown v-model="showEditBaseDropdown" class="flex" overlay-class-name="nc-dropdown-shared-base-toggle"> <a-dropdown v-model="showEditBaseDropdown" class="flex" overlay-class-name="nc-dropdown-shared-base-toggle">
<a-button> <a-button>
<div class="flex flex-row rounded-md items-center space-x-2 nc-disable-shared-base"> <div class="flex flex-row rounded-md items-center space-x-2 nc-disable-shared-base">
<div v-if="base?.uuid">{{ $t('activity.shareBase.enable') }}</div> <div v-if="sharedBase?.uuid">{{ $t('activity.shareBase.enable') }}</div>
<div v-else>{{ $t('activity.shareBase.disable') }}</div> <div v-else>{{ $t('activity.shareBase.disable') }}</div>
<IcRoundKeyboardArrowDown class="h-[1rem]" /> <IcRoundKeyboardArrowDown class="h-[1rem]" />
</div> </div>
@ -169,7 +169,7 @@ onMounted(() => {
<template #overlay> <template #overlay>
<a-menu> <a-menu>
<a-menu-item> <a-menu-item>
<div v-if="base?.uuid" class="py-3" @click="disableSharedBase">{{ $t('activity.shareBase.disable') }}</div> <div v-if="sharedBase?.uuid" class="py-3" @click="disableSharedBase">{{ $t('activity.shareBase.disable') }}</div>
<div v-else class="py-3" @click="createShareBase(ShareBaseRole.Viewer)">{{ $t('activity.shareBase.enable') }}</div> <div v-else class="py-3" @click="createShareBase(ShareBaseRole.Viewer)">{{ $t('activity.shareBase.enable') }}</div>
</a-menu-item> </a-menu-item>
</a-menu> </a-menu>
@ -177,8 +177,8 @@ onMounted(() => {
</a-dropdown> </a-dropdown>
<a-select <a-select
v-if="base?.uuid" v-if="sharedBase?.uuid"
v-model:value="base.role" v-model:value="sharedBase.role"
class="flex nc-shared-base-role" class="flex nc-shared-base-role"
dropdown-class-name="nc-dropdown-share-base-role" dropdown-class-name="nc-dropdown-share-base-role"
> >
@ -201,7 +201,7 @@ onMounted(() => {
</a-select-option> </a-select-option>
</a-select> </a-select>
</div> </div>
<div v-if="base?.uuid" class="flex flex-row mt-2 bg-red-50 py-4 mx-1 px-2 items-center rounded-sm w-full justify-between"> <div v-if="sharedBase?.uuid" class="flex flex-row mt-2 bg-red-50 py-4 mx-1 px-2 items-center rounded-sm w-full justify-between">
<span class="flex text-xs overflow-x-hidden overflow-ellipsis text-gray-700 pl-2 nc-url">{{ url }}</span> <span class="flex text-xs overflow-x-hidden overflow-ellipsis text-gray-700 pl-2 nc-url">{{ url }}</span>
<div class="flex border-l-1 pt-1 pl-1"> <div class="flex border-l-1 pt-1 pl-1">

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

Loading…
Cancel
Save