Browse Source

Merge pull request #6539 from nocodb/nc-fix/removed-no-views

Removed no views
pull/6542/head
Muhammed Mustafa 1 year ago committed by GitHub
parent
commit
ef7fffcb4b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      packages/nc-gui/assets/nc-icons/add-data-source.svg
  2. 8
      packages/nc-gui/assets/nc-icons/users.svg
  3. 1
      packages/nc-gui/components/dashboard/Sidebar/Header.vue
  4. 1
      packages/nc-gui/components/dashboard/Sidebar/TopSection.vue
  5. 16
      packages/nc-gui/components/dashboard/Sidebar/UserInfo.vue
  6. 4
      packages/nc-gui/components/dashboard/TreeView/BaseOptions.vue
  7. 40
      packages/nc-gui/components/dashboard/TreeView/CreateViewBtn.vue
  8. 73
      packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
  9. 1
      packages/nc-gui/components/dashboard/TreeView/TableList.vue
  10. 17
      packages/nc-gui/components/dashboard/TreeView/TableNode.vue
  11. 20
      packages/nc-gui/components/dashboard/TreeView/ViewsList.vue
  12. 28
      packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue
  13. 4
      packages/nc-gui/components/dashboard/TreeView/index.vue
  14. 14
      packages/nc-gui/components/dashboard/settings/DataSources.vue
  15. 7
      packages/nc-gui/components/dashboard/settings/data-sources/CreateBase.vue
  16. 4
      packages/nc-gui/components/dlg/ProjectDuplicate.vue
  17. 1
      packages/nc-gui/components/dlg/TableCreate.vue
  18. 4
      packages/nc-gui/components/dlg/TableDuplicate.vue
  19. 39
      packages/nc-gui/components/dlg/ViewCreate.vue
  20. 9
      packages/nc-gui/components/dlg/share-and-collaborate/ShareBase.vue
  21. 16
      packages/nc-gui/components/dlg/share-and-collaborate/SharePage.vue
  22. 4
      packages/nc-gui/components/dlg/share-and-collaborate/View.vue
  23. 1
      packages/nc-gui/components/general/OpenLeftSidebarBtn.vue
  24. 4
      packages/nc-gui/components/general/ProjectIcon.vue
  25. 1
      packages/nc-gui/components/general/ShareProject.vue
  26. 15
      packages/nc-gui/components/nc/Pagination.vue
  27. 4
      packages/nc-gui/components/nc/Tabs.vue
  28. 2
      packages/nc-gui/components/project/AccessSettings.vue
  29. 61
      packages/nc-gui/components/project/AllTables.vue
  30. 48
      packages/nc-gui/components/project/View.vue
  31. 9
      packages/nc-gui/components/roles/Selector.vue
  32. 34
      packages/nc-gui/components/smartsheet/Details.vue
  33. 1
      packages/nc-gui/components/smartsheet/Pagination.vue
  34. 16
      packages/nc-gui/components/smartsheet/Topbar.vue
  35. 13
      packages/nc-gui/components/smartsheet/expanded-form/Comments.vue
  36. 34
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  37. 16
      packages/nc-gui/components/smartsheet/grid/Table.vue
  38. 10
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue
  39. 1
      packages/nc-gui/components/smartsheet/toolbar/CreateSort.vue
  40. 4
      packages/nc-gui/components/smartsheet/toolbar/ExportSubActions.vue
  41. 9
      packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue
  42. 12
      packages/nc-gui/components/smartsheet/toolbar/FilterInput.vue
  43. 4
      packages/nc-gui/components/smartsheet/toolbar/GroupByMenu.vue
  44. 9
      packages/nc-gui/components/smartsheet/toolbar/SearchData.vue
  45. 11
      packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue
  46. 4
      packages/nc-gui/components/smartsheet/toolbar/ViewActions.vue
  47. 74
      packages/nc-gui/components/smartsheet/toolbar/ViewInfo.vue
  48. 2
      packages/nc-gui/components/smartsheet/topbar/SelectMode.vue
  49. 1
      packages/nc-gui/components/virtual-cell/Links.vue
  50. 1
      packages/nc-gui/components/virtual-cell/components/ItemChip.vue
  51. 2
      packages/nc-gui/components/virtual-cell/components/ListChildItems.vue
  52. 1
      packages/nc-gui/components/virtual-cell/components/ListItem.vue
  53. 6
      packages/nc-gui/components/virtual-cell/components/ListItems.vue
  54. 8
      packages/nc-gui/components/workspace/Billing.vue
  55. 1
      packages/nc-gui/components/workspace/CreateProjectBtn.vue
  56. 1
      packages/nc-gui/components/workspace/CreateProjectDlg.vue
  57. 9
      packages/nc-gui/components/workspace/Settings.vue
  58. 6
      packages/nc-gui/composables/useLTARStore.ts
  59. 1
      packages/nc-gui/composables/useViewData.ts
  60. 9
      packages/nc-gui/lang/en.json
  61. 4
      packages/nc-gui/pages/signin.vue
  62. 6
      packages/nc-gui/plugins/tele.ts
  63. 3
      packages/nc-gui/store/config.ts
  64. 2
      packages/nc-gui/store/projects.ts
  65. 15
      packages/nc-gui/store/tables.ts
  66. 4
      packages/nc-gui/utils/iconUtils.ts
  67. 9
      packages/nc-gui/windi.config.ts
  68. 47
      packages/nocodb/src/models/Model.ts
  69. 10
      packages/nocodb/src/models/View.ts
  70. 14
      packages/nocodb/tests/unit/factory/table.ts
  71. 16
      packages/nocodb/tests/unit/factory/view.ts
  72. 1
      packages/nocodb/tests/unit/rest/tests/attachment.test.ts
  73. 81
      packages/nocodb/tests/unit/rest/tests/table.test.ts
  74. 8
      tests/playwright/pages/Dashboard/ProjectView/TablesViewPage.ts
  75. 2
      tests/playwright/pages/Dashboard/common/WorkspaceMenu/index.ts
  76. 2
      tests/playwright/setup/knexHelper.ts

6
packages/nc-gui/assets/nc-icons/add-data-source.svg

@ -0,0 +1,6 @@
<svg width="100%" height="100%" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 5.3335C11.3137 5.3335 14 4.43807 14 3.3335C14 2.22893 11.3137 1.3335 8 1.3335C4.68629 1.3335 2 2.22893 2 3.3335C2 4.43807 4.68629 5.3335 8 5.3335Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 3.3335V12.6668C2 13.7735 5 14.5 7.5 14.5M14 3.3335V7.5M2 8.16683C2 9.2735 5.5 10 8 10" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.3333 10.3334V14.3334" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.3333 12.3334H10.3333" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 828 B

8
packages/nc-gui/assets/nc-icons/users.svg

@ -1,6 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" 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">
<path d="M15.3333 14V12.6667C15.3328 12.0758 15.1362 11.5019 14.7742 11.0349C14.4122 10.5679 13.9053 10.2344 13.3333 10.0867" stroke="#1F293A" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/> <path d="M15.3333 14V12.6667C15.3328 12.0758 15.1362 11.5019 14.7742 11.0349C14.4122 10.5679 13.9053 10.2344 13.3333 10.0867" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.3334 14V12.6667C11.3334 11.9594 11.0525 11.2811 10.5524 10.781C10.0523 10.281 9.37399 10 8.66675 10H3.33341C2.62617 10 1.94789 10.281 1.4478 10.781C0.9477 11.2811 0.666748 11.9594 0.666748 12.6667V14" stroke="#1F293A" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/> <path d="M11.3334 14V12.6667C11.3334 11.9594 11.0525 11.2811 10.5524 10.781C10.0523 10.281 9.37399 10 8.66675 10H3.33341C2.62617 10 1.94789 10.281 1.4478 10.781C0.9477 11.2811 0.666748 11.9594 0.666748 12.6667V14" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.6667 2.08667C11.2404 2.23354 11.7488 2.56714 12.1118 3.03488C12.4749 3.50262 12.672 4.07789 12.672 4.67C12.672 5.26212 12.4749 5.83739 12.1118 6.30513C11.7488 6.77287 11.2404 7.10647 10.6667 7.25334" stroke="#4A5268" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/> <path d="M10.6667 2.08667C11.2404 2.23354 11.7488 2.56714 12.1118 3.03488C12.4749 3.50262 12.672 4.07789 12.672 4.67C12.672 5.26212 12.4749 5.83739 12.1118 6.30513C11.7488 6.77287 11.2404 7.10647 10.6667 7.25334" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.99992 7.33333C7.47268 7.33333 8.66659 6.13943 8.66659 4.66667C8.66659 3.19391 7.47268 2 5.99992 2C4.52716 2 3.33325 3.19391 3.33325 4.66667C3.33325 6.13943 4.52716 7.33333 5.99992 7.33333Z" stroke="#4A5268" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/> <path d="M5.99992 7.33333C7.47268 7.33333 8.66659 6.13943 8.66659 4.66667C8.66659 3.19391 7.47268 2 5.99992 2C4.52716 2 3.33325 3.19391 3.33325 4.66667C3.33325 6.13943 4.52716 7.33333 5.99992 7.33333Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

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

@ -38,6 +38,7 @@ const showSidebarBtn = computed(() => !(isMobileMode.value && !activeViewTitleOr
</template> </template>
<NcButton <NcButton
v-if="showSidebarBtn" v-if="showSidebarBtn"
v-e="['c:leftSidebar:hideToggle']"
:type="isMobileMode ? 'secondary' : 'text'" :type="isMobileMode ? 'secondary' : 'text'"
:size="isMobileMode ? 'medium' : 'small'" :size="isMobileMode ? 'medium' : 'small'"
class="nc-sidebar-left-toggle-icon !text-gray-700 !hover:text-gray-800 !xs:(h-10.5 max-h-10.5 max-w-10.5) !md:(hover:bg-gray-200)" class="nc-sidebar-left-toggle-icon !text-gray-700 !hover:text-gray-800 !xs:(h-10.5 max-h-10.5 max-w-10.5) !md:(hover:bg-gray-200)"

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

@ -48,6 +48,7 @@ const navigateToSettings = () => {
<NcButton <NcButton
v-if="isUIAllowed('workspaceSettings')" v-if="isUIAllowed('workspaceSettings')"
v-e="['c:team:settings']"
type="text" type="text"
size="small" size="small"
class="nc-sidebar-top-button !xs:hidden" class="nc-sidebar-top-button !xs:hidden"

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

@ -81,14 +81,14 @@ onMounted(() => {
</div> </div>
<template #overlay> <template #overlay>
<NcMenu data-testid="nc-sidebar-userinfo"> <NcMenu data-testid="nc-sidebar-userinfo">
<NcMenuItem data-testid="nc-sidebar-user-logout" @click="logout"> <NcMenuItem v-e="['c:user:logout']" data-testid="nc-sidebar-user-logout" @click="logout">
<GeneralLoader v-if="isLoggingOut" class="!ml-0.5 !mr-0.5 !max-h-4.5 !-mt-0.5" /> <GeneralLoader v-if="isLoggingOut" class="!ml-0.5 !mr-0.5 !max-h-4.5 !-mt-0.5" />
<GeneralIcon v-else icon="signout" class="menu-icon" /> <GeneralIcon v-else icon="signout" class="menu-icon" />
<span class="menu-btn"> {{ $t('general.logout') }}</span> <span class="menu-btn"> {{ $t('general.logout') }}</span>
</NcMenuItem> </NcMenuItem>
<template v-if="!isMobileMode"> <template v-if="!isMobileMode">
<NcDivider /> <NcDivider />
<a href="https://docs.nocodb.com" target="_blank" class="!underline-transparent"> <a v-e="['c:nocodb:docs-open']" href="https://docs.nocodb.com" target="_blank" class="!underline-transparent">
<NcMenuItem> <NcMenuItem>
<GeneralIcon icon="help" class="menu-icon mt-0.5" /> <GeneralIcon icon="help" class="menu-icon mt-0.5" />
<span class="menu-btn"> {{ $t('title.helpCenter') }} </span> <span class="menu-btn"> {{ $t('title.helpCenter') }} </span>
@ -96,19 +96,19 @@ onMounted(() => {
</a> </a>
</template> </template>
<NcDivider /> <NcDivider />
<a href="https://discord.gg/5RgZmkW" target="_blank" class="!underline-transparent"> <a v-e="['c:nocodb:discord']" href="https://discord.gg/5RgZmkW" target="_blank" class="!underline-transparent">
<NcMenuItem class="social-icon-wrapper"> <NcMenuItem class="social-icon-wrapper">
<GeneralIcon class="social-icon" icon="discord" /> <GeneralIcon class="social-icon" icon="discord" />
<span class="menu-btn"> {{ $t('labels.community.joinDiscord') }} </span> <span class="menu-btn"> {{ $t('labels.community.joinDiscord') }} </span>
</NcMenuItem> </NcMenuItem>
</a> </a>
<a href="https://www.reddit.com/r/NocoDB" target="_blank" class="!underline-transparent"> <a v-e="['c:nocodb:reddit']" href="https://www.reddit.com/r/NocoDB" target="_blank" class="!underline-transparent">
<NcMenuItem class="social-icon-wrapper"> <NcMenuItem class="social-icon-wrapper">
<GeneralIcon class="social-icon" icon="reddit" /> <GeneralIcon class="social-icon" icon="reddit" />
<span class="menu-btn"> {{ $t('labels.community.joinReddit') }} </span> <span class="menu-btn"> {{ $t('labels.community.joinReddit') }} </span>
</NcMenuItem> </NcMenuItem>
</a> </a>
<a href="https://twitter.com/nocodb" target="_blank" class="!underline-transparent"> <a v-e="['c:nocodb:twitter']" href="https://twitter.com/nocodb" target="_blank" class="!underline-transparent">
<NcMenuItem class="social-icon-wrapper group"> <NcMenuItem class="social-icon-wrapper group">
<GeneralIcon class="text-gray-500 group-hover:text-gray-800 my-0.5" icon="twitter" /> <GeneralIcon class="text-gray-500 group-hover:text-gray-800 my-0.5" icon="twitter" />
<span class="menu-btn"> {{ $t('labels.twitter') }} </span> <span class="menu-btn"> {{ $t('labels.twitter') }} </span>
@ -117,7 +117,7 @@ onMounted(() => {
<template v-if="!appInfo.ee"> <template v-if="!appInfo.ee">
<NcDivider /> <NcDivider />
<a-popover key="language" class="lang-menu !py-1.5" placement="rightBottom"> <a-popover key="language" class="lang-menu !py-1.5" placement="rightBottom">
<NcMenuItem> <NcMenuItem v-e="['c:translate:open']">
<GeneralIcon icon="translate" class="group-hover:text-black nc-language ml-0.25 menu-icon" /> <GeneralIcon icon="translate" class="group-hover:text-black nc-language ml-0.25 menu-icon" />
{{ $t('labels.language') }} {{ $t('labels.language') }}
<div class="flex items-center text-gray-400 text-xs">{{ $t('labels.community.communityTranslated') }}</div> <div class="flex items-center text-gray-400 text-xs">{{ $t('labels.community.communityTranslated') }}</div>
@ -136,13 +136,13 @@ onMounted(() => {
<template v-if="!isMobileMode"> <template v-if="!isMobileMode">
<NcDivider /> <NcDivider />
<NcMenuItem @click="onCopy"> <NcMenuItem v-e="['c:auth-token:copy']" @click="onCopy">
<GeneralIcon v-if="isAuthTokenCopied" icon="check" class="group-hover:text-black menu-icon" /> <GeneralIcon v-if="isAuthTokenCopied" icon="check" class="group-hover:text-black menu-icon" />
<GeneralIcon v-else icon="copy" class="menu-icon" /> <GeneralIcon v-else icon="copy" class="menu-icon" />
<template v-if="isAuthTokenCopied"> {{ $t('title.copiedAuthToken') }} </template> <template v-if="isAuthTokenCopied"> {{ $t('title.copiedAuthToken') }} </template>
<template v-else> {{ $t('title.copyAuthToken') }} </template> <template v-else> {{ $t('title.copyAuthToken') }} </template>
</NcMenuItem> </NcMenuItem>
<nuxt-link v-e="['c:navbar:user:email']" class="!no-underline" to="/account/profile"> <nuxt-link v-e="['c:user:settings']" class="!no-underline" to="/account/profile">
<NcMenuItem> <GeneralIcon icon="settings" class="menu-icon" /> {{ $t('title.accountSettings') }} </NcMenuItem> <NcMenuItem> <GeneralIcon icon="settings" class="menu-icon" /> {{ $t('title.accountSettings') }} </NcMenuItem>
</nuxt-link> </nuxt-link>
</template> </template>

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

@ -70,6 +70,7 @@ function openQuickImportDialog(type: string) {
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('airtableImport', { roles: projectRole })" v-if="isUIAllowed('airtableImport', { roles: projectRole })"
key="quick-import-airtable" key="quick-import-airtable"
v-e="['c:import:airtable']"
@click="openAirtableImportDialog(base.id)" @click="openAirtableImportDialog(base.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" />
@ -79,6 +80,7 @@ function openQuickImportDialog(type: string) {
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('csvImport', { roles: projectRole })" v-if="isUIAllowed('csvImport', { roles: projectRole })"
key="quick-import-csv" key="quick-import-csv"
v-e="['c:import:csv']"
@click="openQuickImportDialog('csv')" @click="openQuickImportDialog('csv')"
> >
<GeneralIcon icon="csv" class="w-4 group-hover:text-black" /> <GeneralIcon icon="csv" class="w-4 group-hover:text-black" />
@ -88,6 +90,7 @@ function openQuickImportDialog(type: string) {
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('jsonImport', { roles: projectRole })" v-if="isUIAllowed('jsonImport', { roles: projectRole })"
key="quick-import-json" key="quick-import-json"
v-e="['c:import:json']"
@click="openQuickImportDialog('json')" @click="openQuickImportDialog('json')"
> >
<GeneralIcon icon="code" class="w-4 group-hover:text-black" /> <GeneralIcon icon="code" class="w-4 group-hover:text-black" />
@ -97,6 +100,7 @@ function openQuickImportDialog(type: string) {
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('excelImport', { roles: projectRole })" v-if="isUIAllowed('excelImport', { roles: projectRole })"
key="quick-import-excel" key="quick-import-excel"
v-e="['c:import:excel']"
@click="openQuickImportDialog('excel')" @click="openQuickImportDialog('excel')"
> >
<GeneralIcon icon="excel" class="max-w-4 group-hover:text-black" /> <GeneralIcon icon="excel" class="max-w-4 group-hover:text-black" />

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

@ -6,15 +6,17 @@ const { $e } = useNuxtApp()
const { refreshCommandPalette } = useCommandPalette() const { refreshCommandPalette } = useCommandPalette()
const viewsStore = useViewsStore() const viewsStore = useViewsStore()
const { views } = storeToRefs(viewsStore)
const { loadViews, navigateToView } = viewsStore const { loadViews, navigateToView } = viewsStore
const table = inject(SidebarTableInj)! const table = inject(SidebarTableInj)!
const project = inject(ProjectInj)! const project = inject(ProjectInj)!
const isViewListLoading = ref(false)
const toBeCreateType = ref<ViewTypes>()
const isOpen = ref(false) const isOpen = ref(false)
function onOpenModal({ async function onOpenModal({
title = '', title = '',
type, type,
copyViewId, copyViewId,
@ -25,7 +27,17 @@ function onOpenModal({
copyViewId?: string copyViewId?: string
groupingFieldColumnId?: string groupingFieldColumnId?: string
}) { }) {
if (isViewListLoading.value) return
toBeCreateType.value = type
isViewListLoading.value = true
await loadViews({
tableId: table.value.id!,
})
isOpen.value = false isOpen.value = false
isViewListLoading.value = false
const isDlgOpen = ref(true) const isDlgOpen = ref(true)
@ -36,7 +48,6 @@ function onOpenModal({
'tableId': table.value.id, 'tableId': table.value.id,
'selectedViewId': copyViewId, 'selectedViewId': copyViewId,
groupingFieldColumnId, groupingFieldColumnId,
'views': views,
'onUpdate:modelValue': closeDialog, 'onUpdate:modelValue': closeDialog,
'onCreated': async (view: ViewType) => { 'onCreated': async (view: ViewType) => {
closeDialog() closeDialog()
@ -44,9 +55,15 @@ function onOpenModal({
refreshCommandPalette() refreshCommandPalette()
await loadViews({ await loadViews({
tableId: table.value.id!,
force: true, force: true,
}) })
table.value.meta = {
...(table.value.meta as object),
hasNonDefaultViews: true,
}
navigateToView({ navigateToView({
view, view,
tableId: table.value.id!, tableId: table.value.id!,
@ -59,6 +76,7 @@ function onOpenModal({
function closeDialog() { function closeDialog() {
isOpen.value = false isOpen.value = false
isDlgOpen.value = false
close(1000) close(1000)
} }
@ -66,18 +84,19 @@ function onOpenModal({
</script> </script>
<template> <template>
<NcDropdown v-model:isOpen="isOpen" destroy-popup-on-hide @click.stop="isOpen = !isOpen"> <NcDropdown v-model:visible="isOpen" destroy-popup-on-hide @click.stop="isOpen = true">
<slot /> <slot />
<template #overlay> <template #overlay>
<NcMenu class="max-w-48"> <NcMenu class="max-w-48">
<NcMenuItem @click="onOpenModal({ type: ViewTypes.GRID })"> <NcMenuItem @click.stop="onOpenModal({ type: ViewTypes.GRID })">
<div class="item" data-testid="sidebar-view-create-grid"> <div class="item" data-testid="sidebar-view-create-grid">
<div class="item-inner"> <div class="item-inner">
<GeneralViewIcon :meta="{ type: ViewTypes.GRID }" /> <GeneralViewIcon :meta="{ type: ViewTypes.GRID }" />
<div>Grid</div> <div>Grid</div>
</div> </div>
<GeneralIcon class="plus" icon="plus" /> <GeneralLoader v-if="toBeCreateType === ViewTypes.GRID && isViewListLoading" />
<GeneralIcon v-else class="plus" icon="plus" />
</div> </div>
</NcMenuItem> </NcMenuItem>
@ -88,7 +107,8 @@ function onOpenModal({
<div>Form</div> <div>Form</div>
</div> </div>
<GeneralIcon class="plus" icon="plus" /> <GeneralLoader v-if="toBeCreateType === ViewTypes.FORM && isViewListLoading" />
<GeneralIcon v-else class="plus" icon="plus" />
</div> </div>
</NcMenuItem> </NcMenuItem>
<NcMenuItem @click="onOpenModal({ type: ViewTypes.GALLERY })"> <NcMenuItem @click="onOpenModal({ type: ViewTypes.GALLERY })">
@ -98,7 +118,8 @@ function onOpenModal({
<div>Gallery</div> <div>Gallery</div>
</div> </div>
<GeneralIcon class="plus" icon="plus" /> <GeneralLoader v-if="toBeCreateType === ViewTypes.GALLERY && isViewListLoading" />
<GeneralIcon v-else class="plus" icon="plus" />
</div> </div>
</NcMenuItem> </NcMenuItem>
<NcMenuItem data-testid="sidebar-view-create-kanban" @click="onOpenModal({ type: ViewTypes.KANBAN })"> <NcMenuItem data-testid="sidebar-view-create-kanban" @click="onOpenModal({ type: ViewTypes.KANBAN })">
@ -108,7 +129,8 @@ function onOpenModal({
<div>Kanban</div> <div>Kanban</div>
</div> </div>
<GeneralIcon class="plus" icon="plus" /> <GeneralLoader v-if="toBeCreateType === ViewTypes.KANBAN && isViewListLoading" />
<GeneralIcon v-else class="plus" icon="plus" />
</div> </div>
</NcMenuItem> </NcMenuItem>
</NcMenu> </NcMenu>

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

@ -29,6 +29,9 @@ const indicator = h(LoadingOutlined, {
const router = useRouter() const router = useRouter()
const route = router.currentRoute const route = router.currentRoute
const { isSharedBase } = storeToRefs(useProject())
const { projectUrl } = useProject()
const { setMenuContext, openRenameTableDialog, duplicateTable, contextMenuTarget } = inject(TreeViewInj)! const { setMenuContext, openRenameTableDialog, duplicateTable, contextMenuTarget } = inject(TreeViewInj)!
const project = inject(ProjectInj)! const project = inject(ProjectInj)!
@ -65,8 +68,6 @@ const projectRole = inject(ProjectRoleInj)
const { activeProjectId } = storeToRefs(useProjects()) const { activeProjectId } = storeToRefs(useProjects())
const { projectUrl } = useProject()
const toggleDialog = inject(ToggleDialogInj, () => {}) const toggleDialog = inject(ToggleDialogInj, () => {})
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
@ -162,8 +163,6 @@ const setIcon = async (icon: string, project: ProjectType) => {
} }
function openTableCreateDialog(baseIndex?: number | undefined) { function openTableCreateDialog(baseIndex?: number | undefined) {
$e('c:table:create:navdraw')
const isOpen = ref(true) const isOpen = ref(true)
let baseId = project.value!.bases?.[0].id let baseId = project.value!.bases?.[0].id
if (typeof baseIndex === 'number') { if (typeof baseIndex === 'number') {
@ -228,14 +227,13 @@ const addNewProjectChildEntity = async () => {
} }
} }
// todo: temp
const isSharedBase = ref(false)
const onProjectClick = async (project: NcProject, ignoreNavigation?: boolean, toggleIsExpanded?: boolean) => { const onProjectClick = async (project: NcProject, ignoreNavigation?: boolean, toggleIsExpanded?: boolean) => {
if (!project) { if (!project) {
return return
} }
if (!toggleIsExpanded) $e('c:base:open')
ignoreNavigation = isMobileMode.value || ignoreNavigation ignoreNavigation = isMobileMode.value || ignoreNavigation
toggleIsExpanded = isMobileMode.value || toggleIsExpanded toggleIsExpanded = isMobileMode.value || toggleIsExpanded
@ -247,12 +245,6 @@ const onProjectClick = async (project: NcProject, ignoreNavigation?: boolean, to
const isProjectPopulated = projectsStore.isProjectPopulated(project.id!) const isProjectPopulated = projectsStore.isProjectPopulated(project.id!)
let isSharedBase = false
// if shared base ignore navigation
if (route.value.params.typeOrId === 'base') {
isSharedBase = true
}
if (!isProjectPopulated) project.isLoading = true if (!isProjectPopulated) project.isLoading = true
if (!ignoreNavigation) { if (!ignoreNavigation) {
@ -260,7 +252,7 @@ const onProjectClick = async (project: NcProject, ignoreNavigation?: boolean, to
projectUrl({ projectUrl({
id: project.id!, id: project.id!,
type: 'database', type: 'database',
isSharedBase, isSharedBase: isSharedBase.value,
}), }),
) )
} }
@ -384,6 +376,16 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
) )
$e('a:project:duplicate') $e('a:project:duplicate')
} }
const tableDelete = () => {
isTableDeleteDialogVisible.value = true
$e('c:table:delete')
}
const projectDelete = () => {
isProjectDeleteDialogVisible.value = true
$e('c:project:delete')
}
</script> </script>
<template> <template>
@ -405,6 +407,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
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 project-title-node h-7.25 flex-grow rounded-md group flex items-center w-full pr-1"
> >
<NcButton <NcButton
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"
@ -428,6 +431,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
<LazyGeneralEmojiPicker <LazyGeneralEmojiPicker
v-else v-else
:key="project.meta?.icon" :key="project.meta?.icon"
v-e="['c:base:emojiSelect']"
:emoji="project.meta?.icon" :emoji="project.meta?.icon"
:readonly="true" :readonly="true"
size="small" size="small"
@ -464,6 +468,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
<NcDropdown v-model:visible="isOptionsOpen" :trigger="['click']"> <NcDropdown v-model:visible="isOptionsOpen" :trigger="['click']">
<NcButton <NcButton
v-e="['c:base:options']"
class="nc-sidebar-node-btn" class="nc-sidebar-node-btn"
:class="{ '!text-black !opacity-100': isOptionsOpen }" :class="{ '!text-black !opacity-100': isOptionsOpen }"
data-testid="nc-sidebar-context-menu" data-testid="nc-sidebar-context-menu"
@ -484,13 +489,19 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
@click="isOptionsOpen = false" @click="isOptionsOpen = false"
> >
<template v-if="!isSharedBase"> <template v-if="!isSharedBase">
<NcMenuItem v-if="isUIAllowed('projectRename')" data-testid="nc-sidebar-project-rename" @click="enableEditMode"> <NcMenuItem
v-if="isUIAllowed('projectRename')"
v-e="['c:base:rename']"
data-testid="nc-sidebar-project-rename"
@click="enableEditMode"
>
<GeneralIcon icon="edit" class="group-hover:text-black" /> <GeneralIcon icon="edit" class="group-hover:text-black" />
{{ $t('general.rename') }} {{ $t('general.rename') }}
</NcMenuItem> </NcMenuItem>
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('projectDuplicate', { roles: [stringifyRolesObj(orgRoles), projectRole].join() })" v-if="isUIAllowed('projectDuplicate', { roles: [stringifyRolesObj(orgRoles), projectRole].join() })"
v-e="['c:base:duplicate']"
data-testid="nc-sidebar-project-duplicate" data-testid="nc-sidebar-project-duplicate"
@click="duplicateProject(project)" @click="duplicateProject(project)"
> >
@ -504,7 +515,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
<NcMenuItem <NcMenuItem
v-if="!isEeUI" v-if="!isEeUI"
key="copy" key="copy"
v-e="['c:navbar:user:copy-proj-info']" v-e="['c:base:copy-proj-info']"
data-testid="nc-sidebar-project-copy-project-info" data-testid="nc-sidebar-project-copy-project-info"
@click.stop="copyProjectInfo" @click.stop="copyProjectInfo"
> >
@ -513,7 +524,12 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
</NcMenuItem> </NcMenuItem>
<!-- ERD View --> <!-- ERD View -->
<NcMenuItem key="erd" data-testid="nc-sidebar-project-relations" @click="openProjectErdView(project)"> <NcMenuItem
key="erd"
v-e="['c:base:erd']"
data-testid="nc-sidebar-project-relations"
@click="openProjectErdView(project)"
>
<GeneralIcon icon="erd" /> <GeneralIcon icon="erd" />
{{ $t('title.relations') }} {{ $t('title.relations') }}
</NcMenuItem> </NcMenuItem>
@ -522,9 +538,14 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('apiDocs')" v-if="isUIAllowed('apiDocs')"
key="api" key="api"
v-e="['e:api-docs']" v-e="['c:base:api-docs']"
data-testid="nc-sidebar-project-rest-apis" data-testid="nc-sidebar-project-rest-apis"
@click.stop="openLink(`/api/v1/db/meta/projects/${project.id}/swagger`, appInfo.ncSiteUrl)" @click.stop="
() => {
$e('c:base:api-docs')
openLink(`/api/v1/db/meta/projects/${project.id}/swagger`, appInfo.ncSiteUrl)
}
"
> >
<GeneralIcon icon="snippet" class="group-hover:text-black !max-w-3.9" /> <GeneralIcon icon="snippet" class="group-hover:text-black !max-w-3.9" />
{{ $t('activity.account.swagger') }} {{ $t('activity.account.swagger') }}
@ -541,7 +562,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('projectMiscSettings')" v-if="isUIAllowed('projectMiscSettings')"
key="teamAndSettings" key="teamAndSettings"
v-e="['c:navdraw:project-settings']" v-e="['c:base:settings']"
data-testid="nc-sidebar-project-settings" data-testid="nc-sidebar-project-settings"
class="nc-sidebar-project-project-settings" class="nc-sidebar-project-project-settings"
@click="toggleDialog(true, 'teamAndAuth', undefined, project.id)" @click="toggleDialog(true, 'teamAndAuth', undefined, project.id)"
@ -553,7 +574,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
v-if="isUIAllowed('projectDelete', { roles: [stringifyRolesObj(orgRoles), projectRole].join() })" v-if="isUIAllowed('projectDelete', { roles: [stringifyRolesObj(orgRoles), projectRole].join() })"
data-testid="nc-sidebar-project-delete" data-testid="nc-sidebar-project-delete"
class="!text-red-500 !hover:bg-red-50" class="!text-red-500 !hover:bg-red-50"
@click="isProjectDeleteDialogVisible = true" @click="projectDelete"
> >
<GeneralIcon icon="delete" class="w-4" /> <GeneralIcon icon="delete" class="w-4" />
{{ $t('general.delete') }} {{ $t('general.delete') }}
@ -564,6 +585,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
<NcButton <NcButton
v-if="isUIAllowed('tableCreate', { roles: projectRole })" v-if="isUIAllowed('tableCreate', { roles: projectRole })"
v-e="['c:base:create-table']"
class="nc-sidebar-node-btn" class="nc-sidebar-node-btn"
size="xxsmall" size="xxsmall"
type="text" type="text"
@ -598,6 +620,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
<a-collapse <a-collapse
v-else-if="base && base.enabled" v-else-if="base && base.enabled"
v-model:activeKey="activeKey" v-model:activeKey="activeKey"
v-e="['c:source:toggle-expand']"
class="!mx-0 !px-0 nc-sidebar-base-node" 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"
@ -653,6 +676,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
@update:visible="isBasesOptionsOpen[base!.id!] = $event" @update:visible="isBasesOptionsOpen[base!.id!] = $event"
> >
<NcButton <NcButton
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[base!.id!] }"
type="text" type="text"
@ -671,7 +695,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
@click="isBasesOptionsOpen[base!.id!] = false" @click="isBasesOptionsOpen[base!.id!] = false"
> >
<!-- ERD View --> <!-- ERD View -->
<NcMenuItem key="erd" @click="openErdView(base)"> <NcMenuItem key="erd" v-e="['c:source:erd']" @click="openErdView(base)">
<GeneralIcon icon="erd" /> <GeneralIcon icon="erd" />
{{ $t('title.relations') }} {{ $t('title.relations') }}
</NcMenuItem> </NcMenuItem>
@ -683,6 +707,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
<NcButton <NcButton
v-if="isUIAllowed('tableCreate', { roles: projectRole })" v-if="isUIAllowed('tableCreate', { roles: projectRole })"
v-e="['c:source:add-table']"
type="text" type="text"
size="xxsmall" size="xxsmall"
class="nc-sidebar-node-btn" class="nc-sidebar-node-btn"
@ -716,6 +741,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
<template v-else-if="contextMenuTarget.type === 'base'"></template> <template v-else-if="contextMenuTarget.type === 'base'"></template>
<template v-else-if="contextMenuTarget.type === 'table'"> <template v-else-if="contextMenuTarget.type === 'table'">
v-e="['c:table:rename']"
<NcMenuItem v-if="isUIAllowed('tableRename')" @click="openRenameTableDialog(contextMenuTarget.value, true)"> <NcMenuItem v-if="isUIAllowed('tableRename')" @click="openRenameTableDialog(contextMenuTarget.value, true)">
<div class="nc-project-option-item"> <div class="nc-project-option-item">
<GeneralIcon icon="edit" class="text-gray-700" /> <GeneralIcon icon="edit" class="text-gray-700" />
@ -725,6 +751,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('tableDuplicate') && (contextMenuBase?.is_meta || contextMenuBase?.is_local)" v-if="isUIAllowed('tableDuplicate') && (contextMenuBase?.is_meta || contextMenuBase?.is_local)"
v-e="['c:table:duplicate']"
@click="duplicateTable(contextMenuTarget.value)" @click="duplicateTable(contextMenuTarget.value)"
> >
<div class="nc-project-option-item"> <div class="nc-project-option-item">
@ -733,7 +760,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
</div> </div>
</NcMenuItem> </NcMenuItem>
<NcDivider /> <NcDivider />
<NcMenuItem v-if="isUIAllowed('table-delete')" class="!hover:bg-red-50" @click="isTableDeleteDialogVisible = true"> <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-project-option-item text-red-600">
<GeneralIcon icon="delete" /> <GeneralIcon icon="delete" />
{{ $t('general.delete') }} {{ $t('general.delete') }}

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

@ -151,7 +151,6 @@ const availableTables = computed(() => {
<TableNode <TableNode
v-for="table of availableTables" v-for="table of availableTables"
:key="table.id" :key="table.id"
v-e="['a:table:open']"
class="nc-tree-item text-sm" class="nc-tree-item text-sm"
:data-order="table.order" :data-order="table.order"
:data-id="table.id" :data-id="table.id"

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

@ -161,19 +161,28 @@ const isTableOpened = computed(() => {
> >
<template #title>{{ table.table_name }}</template> <template #title>{{ table.table_name }}</template>
<div <div
v-e="['a:table:open']"
class="table-context flex items-center gap-1 h-full" class="table-context flex items-center gap-1 h-full"
:data-testid="`nc-tbl-side-node-${table.title}`" :data-testid="`nc-tbl-side-node-${table.title}`"
@contextmenu="setMenuContext('table', table)" @contextmenu="setMenuContext('table', table)"
@click="onOpenTable" @click="onOpenTable"
> >
<div class="flex flex-row h-full items-center"> <div class="flex flex-row h-full items-center">
<NcButton type="text" size="xxsmall" class="nc-sidebar-node-btn nc-sidebar-expand" @click.stop="onExpand"> <NcButton
v-if="(table.meta as any)?.hasNonDefaultViews"
v-e="['c:table:toggle-expand']"
type="text"
size="xxsmall"
class="nc-sidebar-node-btn nc-sidebar-expand"
@click.stop="onExpand"
>
<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-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="{ '!rotate-180': isExpanded }" :class="{ '!rotate-180': isExpanded }"
/> />
</NcButton> </NcButton>
<div v-else class="min-w-5.75"></div>
<div class="flex w-auto" :data-testid="`tree-view-table-draggable-handle-${table.title}`"> <div class="flex w-auto" :data-testid="`tree-view-table-draggable-handle-${table.title}`">
<div <div
class="flex items-center nc-table-icon" class="flex items-center nc-table-icon"
@ -184,6 +193,7 @@ const isTableOpened = computed(() => {
> >
<LazyGeneralEmojiPicker <LazyGeneralEmojiPicker
:key="table.meta?.icon" :key="table.meta?.icon"
v-e="['c:table:emoji-picker']"
:emoji="table.meta?.icon" :emoji="table.meta?.icon"
size="small" size="small"
:readonly="!canUserEditEmote || isMobileMode" :readonly="!canUserEditEmote || isMobileMode"
@ -235,6 +245,7 @@ const isTableOpened = computed(() => {
!isSharedBase && !isSharedBase &&
(isUIAllowed('tableRename', { roles: projectRole }) || isUIAllowed('tableDelete', { roles: projectRole })) (isUIAllowed('tableRename', { roles: projectRole }) || isUIAllowed('tableDelete', { roles: projectRole }))
" "
v-e="['c:table:option']"
:trigger="['click']" :trigger="['click']"
class="nc-sidebar-node-btn" class="nc-sidebar-node-btn"
@click.stop @click.stop
@ -248,6 +259,7 @@ const isTableOpened = computed(() => {
<NcMenu> <NcMenu>
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('tableRename', { roles: projectRole })" v-if="isUIAllowed('tableRename', { roles: projectRole })"
v-e="['c:table:rename']"
:data-testid="`sidebar-table-rename-${table.title}`" :data-testid="`sidebar-table-rename-${table.title}`"
@click="openRenameTableDialog(table, project.bases[baseIndex].id)" @click="openRenameTableDialog(table, project.bases[baseIndex].id)"
> >
@ -261,6 +273,7 @@ const isTableOpened = computed(() => {
project.bases?.[baseIndex] && project.bases?.[baseIndex] &&
(project.bases[baseIndex].is_meta || project.bases[baseIndex].is_local) (project.bases[baseIndex].is_meta || project.bases[baseIndex].is_local)
" "
v-e="['c:table:duplicate']"
:data-testid="`sidebar-table-duplicate-${table.title}`" :data-testid="`sidebar-table-duplicate-${table.title}`"
@click="duplicateTable(table)" @click="duplicateTable(table)"
> >
@ -270,6 +283,7 @@ const isTableOpened = computed(() => {
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('tableDelete', { roles: projectRole })" v-if="isUIAllowed('tableDelete', { roles: projectRole })"
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"
@click="isTableDeleteDialogVisible = true" @click="isTableDeleteDialogVisible = true"
@ -282,6 +296,7 @@ const isTableOpened = computed(() => {
</NcDropdown> </NcDropdown>
<DashboardTreeViewCreateViewBtn v-if="isUIAllowed('viewCreateOrEdit')"> <DashboardTreeViewCreateViewBtn v-if="isUIAllowed('viewCreateOrEdit')">
<NcButton <NcButton
v-e="['c:view:create']"
type="text" type="text"
size="xxsmall" size="xxsmall"
class="nc-create-view-btn nc-sidebar-node-btn" class="nc-create-view-btn nc-sidebar-node-btn"

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

@ -277,6 +277,13 @@ function openDeleteDialog(view: ViewType) {
tableId: table.value.id!, tableId: table.value.id!,
force: true, force: true,
}) })
const activeNonDefaultViews = viewsByTable.value.get(table.value.id!)?.filter((v) => !v.is_default) ?? []
table.value.meta = {
...(table.value.meta as object),
hasNonDefaultViews: activeNonDefaultViews.length > 1,
}
}, },
}) })
@ -334,6 +341,7 @@ function onOpenModal({
await loadViews({ await loadViews({
force: true, force: true,
tableId: table.value.id!,
}) })
navigateToView({ navigateToView({
@ -356,18 +364,8 @@ function onOpenModal({
</script> </script>
<template> <template>
<div
v-if="!views.length"
class="text-gray-500 my-1.75 xs:(my-2.5 text-base)"
:class="{
'ml-19.25 xs:ml-22.25': isDefaultBase,
'ml-24.75 xs:ml-30': !isDefaultBase,
}"
>
{{ $t('labels.noViews') }}
</div>
<a-menu <a-menu
v-if="views.length"
ref="menuRef" ref="menuRef"
:class="{ dragging }" :class="{ dragging }"
class="nc-views-menu flex flex-col w-full !border-r-0 !bg-inherit" class="nc-views-menu flex flex-col w-full !border-r-0 !bg-inherit"

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

@ -45,8 +45,6 @@ const { isMobileMode } = useGlobal()
const { isUIAllowed } = useRoles() const { isUIAllowed } = useRoles()
const { activeViewTitleOrId } = storeToRefs(useViewsStore())
const project = inject(ProjectInj, ref()) const project = inject(ProjectInj, ref())
const activeView = inject(ActiveViewInj, ref()) const activeView = inject(ActiveViewInj, ref())
@ -191,20 +189,11 @@ function onStopEdit() {
isStopped.value = false isStopped.value = false
}, 250) }, 250)
} }
function onRef(el: HTMLElement) {
if (activeViewTitleOrId.value === vModel.value.id) {
nextTick(() => {
setTimeout(() => {
el?.scrollIntoView({ block: 'nearest', inline: 'nearest' })
}, 1000)
})
}
}
</script> </script>
<template> <template>
<a-menu-item <a-menu-item
v-e="['c:view:open']"
class="nc-sidebar-node !min-h-7 !max-h-7 !mb-0.25 select-none group text-gray-700 !flex !items-center !mt-0 hover:(!bg-gray-200 !text-gray-900) cursor-pointer" class="nc-sidebar-node !min-h-7 !max-h-7 !mb-0.25 select-none group text-gray-700 !flex !items-center !mt-0 hover:(!bg-gray-200 !text-gray-900) cursor-pointer"
:class="{ :class="{
'!pl-18 !xs:(pl-19.75)': isDefaultBase, '!pl-18 !xs:(pl-19.75)': isDefaultBase,
@ -214,14 +203,10 @@ function onRef(el: HTMLElement) {
@dblclick.stop="onDblClick" @dblclick.stop="onDblClick"
@click="onClick" @click="onClick"
> >
<div <div v-e="['a:view:open', { view: vModel.type }]" class="text-sm flex items-center w-full gap-1" data-testid="view-item">
:ref="onRef"
v-e="['a:view:open', { view: vModel.type }]"
class="text-sm flex items-center w-full gap-1"
data-testid="view-item"
>
<div class="flex min-w-6" :data-testid="`view-sidebar-drag-handle-${vModel.alias || vModel.title}`"> <div class="flex min-w-6" :data-testid="`view-sidebar-drag-handle-${vModel.alias || vModel.title}`">
<LazyGeneralEmojiPicker <LazyGeneralEmojiPicker
v-e="['c:view:emoji-picker']"
class="nc-table-icon" class="nc-table-icon"
:emoji="props.view?.meta?.icon" :emoji="props.view?.meta?.icon"
size="small" size="small"
@ -264,6 +249,7 @@ function onRef(el: HTMLElement) {
<template v-if="!isEditing && !isLocked && isUIAllowed('viewCreateOrEdit')"> <template v-if="!isEditing && !isLocked && isUIAllowed('viewCreateOrEdit')">
<NcDropdown v-model:visible="isDropdownOpen" overlay-class-name="!rounded-lg"> <NcDropdown v-model:visible="isDropdownOpen" overlay-class-name="!rounded-lg">
<NcButton <NcButton
v-e="['c:view:option']"
type="text" type="text"
size="xxsmall" size="xxsmall"
class="nc-sidebar-node-btn invisible !group-hover:visible nc-sidebar-view-node-context-btn" class="nc-sidebar-node-btn invisible !group-hover:visible nc-sidebar-view-node-context-btn"
@ -277,11 +263,11 @@ function onRef(el: HTMLElement) {
<template #overlay> <template #overlay>
<NcMenu class="min-w-27" :data-testid="`view-sidebar-view-actions-${vModel.alias || vModel.title}`"> <NcMenu class="min-w-27" :data-testid="`view-sidebar-view-actions-${vModel.alias || vModel.title}`">
<NcMenuItem @click.stop="onDblClick"> <NcMenuItem v-e="['c:view:rename']" @click.stop="onDblClick">
<GeneralIcon icon="edit" /> <GeneralIcon icon="edit" />
<div class="-ml-0.25">{{ $t('general.rename') }}</div> <div class="-ml-0.25">{{ $t('general.rename') }}</div>
</NcMenuItem> </NcMenuItem>
<NcMenuItem @click.stop="onDuplicate"> <NcMenuItem v-e="['c:view:duplicate']" @click.stop="onDuplicate">
<GeneralIcon icon="duplicate" class="nc-view-copy-icon" /> <GeneralIcon icon="duplicate" class="nc-view-copy-icon" />
{{ $t('general.duplicate') }} {{ $t('general.duplicate') }}
</NcMenuItem> </NcMenuItem>
@ -289,7 +275,7 @@ function onRef(el: HTMLElement) {
<NcDivider /> <NcDivider />
<template v-if="!vModel.is_default"> <template v-if="!vModel.is_default">
<NcMenuItem class="!text-red-500 !hover:bg-red-50" @click.stop="onDelete"> <NcMenuItem v-e="['c:view:delete']" class="!text-red-500 !hover:bg-red-50" @click.stop="onDelete">
<GeneralIcon icon="delete" class="text-sm nc-view-delete-icon" /> <GeneralIcon icon="delete" class="text-sm nc-view-delete-icon" />
<div class="-ml-0.25">{{ $t('general.delete') }}</div> <div class="-ml-0.25">{{ $t('general.delete') }}</div>
</NcMenuItem> </NcMenuItem>

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

@ -71,7 +71,7 @@ const setMenuContext = (type: 'project' | 'base' | 'table' | 'main' | 'layout',
function openRenameTableDialog(table: TableType, rightClick = false) { function openRenameTableDialog(table: TableType, rightClick = false) {
if (!table || !table.base_id) return if (!table || !table.base_id) return
$e(rightClick ? 'c:table:rename:navdraw:right-click' : 'c:table:rename:navdraw:options') $e('c:table:rename')
const isOpen = ref(true) const isOpen = ref(true)
@ -115,6 +115,8 @@ const duplicateTable = async (table: TableType) => {
const isOpen = ref(true) const isOpen = ref(true)
$e('c:table:duplicate')
const { close } = useDialog(resolveComponent('DlgTableDuplicate'), { const { close } = useDialog(resolveComponent('DlgTableDuplicate'), {
'modelValue': isOpen, 'modelValue': isOpen,
'table': table, 'table': table,

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

@ -25,6 +25,8 @@ const { loadProject } = useProjects()
const projectStore = useProject() const projectStore = useProject()
const { project } = storeToRefs(projectStore) const { project } = storeToRefs(projectStore)
const { projectPageTab } = storeToRefs(useConfigStore())
const { refreshCommandPalette } = useCommandPalette() const { refreshCommandPalette } = useCommandPalette()
const sources = ref<BaseType[]>([]) const sources = ref<BaseType[]>([])
@ -140,11 +142,17 @@ const forceAwaken = () => {
emits('awaken', forceAwakened.value) emits('awaken', forceAwakened.value)
} }
onMounted(async () => { watch(
if (sources.value.length === 0) { projectPageTab,
() => {
if (projectPageTab.value === 'data-source') {
loadBases() loadBases()
} }
}) },
{
immediate: true,
},
)
watch( watch(
() => props.reload, () => props.reload,

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

@ -27,10 +27,12 @@ import {
watch, watch,
} from '#imports' } from '#imports'
const { connectionType } = defineProps<{ connectionType: ClientType }>() const props = defineProps<{ connectionType?: ClientType }>()
const emit = defineEmits(['baseCreated', 'close']) const emit = defineEmits(['baseCreated', 'close'])
const connectionType = computed(() => props.connectionType ?? ClientType.MYSQL)
const projectStore = useProject() const projectStore = useProject()
const { loadProject } = useProjects() const { loadProject } = useProjects()
const { project } = storeToRefs(projectStore) const { project } = storeToRefs(projectStore)
@ -391,7 +393,7 @@ onMounted(async () => {
}) })
watch( watch(
() => connectionType, connectionType,
(v) => { (v) => {
formState.value.dataSource.client = v formState.value.dataSource.client = v
onClientChange() onClientChange()
@ -626,6 +628,7 @@ watch(
</NcButton> </NcButton>
<NcButton <NcButton
v-e="['a:source:create']"
size="small" size="small"
type="primary" type="primary"
:disabled="!testSuccess" :disabled="!testSuccess"

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

@ -95,7 +95,9 @@ const isEaster = ref(false)
</div> </div>
<div class="flex flex-row gap-x-2 mt-2.5 pt-2.5 justify-end"> <div class="flex flex-row gap-x-2 mt-2.5 pt-2.5 justify-end">
<NcButton key="back" type="secondary" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton> <NcButton key="back" type="secondary" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton>
<NcButton key="submit" :loading="isLoading" @click="_duplicate">{{ $t('general.confirm') }} </NcButton> <NcButton key="submit" v-e="['a:base:duplicate']" :loading="isLoading" @click="_duplicate"
>{{ $t('general.confirm') }}
</NcButton>
</div> </div>
</GeneralModal> </GeneralModal>
</template> </template>

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

@ -184,6 +184,7 @@ onMounted(() => {
<NcButton type="secondary" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton> <NcButton type="secondary" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton>
<NcButton <NcButton
v-e="['a:table:create']"
type="primary" type="primary"
:disabled="validateInfos.title.validateStatus === 'error'" :disabled="validateInfos.title.validateStatus === 'error'"
:loading="creating" :loading="creating"

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

@ -84,7 +84,9 @@ const isEaster = ref(false)
</div> </div>
<div class="flex flex-row gap-x-2 mt-2.5 pt-2.5 justify-end"> <div class="flex flex-row gap-x-2 mt-2.5 pt-2.5 justify-end">
<NcButton key="back" type="secondary" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton> <NcButton key="back" type="secondary" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton>
<NcButton key="submit" type="primary" :loading="isLoading" @click="_duplicate">{{ $t('general.confirm') }} </NcButton> <NcButton key="submit" v-e="['a:table:duplicate']" type="primary" :loading="isLoading" @click="_duplicate"
>{{ $t('general.confirm') }}
</NcButton>
</div> </div>
</GeneralModal> </GeneralModal>
</template> </template>

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

@ -13,7 +13,6 @@ interface Props {
selectedViewId?: string selectedViewId?: string
groupingFieldColumnId?: string groupingFieldColumnId?: string
geoDataFieldColumnId?: string geoDataFieldColumnId?: string
views: ViewType[]
tableId: string tableId: string
} }
@ -41,7 +40,9 @@ const emits = defineEmits<Emits>()
const { getMeta } = useMetas() const { getMeta } = useMetas()
const { views, selectedViewId, groupingFieldColumnId, geoDataFieldColumnId, tableId } = toRefs(props) const { viewsByTable } = storeToRefs(useViewsStore())
const { selectedViewId, groupingFieldColumnId, geoDataFieldColumnId, tableId } = toRefs(props)
const meta = ref<TableType | undefined>() const meta = ref<TableType | undefined>()
@ -57,6 +58,8 @@ const { api } = useApi()
const isViewCreating = ref(false) const isViewCreating = ref(false)
const views = computed(() => viewsByTable.value.get(tableId.value) ?? [])
const form = reactive<Form>({ const form = reactive<Form>({
title: props.title || '', title: props.title || '',
type: props.type, type: props.type,
@ -239,20 +242,45 @@ onMounted(async () => {
<div class="flex flex-row items-center gap-x-1.5"> <div class="flex flex-row items-center gap-x-1.5">
<GeneralViewIcon :meta="{ type: form.type }" class="nc-view-icon !text-xl" /> <GeneralViewIcon :meta="{ type: form.type }" class="nc-view-icon !text-xl" />
<template v-if="form.type === ViewTypes.GRID"> <template v-if="form.type === ViewTypes.GRID">
<template v-if="form.copy_from_id">
{{ $t('labels.duplicateGridView') }} {{ $t('labels.duplicateGridView') }}
</template> </template>
<template v-else>
{{ $t('labels.createGridView') }}
</template>
</template>
<template v-else-if="form.type === ViewTypes.GALLERY"> <template v-else-if="form.type === ViewTypes.GALLERY">
<template v-if="form.copy_from_id">
{{ $t('labels.duplicateGalleryView') }} {{ $t('labels.duplicateGalleryView') }}
</template> </template>
<template v-else>
{{ $t('labels.createGalleryView') }}
</template>
</template>
<template v-else-if="form.type === ViewTypes.FORM"> <template v-else-if="form.type === ViewTypes.FORM">
<template v-if="form.copy_from_id">
{{ $t('labels.duplicateFormView') }} {{ $t('labels.duplicateFormView') }}
</template> </template>
<template v-else>
{{ $t('labels.createFormView') }}
</template>
</template>
<template v-else-if="form.type === ViewTypes.KANBAN"> <template v-else-if="form.type === ViewTypes.KANBAN">
<template v-if="form.copy_from_id">
{{ $t('labels.duplicateKanbanView') }} {{ $t('labels.duplicateKanbanView') }}
</template> </template>
<template v-else>
{{ $t('labels.createKanbanView') }}
</template>
</template>
<template v-else>
<template v-if="form.copy_from_id">
{{ $t('labels.duplicateMapView') }}
</template>
<template v-else> <template v-else>
{{ $t('labels.duplicateView') }} {{ $t('labels.duplicateView') }}
</template> </template>
</template>
</div> </div>
</template> </template>
<div class="mt-2"> <div class="mt-2">
@ -306,7 +334,12 @@ onMounted(async () => {
{{ $t('general.cancel') }} {{ $t('general.cancel') }}
</NcButton> </NcButton>
<NcButton type="primary" :loading="isViewCreating" @click="onSubmit"> <NcButton
v-e="[form.copy_from_id ? 'a:view:duplicate' : 'a:view:create']"
type="primary"
:loading="isViewCreating"
@click="onSubmit"
>
{{ $t('labels.createView') }} {{ $t('labels.createView') }}
<template #loading> {{ $t('labels.creatingView') }}</template> <template #loading> {{ $t('labels.creatingView') }}</template>
</NcButton> </NcButton>

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

@ -119,13 +119,20 @@ const onRoleToggle = async () => {
<div class="flex flex-col w-full p-3 border-1 border-gray-100 rounded-md"> <div class="flex flex-col w-full p-3 border-1 border-gray-100 rounded-md">
<div class="flex flex-row w-full justify-between"> <div class="flex flex-row w-full justify-between">
<div class="text-black font-medium">{{ $t('activity.enablePublicAccess') }}</div> <div class="text-black font-medium">{{ $t('activity.enablePublicAccess') }}</div>
<a-switch :checked="isSharedBaseEnabled" :loading="isToggleBaseLoading" class="ml-2" @click="toggleSharedBase" /> <a-switch
v-e="['c:share:base:enable:toggle']"
:checked="isSharedBaseEnabled"
:loading="isToggleBaseLoading"
class="ml-2"
@click="toggleSharedBase"
/>
</div> </div>
<div v-if="isSharedBaseEnabled" class="flex flex-col w-full mt-3 border-t-1 pt-3 border-gray-100"> <div v-if="isSharedBaseEnabled" class="flex flex-col w-full mt-3 border-t-1 pt-3 border-gray-100">
<GeneralCopyUrl v-model:url="url" /> <GeneralCopyUrl v-model:url="url" />
<div class="flex flex-row justify-between mt-3 bg-gray-50 px-3 py-2 rounded-md"> <div class="flex flex-row justify-between mt-3 bg-gray-50 px-3 py-2 rounded-md">
<div class="text-black">{{ $t('activity.editingAccess') }}</div> <div class="text-black">{{ $t('activity.editingAccess') }}</div>
<a-switch <a-switch
v-e="['c:share:base:role:toggle']"
:loading="isRoleToggleLoading" :loading="isRoleToggleLoading"
:checked="base?.role === ShareBaseRole.Editor" :checked="base?.role === ShareBaseRole.Editor"
class="ml-2" class="ml-2"

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

@ -270,6 +270,7 @@ const isPublicShareDisabled = computed(() => {
<div class="flex flex-row w-full justify-between py-0.5"> <div class="flex flex-row w-full justify-between py-0.5">
<div class="flex" :style="{ fontWeight: 500 }">{{ $t('activity.enabledPublicViewing') }}</div> <div class="flex" :style="{ fontWeight: 500 }">{{ $t('activity.enabledPublicViewing') }}</div>
<a-switch <a-switch
v-e="['c:share:view:enable:toggle']"
data-testid="share-view-toggle" data-testid="share-view-toggle"
:checked="isPublicShared" :checked="isPublicShared"
:loading="isUpdating.public" :loading="isUpdating.public"
@ -286,6 +287,7 @@ const isPublicShareDisabled = computed(() => {
<div class="flex flex-row justify-between"> <div class="flex flex-row justify-between">
<div class="flex text-black">{{ $t('activity.restrictAccessWithPassword') }}</div> <div class="flex text-black">{{ $t('activity.restrictAccessWithPassword') }}</div>
<a-switch <a-switch
v-e="['c:share:view:password:toggle']"
data-testid="share-password-toggle" data-testid="share-password-toggle"
:checked="passwordProtected" :checked="passwordProtected"
:loading="isUpdating.password" :loading="isUpdating.password"
@ -320,6 +322,7 @@ const isPublicShareDisabled = computed(() => {
<div class="flex text-black">{{ $t('activity.allowDownload') }}</div> <div class="flex text-black">{{ $t('activity.allowDownload') }}</div>
<a-switch <a-switch
v-model:checked="allowCSVDownload" v-model:checked="allowCSVDownload"
v-e="['c:share:view:allow-csv-download:toggle']"
data-testid="share-download-toggle" data-testid="share-download-toggle"
:loading="isUpdating.download" :loading="isUpdating.download"
class="public-password-toggle !mt-0.25" class="public-password-toggle !mt-0.25"
@ -329,14 +332,22 @@ const isPublicShareDisabled = computed(() => {
<div v-if="activeView?.type === ViewTypes.FORM" class="flex flex-row justify-between"> <div v-if="activeView?.type === ViewTypes.FORM" class="flex flex-row justify-between">
<!-- use RTL orientation in form - todo: i18n --> <!-- use RTL orientation in form - todo: i18n -->
<div class="text-black">{{ $t('activity.surveyMode') }}</div> <div class="text-black">{{ $t('activity.surveyMode') }}</div>
<a-switch v-model:checked="surveyMode" data-testid="nc-modal-share-view__surveyMode"> <a-switch
v-model:checked="surveyMode"
v-e="['c:share:view:surver-mode:toggle']"
data-testid="nc-modal-share-view__surveyMode"
>
<!-- todo i18n --> <!-- todo i18n -->
</a-switch> </a-switch>
</div> </div>
<div v-if="activeView?.type === ViewTypes.FORM && isEeUI" class="flex flex-row justify-between"> <div v-if="activeView?.type === ViewTypes.FORM && isEeUI" class="flex flex-row justify-between">
<!-- use RTL orientation in form - todo: i18n --> <!-- use RTL orientation in form - todo: i18n -->
<div class="text-black">{{ $t('activity.rtlOrientation') }}</div> <div class="text-black">{{ $t('activity.rtlOrientation') }}</div>
<a-switch v-model:checked="withRTL" data-testid="nc-modal-share-view__RTL"> <a-switch
v-model:checked="withRTL"
v-e="['c:share:view:rtl-orientation:toggle']"
data-testid="nc-modal-share-view__RTL"
>
<!-- todo i18n --> <!-- todo i18n -->
</a-switch> </a-switch>
</div> </div>
@ -345,6 +356,7 @@ const isPublicShareDisabled = computed(() => {
<div class="flex flex-row justify-between"> <div class="flex flex-row justify-between">
<div class="text-black">{{ $t('activity.useTheme') }}</div> <div class="text-black">{{ $t('activity.useTheme') }}</div>
<a-switch <a-switch
v-e="['c:share:view:theme:toggle']"
data-testid="share-theme-toggle" data-testid="share-theme-toggle"
:checked="viewTheme" :checked="viewTheme"
:loading="isUpdating.password" :loading="isUpdating.password"

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

@ -85,7 +85,7 @@ watch(showShareModal, (val) => {
<template> <template>
<a-modal <a-modal
v-model:visible="showShareModal" v-model:visible="showShareModal"
class="!top-[55%]" class="!top-[1%]"
:class="{ active: showShareModal }" :class="{ active: showShareModal }"
wrap-class-name="nc-modal-share-collaborate" wrap-class-name="nc-modal-share-collaborate"
:closable="false" :closable="false"
@ -206,7 +206,7 @@ watch(showShareModal, (val) => {
<style lang="scss"> <style lang="scss">
.nc-modal-share-collaborate { .nc-modal-share-collaborate {
.ant-modal { .ant-modal {
top: 28vh !important; top: 10vh !important;
} }
.share-view, .share-view,

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

@ -12,6 +12,7 @@ const onClick = () => {
<template> <template>
<NcTooltip <NcTooltip
v-e="['c:leftSidebar:hideToggle']"
placement="topLeft" placement="topLeft"
hide-on-click hide-on-click
class="transition-all duration-150" class="transition-all duration-150"

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

@ -6,8 +6,8 @@ const { hoverable } = defineProps<{
</script> </script>
<template> <template>
<img <GeneralIcon
src="~/assets/nc-icons/database.svg" icon="ncDatabase"
class="text-[#2824FB] nc-project-icon" class="text-[#2824FB] nc-project-icon"
:class="{ :class="{
'nc-project-icon-hoverable': hoverable, 'nc-project-icon-hoverable': hoverable,

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

@ -44,6 +44,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
:data-sharetype="visibility" :data-sharetype="visibility"
> >
<NcButton <NcButton
v-e="['c:share:open']"
:size="isMobileMode ? 'medium' : 'small'" :size="isMobileMode ? 'medium' : 'small'"
class="z-10 !rounded-lg" class="z-10 !rounded-lg"
:class="{ :class="{

15
packages/nc-gui/components/nc/Pagination.vue

@ -3,6 +3,7 @@ const props = defineProps<{
current: number current: number
total: number total: number
pageSize: number pageSize: number
entityName?: string
}>() }>()
const emits = defineEmits(['update:current', 'update:pageSize']) const emits = defineEmits(['update:current', 'update:pageSize'])
@ -13,6 +14,8 @@ const current = useVModel(props, 'current', emits)
const pageSize = useVModel(props, 'pageSize', emits) const pageSize = useVModel(props, 'pageSize', emits)
const entityName = computed(() => props.entityName || 'item')
const totalPages = computed(() => Math.max(Math.ceil(total.value / pageSize.value), 1)) const totalPages = computed(() => Math.max(Math.ceil(total.value / pageSize.value), 1))
const { isMobileMode } = useGlobal() const { isMobileMode } = useGlobal()
@ -38,6 +41,7 @@ const goToFirstPage = () => {
<div class="nc-pagination flex flex-row items-center gap-x-2"> <div class="nc-pagination flex flex-row items-center gap-x-2">
<NcButton <NcButton
v-if="!isMobileMode" v-if="!isMobileMode"
v-e="[`a:pagination:${entityName}:first-page`]"
class="first-page" class="first-page"
type="secondary" type="secondary"
size="small" size="small"
@ -47,7 +51,14 @@ const goToFirstPage = () => {
<GeneralIcon icon="doubleLeftArrow" /> <GeneralIcon icon="doubleLeftArrow" />
</NcButton> </NcButton>
<NcButton class="prev-page" type="secondary" size="small" :disabled="current === 1" @click="changePage({ increase: false })"> <NcButton
v-e="[`a:pagination:${entityName}:prev-page`]"
class="prev-page"
type="secondary"
size="small"
:disabled="current === 1"
@click="changePage({ increase: false })"
>
<GeneralIcon icon="arrowLeft" /> <GeneralIcon icon="arrowLeft" />
</NcButton> </NcButton>
<div class="text-gray-600"> <div class="text-gray-600">
@ -59,6 +70,7 @@ const goToFirstPage = () => {
</div> </div>
<NcButton <NcButton
v-e="[`a:pagination:${entityName}:next-page`]"
class="next-page" class="next-page"
type="secondary" type="secondary"
size="small" size="small"
@ -70,6 +82,7 @@ const goToFirstPage = () => {
<NcButton <NcButton
v-if="!isMobileMode" v-if="!isMobileMode"
v-e="[`a:pagination:${entityName}:last-page`]"
class="last-page" class="last-page"
type="secondary" type="secondary"
size="small" size="small"

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

@ -27,6 +27,10 @@ const props = defineProps<{
} }
} }
.ant-tabs-tab + .ant-tabs-tab {
@apply ml-4;
}
.nc-tabs { .nc-tabs {
.ant-tabs-tab { .ant-tabs-tab {
@apply px-2 text-gray-600 !hover:text-gray-800; @apply px-2 text-gray-600 !hover:text-gray-800;

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

@ -174,7 +174,7 @@ onMounted(async () => {
v-else-if="!collaborators?.length" v-else-if="!collaborators?.length"
class="nc-collaborators-list w-full h-full flex flex-col items-center justify-center mt-36" class="nc-collaborators-list w-full h-full flex flex-col items-center justify-center mt-36"
> >
<Empty :description="$t('title.noMembersFound')" /> <!-- <Empty :description="$t('title.noMembersFound')" /> -->
</div> </div>
<div v-else class="nc-collaborators-list nc-scrollbar-md"> <div v-else class="nc-collaborators-list nc-scrollbar-md">
<div class="nc-collaborators-list-header"> <div class="nc-collaborators-list-header">

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

@ -1,11 +1,16 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { BaseType, TableType } from 'nocodb-sdk' import type { BaseType, TableType } from 'nocodb-sdk'
import dayjs from 'dayjs' import dayjs from 'dayjs'
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(useProjects())
const isNewBaseModalOpen = ref(false)
const isDataSourceLimitReached = computed(() => Number(openedProject.value?.bases?.length) > 1)
const { isUIAllowed } = useRoles() const { isUIAllowed } = useRoles()
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
@ -63,19 +68,58 @@ function openTableCreateDialog(baseIndex?: number | undefined) {
close(1000) close(1000)
} }
} }
const onCreateBaseClick = () => {
if (isDataSourceLimitReached.value) return
isNewBaseModalOpen.value = true
}
</script> </script>
<template> <template>
<div class="nc-all-tables-view"> <div class="nc-all-tables-view">
<div v-if="isUIAllowed('tableCreate')" class="flex flex-row gap-x-6 pb-3 pt-6"> <div class="flex flex-row gap-x-6 pb-3 pt-6">
<div class="nc-project-view-all-table-btn" data-testid="proj-view-btn__add-new-table" @click="openTableCreateDialog()"> <div
v-if="isUIAllowed('tableCreate')"
role="button"
class="nc-project-view-all-table-btn"
data-testid="proj-view-btn__add-new-table"
@click="openTableCreateDialog()"
>
<GeneralIcon icon="addOutlineBox" /> <GeneralIcon icon="addOutlineBox" />
<div class="label">{{ $t('general.new') }} {{ $t('objects.table') }}</div> <div class="label">{{ $t('general.new') }} {{ $t('objects.table') }}</div>
</div> </div>
<div class="nc-project-view-all-table-btn" data-testid="proj-view-btn__import-data" @click="isImportModalOpen = true"> <div
v-if="isUIAllowed('tableCreate')"
v-e="['c:table:import']"
role="button"
class="nc-project-view-all-table-btn"
data-testid="proj-view-btn__import-data"
@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')">
<template #title>
<div>
{{ $t('tooltip.reachedSourceLimit') }}
</div>
</template>
<div
v-e="['c:table:create-source']"
role="button"
class="nc-project-view-all-table-btn"
data-testid="proj-view-btn__create-source"
:class="{
disabled: isDataSourceLimitReached,
}"
@click="onCreateBaseClick"
>
<GeneralIcon icon="dataSource" />
<div class="label">{{ $t('labels.connectDataSource') }}</div>
</div>
</component>
</div> </div>
<div class="flex flex-row w-full text-gray-400 border-b-1 border-gray-50 py-3 px-2.5"> <div class="flex flex-row w-full text-gray-400 border-b-1 border-gray-50 py-3 px-2.5">
<div class="w-2/5">{{ $t('objects.table') }}</div> <div class="w-2/5">{{ $t('objects.table') }}</div>
@ -114,12 +158,17 @@ function openTableCreateDialog(baseIndex?: number | undefined) {
</div> </div>
</div> </div>
<ProjectImportModal v-if="defaultBase" v-model:visible="isImportModalOpen" :base="defaultBase" /> <ProjectImportModal v-if="defaultBase" v-model:visible="isImportModalOpen" :base="defaultBase" />
<GeneralModal v-model:visible="isNewBaseModalOpen" size="medium">
<div class="py-6 px-8">
<LazyDashboardSettingsDataSourcesCreateBase @close="isNewBaseModalOpen = false" />
</div>
</GeneralModal>
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.nc-project-view-all-table-btn { .nc-project-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 {
@apply h-10 w-10; @apply h-10 w-10;
@ -129,4 +178,8 @@ function openTableCreateDialog(baseIndex?: number | undefined) {
@apply text-base font-medium; @apply text-base font-medium;
} }
} }
.nc-project-view-all-table-btn.disabled {
@apply bg-gray-50 text-gray-400 hover:(bg-gray-50 text-gray-400) cursor-not-allowed;
}
</style> </style>

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

@ -10,15 +10,19 @@ const { navigateToProjectPage } = useProject()
const router = useRouter() const router = useRouter()
const route = router.currentRoute const route = router.currentRoute
const { $e } = useNuxtApp()
/* const defaultBase = computed(() => { /* const defaultBase = computed(() => {
return openedProject.value?.bases?.[0] return openedProject.value?.bases?.[0]
}) */ }) */
const { isUIAllowed } = useRoles() const { isUIAllowed } = useRoles()
const { isMobileMode } = useGlobal() const { project } = storeToRefs(useProject())
const activeKey = ref<'allTable' | 'collaborator' | 'data-source'>('allTable') const { projectPageTab } = storeToRefs(useConfigStore())
const { isMobileMode } = useGlobal()
const baseSettingsState = ref('') const baseSettingsState = ref('')
@ -27,21 +31,27 @@ watch(
(newVal, oldVal) => { (newVal, oldVal) => {
if (newVal && newVal !== oldVal) { if (newVal && newVal !== oldVal) {
if (newVal === 'collaborator') { if (newVal === 'collaborator') {
activeKey.value = 'collaborator' projectPageTab.value = 'collaborator'
} else if (newVal === 'data-source') { } else if (newVal === 'data-source') {
activeKey.value = 'data-source' projectPageTab.value = 'data-source'
} else { } else {
activeKey.value = 'allTable' projectPageTab.value = 'allTable'
} }
return
} }
projectPageTab.value = 'allTable'
}, },
{ immediate: true }, { immediate: true },
) )
watch(activeKey, () => { watch(projectPageTab, () => {
if (activeKey.value) { $e(`a:project:view:tab-change:${projectPageTab.value}`)
if (projectPageTab.value) {
navigateToProjectPage({ navigateToProjectPage({
page: activeKey.value as any, page: projectPageTab.value as any,
}) })
} }
}) })
@ -75,17 +85,17 @@ watch(
height: 'calc(100% - var(--topbar-height))', height: 'calc(100% - var(--topbar-height))',
}" }"
> >
<a-tabs v-model:activeKey="activeKey" class="w-full"> <a-tabs v-model:activeKey="projectPageTab" class="w-full">
<a-tab-pane key="allTable"> <a-tab-pane key="allTable">
<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">
<NcLayout /> <NcLayout />
<div>{{ $t('labels.allTables') }}</div> <div>{{ $t('labels.allTables') }}</div>
<div <div
class="flex pl-1.25 px-1.5 py-0.75 rounded-md text-xs" class="tab-info"
:class="{ :class="{
'bg-primary-selected': activeKey === 'allTable', 'bg-primary-selected': projectPageTab === 'allTable',
'bg-gray-50': activeKey !== 'allTable', 'bg-gray-50': projectPageTab !== 'allTable',
}" }"
> >
{{ activeTables.length }} {{ activeTables.length }}
@ -111,6 +121,16 @@ watch(
<div class="tab-title" data-testid="proj-view-tab__data-sources"> <div class="tab-title" data-testid="proj-view-tab__data-sources">
<GeneralIcon icon="database" /> <GeneralIcon icon="database" />
<div>{{ $t('labels.dataSources') }}</div> <div>{{ $t('labels.dataSources') }}</div>
<div
v-if="project.bases?.length"
class="tab-info"
:class="{
'bg-primary-selected': projectPageTab === 'data-source',
'bg-gray-50': projectPageTab !== 'data-source',
}"
>
{{ project.bases.length - 1 }}
</div>
</div> </div>
</template> </template>
<DashboardSettingsDataSources v-model:state="baseSettingsState" /> <DashboardSettingsDataSources v-model:state="baseSettingsState" />
@ -137,4 +157,8 @@ watch(
:deep(.ant-tabs-tab-active .tab-title) { :deep(.ant-tabs-tab-active .tab-title) {
@apply text-primary; @apply text-primary;
} }
.tab-info {
@apply flex pl-1.25 px-1.5 py-0.75 rounded-md text-xs;
}
</style> </style>

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

@ -30,7 +30,14 @@ const sizeRef = toRef(props, 'size')
<template #overlay> <template #overlay>
<div class="nc-role-select-dropdown flex flex-col gap-1 p-2"> <div class="nc-role-select-dropdown flex flex-col gap-1 p-2">
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<div v-for="rl in props.roles" :key="rl" :value="rl" :selected="rl === roleRef" @click="props.onRoleChange(rl)"> <div
v-for="rl in props.roles"
:key="rl"
v-e="['c:workspace:settings:user-role-change']"
:value="rl"
:selected="rl === roleRef"
@click="props.onRoleChange(rl)"
>
<div <div
class="flex flex-col py-1.5 rounded-lg px-2 gap-1 bg-transparent cursor-pointer hover:bg-gray-100" class="flex flex-col py-1.5 rounded-lg px-2 gap-1 bg-transparent cursor-pointer hover:bg-gray-100"
:class="{ :class="{

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

@ -2,6 +2,10 @@
const { openedViewsTab } = storeToRefs(useViewsStore()) const { openedViewsTab } = storeToRefs(useViewsStore())
const { onViewsTabChange } = useViewsStore() const { onViewsTabChange } = useViewsStore()
const { isLeftSidebarOpen } = storeToRefs(useSidebarStore())
const { $e } = useNuxtApp()
const { isUIAllowed } = useRoles() const { isUIAllowed } = useRoles()
const openedSubTab = computed({ const openedSubTab = computed({
@ -22,6 +26,8 @@ watch(
if (openedSubTab.value === 'webhook' && !isUIAllowed('hookList')) { if (openedSubTab.value === 'webhook' && !isUIAllowed('hookList')) {
onViewsTabChange('relation') onViewsTabChange('relation')
} }
$e(`c:table:tab-open:${openedSubTab.value}`)
}, },
{ {
immediate: true, immediate: true,
@ -30,8 +36,14 @@ watch(
</script> </script>
<template> <template>
<div class="flex flex-col h-full w-full" data-testid="nc-details-wrapper"> <div
<NcTabs v-model="openedSubTab" centered> class="flex flex-col h-full w-full"
data-testid="nc-details-wrapper"
:class="{
'nc-details-tab-left-sidebar-close': !isLeftSidebarOpen,
}"
>
<NcTabs v-model:activeKey="openedSubTab" centered class="nc-details-tab">
<a-tab-pane v-if="isUIAllowed('fieldAdd')" key="field"> <a-tab-pane v-if="isUIAllowed('fieldAdd')" key="field">
<template #tab> <template #tab>
<div class="tab" data-testid="nc-fields-tab"> <div class="tab" data-testid="nc-fields-tab">
@ -79,15 +91,25 @@ watch(
@apply flex flex-row items-center gap-x-1.5 pr-0.5; @apply flex flex-row items-center gap-x-1.5 pr-0.5;
} }
:deep(.nc-tabs.centered) { :deep(.ant-tabs-nav) {
min-height: calc(var(--topbar-height) - 1.75px);
}
</style>
<style lang="scss">
.nc-details-tab.nc-tabs.centered {
> .ant-tabs-nav { > .ant-tabs-nav {
.ant-tabs-nav-wrap { .ant-tabs-nav-wrap {
@apply absolute mx-auto -left-1/8 right-0; @apply absolute mx-auto -left-9.5;
} }
} }
} }
:deep(.ant-tabs-nav) { .nc-details-tab-left-sidebar-close > .nc-details-tab.nc-tabs.centered {
min-height: calc(var(--topbar-height) - 1.75px); > .ant-tabs-nav {
.ant-tabs-nav-wrap {
@apply absolute mx-auto left-0;
}
}
} }
</style> </style>

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

@ -94,6 +94,7 @@ const isRTLLanguage = computed(() => isRtlLang(locale.value as keyof typeof Lang
class="xs:(mr-2)" class="xs:(mr-2)"
:class="{ 'rtl-pagination': isRTLLanguage }" :class="{ 'rtl-pagination': isRTLLanguage }"
:total="+count" :total="+count"
entity-name="grid"
/> />
<div v-else class="mx-auto flex items-center mt-n1" style="max-width: 250px"> <div v-else class="mx-auto flex items-center mt-n1" style="max-width: 250px">
<span class="text-xs" style="white-space: nowrap"> Change page:</span> <span class="text-xs" style="white-space: nowrap"> Change page:</span>

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

@ -14,6 +14,8 @@ const { isLeftSidebarOpen } = storeToRefs(useSidebarStore())
const { isMobileMode } = storeToRefs(useConfigStore()) const { isMobileMode } = storeToRefs(useConfigStore())
const { appInfo } = useGlobal()
const isSharedBase = computed(() => route.value.params.typeOrId === 'base') const isSharedBase = computed(() => route.value.params.typeOrId === 'base')
</script> </script>
@ -29,18 +31,10 @@ const isSharedBase = computed(() => route.value.params.typeOrId === 'base')
<GeneralOpenLeftSidebarBtn /> <GeneralOpenLeftSidebarBtn />
<LazySmartsheetToolbarViewInfo v-if="!isPublic" /> <LazySmartsheetToolbarViewInfo v-if="!isPublic" />
<div class="flex-1" /> <div v-if="!isSharedBase && !isMobileMode" class="w-47.5">
<div
v-if="!isSharedBase && !isMobileMode"
class="absolute mx-auto transition-all duration-150 right-0 w-47.5"
:class="{
'-left-1/10': isLeftSidebarOpen,
'-left-0': !isLeftSidebarOpen,
}"
>
<SmartsheetTopbarSelectMode /> <SmartsheetTopbarSelectMode />
</div> </div>
<div class="flex-1" />
<GeneralApiLoader v-if="!isMobileMode" /> <GeneralApiLoader v-if="!isMobileMode" />
@ -50,7 +44,7 @@ const isSharedBase = computed(() => route.value.params.typeOrId === 'base')
/> />
<LazyGeneralLanguage <LazyGeneralLanguage
v-if="isSharedBase" v-if="isSharedBase && !appInfo.ee"
class="cursor-pointer text-lg hover:(text-black bg-gray-200) mr-0 p-1.5 rounded-md" class="cursor-pointer text-lg hover:(text-black bg-gray-200) mr-0 p-1.5 rounded-md"
/> />
</template> </template>

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

@ -115,6 +115,7 @@ const processedAudit = (log: string) => {
<div class="h-16 bg-white rounded-t-lg border-gray-200 border-b-1"> <div class="h-16 bg-white rounded-t-lg border-gray-200 border-b-1">
<div class="flex flex-row gap-2 m-2 p-1 bg-gray-100 rounded-lg"> <div class="flex flex-row gap-2 m-2 p-1 bg-gray-100 rounded-lg">
<div <div
v-e="['c:row-expand:comment']"
class="tab flex-1 px-4 py-2 transition-all text-gray-600 cursor-pointer rounded-lg" class="tab flex-1 px-4 py-2 transition-all text-gray-600 cursor-pointer rounded-lg"
:class="{ :class="{
'bg-white shadow !text-brand-500 !hover:text-brand-500': tab === 'comments', 'bg-white shadow !text-brand-500 !hover:text-brand-500': tab === 'comments',
@ -127,6 +128,7 @@ const processedAudit = (log: string) => {
</div> </div>
</div> </div>
<div <div
v-e="['c:row-expand:audit']"
class="tab flex-1 px-4 py-2 transition-all text-gray-600 cursor-pointer rounded-lg" class="tab flex-1 px-4 py-2 transition-all text-gray-600 cursor-pointer rounded-lg"
:class="{ :class="{
'bg-white shadow !text-brand-500 !hover:text-brand-500': tab === 'audits', 'bg-white shadow !text-brand-500 !hover:text-brand-500': tab === 'audits',
@ -171,6 +173,7 @@ const processedAudit = (log: string) => {
</div> </div>
<NcButton <NcButton
v-if="log.user === user!.email && !editLog && !appInfo.ee" v-if="log.user === user!.email && !editLog && !appInfo.ee"
v-e="['c:row-expand:comment:edit']"
type="secondary" type="secondary"
class="!px-2 opacity-0 group-hover:opacity-100 transition-all" class="!px-2 opacity-0 group-hover:opacity-100 transition-all"
size="sm" size="sm"
@ -192,7 +195,7 @@ const processedAudit = (log: string) => {
</div> </div>
<div v-if="log.id === editLog?.id" class="flex justify-end gap-1"> <div v-if="log.id === editLog?.id" class="flex justify-end gap-1">
<NcButton type="secondary" size="sm" @click="onCancel"> Cancel </NcButton> <NcButton type="secondary" size="sm" @click="onCancel"> Cancel </NcButton>
<NcButton size="sm" @click="onEditComment"> Save </NcButton> <NcButton v-e="['a:row-expand:comment:save']" size="sm" @click="onEditComment"> Save </NcButton>
</div> </div>
</div> </div>
</div> </div>
@ -209,7 +212,13 @@ const processedAudit = (log: string) => {
@keyup.enter.prevent="saveComment" @keyup.enter.prevent="saveComment"
> >
</a-input> </a-input>
<NcButton size="medium" class="!w-8" :disabled="!comment.length" @click="saveComment"> <NcButton
v-e="['a:row-expand:comment:save']"
size="medium"
class="!w-8"
:disabled="!comment.length"
@click="saveComment"
>
<GeneralIcon icon="send" /> <GeneralIcon icon="send" />
</NcButton> </NcButton>
</div> </div>

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

@ -351,6 +351,10 @@ watch(
immediate: true, immediate: true,
}, },
) )
const showRightSections = computed(() => {
return !isNew.value && commentsDrawer.value && isUIAllowed('commentList')
})
</script> </script>
<script lang="ts"> <script lang="ts">
@ -455,20 +459,38 @@ export default {
</template> </template>
<template v-else> <template v-else>
<div class="flex flex-row w-full"> <div class="flex flex-row w-full">
<NcButton v-if="props.showNextPrevIcons" type="secondary" class="nc-prev-arrow !w-10" @click="$emit('prev')"> <NcButton
v-if="props.showNextPrevIcons"
v-e="['c:row-expand:prev']"
type="secondary"
class="nc-prev-arrow !w-10"
@click="$emit('prev')"
>
<GeneralIcon icon="arrowLeft" class="text-lg text-gray-700" /> <GeneralIcon icon="arrowLeft" class="text-lg text-gray-700" />
</NcButton> </NcButton>
<div class="flex flex-grow justify-center items-center font-semibold text-lg"> <div class="flex flex-grow justify-center items-center font-semibold text-lg">
<div>{{ meta.title }}</div> <div>{{ meta.title }}</div>
</div> </div>
<NcButton v-if="!props.lastRow" type="secondary" class="nc-next-arrow !w-10" @click="onNext"> <NcButton
v-if="!props.lastRow"
v-e="['c:row-expand:next']"
type="secondary"
class="nc-next-arrow !w-10"
@click="onNext"
>
<GeneralIcon icon="arrowRight" class="text-lg text-gray-700" /> <GeneralIcon icon="arrowRight" class="text-lg text-gray-700" />
</NcButton> </NcButton>
</div> </div>
</template> </template>
</div> </div>
<div ref="wrapper" class="flex flex-grow flex-row h-[calc(100%-4rem)] w-full gap-4"> <div ref="wrapper" class="flex flex-grow flex-row h-[calc(100%-4rem)] w-full gap-4">
<div class="flex w-2/3 xs:w-full flex-col border-1 rounded-xl overflow-hidden border-gray-200 xs:(border-0 rounded-none)"> <div
class="flex xs:w-full flex-col border-1 rounded-xl overflow-hidden border-gray-200 xs:(border-0 rounded-none)"
:class="{
'w-full': !showRightSections,
'w-2/3': showRightSections,
}"
>
<div <div
class="flex flex-col flex-grow mt-2 h-full max-h-full nc-scrollbar-md !pb-2 items-center w-full bg-white p-4 xs:p-0" class="flex flex-col flex-grow mt-2 h-full max-h-full nc-scrollbar-md !pb-2 items-center w-full bg-white p-4 xs:p-0"
> >
@ -597,6 +619,7 @@ export default {
<div class="px-1">Close</div> <div class="px-1">Close</div>
</NcButton> </NcButton>
<NcButton <NcButton
v-e="['c:row-expand:save']"
data-testid="nc-expanded-form-save" data-testid="nc-expanded-form-save"
type="primary" type="primary"
size="medium" size="medium"
@ -609,7 +632,7 @@ export default {
</div> </div>
</div> </div>
<div <div
v-if="!isNew && commentsDrawer && isUIAllowed('commentList')" v-if="showRightSections"
class="nc-comments-drawer border-1 relative border-gray-200 w-1/3 max-w-125 bg-gray-50 rounded-xl min-w-0 overflow-hidden h-full xs:hidden" class="nc-comments-drawer border-1 relative border-gray-200 w-1/3 max-w-125 bg-gray-50 rounded-xl min-w-0 overflow-hidden h-full xs:hidden"
:class="{ active: commentsDrawer && isUIAllowed('commentList') }" :class="{ active: commentsDrawer && isUIAllowed('commentList') }"
> >
@ -627,7 +650,8 @@ export default {
</div> </div>
<div class="flex flex-row gap-x-2 mt-4 pt-1.5 justify-end pt-4 gap-x-3"> <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-if="isMobileMode" type="secondary" @click="showDeleteRowModal = false">{{ $t('general.cancel') }} </NcButton>
<NcButton @click="onConfirmDeleteRowClick">{{ $t('general.confirm') }} </NcButton>
<NcButton v-e="['a:row-expand:delete']" @click="onConfirmDeleteRowClick">{{ $t('general.confirm') }} </NcButton>
</div> </div>
</NcModal> </NcModal>
</template> </template>

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

@ -1388,6 +1388,7 @@ const handleCellClick = (event: MouseEvent, row: number, col: number) => {
<template v-else-if="!isLocked"> <template v-else-if="!isLocked">
<span <span
v-if="row.rowMeta?.commentCount && expandForm" v-if="row.rowMeta?.commentCount && expandForm"
v-e="['c:expanded-form:open']"
class="py-1 px-3 rounded-full text-xs cursor-pointer select-none transform hover:(scale-110)" class="py-1 px-3 rounded-full text-xs cursor-pointer select-none transform hover:(scale-110)"
:style="{ backgroundColor: enumColor.light[row.rowMeta.commentCount % enumColor.light.length] }" :style="{ backgroundColor: enumColor.light[row.rowMeta.commentCount % enumColor.light.length] }"
@click="expandAndLooseFocus(row, state)" @click="expandAndLooseFocus(row, state)"
@ -1401,7 +1402,7 @@ const handleCellClick = (event: MouseEvent, row: number, col: number) => {
<component <component
:is="iconMap.expand" :is="iconMap.expand"
v-if="expandForm" v-if="expandForm"
v-e="['c:row-expand']" v-e="['c:row-expand:open']"
class="select-none transform hover:(text-black scale-120) nc-row-expand" class="select-none transform hover:(text-black scale-120) nc-row-expand"
@click="expandAndLooseFocus(row, state)" @click="expandAndLooseFocus(row, state)"
/> />
@ -1627,11 +1628,18 @@ const handleCellClick = (event: MouseEvent, row: number, col: number) => {
> >
<template #add-record> <template #add-record>
<div v-if="isAddingEmptyRowAllowed" class="flex ml-1"> <div v-if="isAddingEmptyRowAllowed" class="flex ml-1">
<NcButton v-if="isMobileMode" class="nc-grid-add-new-row" type="secondary" @click="onNewRecordToFormClick()"> <NcButton
v-if="isMobileMode"
v-e="[isAddNewRecordGridMode ? 'c:row:add:grid' : 'c:row:add:form']"
class="nc-grid-add-new-row"
type="secondary"
@click="onNewRecordToFormClick()"
>
{{ $t('activity.newRecord') }} {{ $t('activity.newRecord') }}
</NcButton> </NcButton>
<a-dropdown-button <a-dropdown-button
v-else v-else
v-e="[isAddNewRecordGridMode ? 'c:row:add:grid:toggle' : 'c:row:add:form:toggle']"
class="nc-grid-add-new-row" class="nc-grid-add-new-row"
placement="top" placement="top"
@click="isAddNewRecordGridMode ? addEmptyRow() : onNewRecordToFormClick()" @click="isAddNewRecordGridMode ? addEmptyRow() : onNewRecordToFormClick()"
@ -1654,7 +1662,7 @@ const handleCellClick = (event: MouseEvent, row: number, col: number) => {
}" }"
> >
<div <div
v-e="['c:row:add:grid-top']" v-e="['c:row:add:grid']"
:class="{ 'group': !isLocked, 'disabled-ring': isLocked }" :class="{ 'group': !isLocked, 'disabled-ring': isLocked }"
class="px-4 py-3 flex flex-col select-none gap-y-2 cursor-pointer hover:bg-gray-100 text-gray-600 nc-new-record-with-grid" class="px-4 py-3 flex flex-col select-none gap-y-2 cursor-pointer hover:bg-gray-100 text-gray-600 nc-new-record-with-grid"
@click="onNewRecordToGridClick" @click="onNewRecordToGridClick"
@ -1671,7 +1679,7 @@ const handleCellClick = (event: MouseEvent, row: number, col: number) => {
<div class="flex flex-row text-xs text-gray-400 ml-7.25">{{ $t('labels.addRowGrid') }}</div> <div class="flex flex-row text-xs text-gray-400 ml-7.25">{{ $t('labels.addRowGrid') }}</div>
</div> </div>
<div <div
v-e="['c:row:add:expanded-form']" v-e="['c:row:add:form']"
:class="{ 'group': !isLocked, 'disabled-ring': isLocked }" :class="{ 'group': !isLocked, 'disabled-ring': isLocked }"
class="px-4 py-3 flex flex-col select-none gap-y-2 cursor-pointer hover:bg-gray-100 text-gray-600 nc-new-record-with-form" class="px-4 py-3 flex flex-col select-none gap-y-2 cursor-pointer hover:bg-gray-100 text-gray-600 nc-new-record-with-form"
@click="onNewRecordToFormClick" @click="onNewRecordToFormClick"

10
packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue

@ -325,6 +325,7 @@ onMounted(async () => {
<div v-else :key="`${i}nested`" class="flex nc-filter-logical-op"> <div v-else :key="`${i}nested`" class="flex nc-filter-logical-op">
<NcSelect <NcSelect
v-model:value="filter.logical_op" v-model:value="filter.logical_op"
v-e="['c:filter:logical-op:select']"
:dropdown-match-select-width="false" :dropdown-match-select-width="false"
class="min-w-20 capitalize" class="min-w-20 capitalize"
placeholder="Group op" placeholder="Group op"
@ -342,6 +343,7 @@ onMounted(async () => {
<NcButton <NcButton
v-if="!filter.readOnly" v-if="!filter.readOnly"
:key="i" :key="i"
v-e="['c:filter:delete']"
type="text" type="text"
size="small" size="small"
class="nc-filter-item-remove-btn cursor-pointer" class="nc-filter-item-remove-btn cursor-pointer"
@ -370,6 +372,7 @@ onMounted(async () => {
<NcSelect <NcSelect
v-else v-else
v-model:value="filter.logical_op" v-model:value="filter.logical_op"
v-e="['c:filter:logical-op:select']"
:dropdown-match-select-width="false" :dropdown-match-select-width="false"
class="h-full !min-w-20 !max-w-20 capitalize" class="h-full !min-w-20 !max-w-20 capitalize"
hide-details hide-details
@ -395,6 +398,7 @@ onMounted(async () => {
/> />
<NcSelect <NcSelect
v-model:value="filter.comparison_op" v-model:value="filter.comparison_op"
v-e="['c:filter:comparison-op:select']"
:dropdown-match-select-width="false" :dropdown-match-select-width="false"
class="caption nc-filter-operation-select !min-w-26.75 !max-w-26.75 max-h-8" class="caption nc-filter-operation-select !min-w-26.75 !max-w-26.75 max-h-8"
:placeholder="$t('labels.operation')" :placeholder="$t('labels.operation')"
@ -416,6 +420,7 @@ onMounted(async () => {
<NcSelect <NcSelect
v-else-if="[UITypes.Date, UITypes.DateTime].includes(getColumn(filter)?.uidt)" v-else-if="[UITypes.Date, UITypes.DateTime].includes(getColumn(filter)?.uidt)"
v-model:value="filter.comparison_sub_op" v-model:value="filter.comparison_sub_op"
v-e="['c:filter:sub-comparison-op:select']"
:dropdown-match-select-width="false" :dropdown-match-select-width="false"
class="caption nc-filter-sub_operation-select min-w-28" class="caption nc-filter-sub_operation-select min-w-28"
:class="{ 'flex-grow w-full': !showFilterInput(filter), 'max-w-28': showFilterInput(filter) }" :class="{ 'flex-grow w-full': !showFilterInput(filter), 'max-w-28': showFilterInput(filter) }"
@ -453,6 +458,7 @@ onMounted(async () => {
<NcButton <NcButton
v-if="!filter.readOnly" v-if="!filter.readOnly"
v-e="['c:filter:delete']"
type="text" type="text"
size="small" size="small"
class="nc-filter-item-remove-btn self-center" class="nc-filter-item-remove-btn self-center"
@ -508,4 +514,8 @@ onMounted(async () => {
:deep(.ant-select-item-option) { :deep(.ant-select-item-option) {
@apply "!min-w-full"; @apply "!min-w-full";
} }
:deep(.ant-select-selector) {
@apply !min-h-8.25;
}
</style> </style>

1
packages/nc-gui/components/smartsheet/toolbar/CreateSort.vue

@ -96,6 +96,7 @@ const onArrowUp = () => {
<div <div
v-for="(option, index) in options" v-for="(option, index) in options"
:key="index" :key="index"
v-e="['c:sort:add:column:select']"
class="flex flex-row h-10 items-center gap-x-1.5 px-2.5 hover:bg-gray-100 cursor-pointer nc-sort-column-search-item" class="flex flex-row h-10 items-center gap-x-1.5 px-2.5 hover:bg-gray-100 cursor-pointer nc-sort-column-search-item"
:class="{ :class="{
'bg-gray-100': activeFieldIndex === index, 'bg-gray-100': activeFieldIndex === index,

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

@ -100,7 +100,7 @@ const exportFile = async (exportType: ExportTypes) => {
<template> <template>
<a-menu-item> <a-menu-item>
<div v-e="['a:actions:download-csv']" class="nc-project-menu-item" @click="exportFile(ExportTypes.CSV)"> <div v-e="['a:download:csv']" class="nc-project-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:actions:download-excel']" class="nc-project-menu-item" @click="exportFile(ExportTypes.EXCEL)"> <div v-e="['a:download:excel']" class="nc-project-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') }}

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

@ -363,7 +363,7 @@ useMenuCloseOnEsc(open)
.includes(field) .includes(field)
" "
:key="field.id" :key="field.id"
class="px-2 py-2 flex flex-row items-center first:border-t-1 border-b-1 border-x-1 first:rounded-t-md last:rounded-b-md border-gray-200" class="px-2 py-2 flex flex-row items-center first:border-t-1 border-b-1 border-x-1 first:rounded-t-lg last:rounded-b-lg border-gray-200"
:data-testid="`nc-fields-menu-${field.title}`" :data-testid="`nc-fields-menu-${field.title}`"
@click.stop @click.stop
> >
@ -393,8 +393,11 @@ useMenuCloseOnEsc(open)
<div <div
v-if="gridDisplayValueField && filteredFieldList[0].title.toLowerCase().includes(filterQuery.toLowerCase())" v-if="gridDisplayValueField && filteredFieldList[0].title.toLowerCase().includes(filterQuery.toLowerCase())"
:key="`pv-${gridDisplayValueField.id}`" :key="`pv-${gridDisplayValueField.id}`"
class="pl-7.5 pr-2.1 py-1.9 flex flex-row items-center border-1 rounded-t-lg border-gray-200" class="pl-7.5 pr-2.1 py-2 flex flex-row items-center border-1 border-gray-200"
:class="{ 'rounded-b-md': filteredFieldList.length === 1 }" :class="{
'rounded-t-lg': filteredFieldList.length > 1,
'rounded-lg': filteredFieldList.length === 1,
}"
:data-testid="`nc-fields-menu-${gridDisplayValueField.title}`" :data-testid="`nc-fields-menu-${gridDisplayValueField.title}`"
@click.stop @click.stop
> >

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

@ -205,7 +205,7 @@ provide(IsFormInj, ref(true))
/> />
<div <div
v-else v-else
class="bg-white border-1 flex flex-grow min-h-32px h-full items-center" class="bg-white border-1 flex flex-grow min-h-4 h-full items-center nc-filter-input-wrapper !rounded-lg"
:class="{ 'px-2': hasExtraPadding, 'border-brand-500': isInputBoxOnFocus }" :class="{ 'px-2': hasExtraPadding, 'border-brand-500': isInputBoxOnFocus }"
@mouseup.stop @mouseup.stop
> >
@ -223,3 +223,13 @@ provide(IsFormInj, ref(true))
/> />
</div> </div>
</template> </template>
<style lang="scss" scoped>
:deep(input) {
@apply py-1.5;
}
:deep(.ant-picker) {
@apply !py-0;
}
</style>

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

@ -215,7 +215,7 @@ onMounted(async () => {
offset-y offset-y
:trigger="['click']" :trigger="['click']"
class="!xs:hidden" class="!xs:hidden"
overlay-class-name="nc-dropdown-group-by-menu nc-toolbar-dropdown" overlay-class-name="nc-dropdown-group-by-menu nc-toolbar-dropdown overflow-hidden"
> >
<div :class="{ 'nc-active-btn': groupedByColumnIds?.length }"> <div :class="{ 'nc-active-btn': groupedByColumnIds?.length }">
<a-button v-e="['c:group-by']" class="nc-group-by-menu-btn nc-toolbar-btn" :disabled="isLocked"> <a-button v-e="['c:group-by']" class="nc-group-by-menu-btn nc-toolbar-btn" :disabled="isLocked">
@ -234,7 +234,7 @@ onMounted(async () => {
<template #overlay> <template #overlay>
<div <div
:class="{ ' min-w-[400px]': _groupBy.length }" :class="{ ' min-w-[400px]': _groupBy.length }"
class="flex flex-col bg-white rounded-md overflow-auto menu-filter-dropdown max-h-[max(80vh,500px)] py-6 pl-6" class="flex flex-col bg-white overflow-auto menu-filter-dropdown max-h-[max(80vh,500px)] py-6 pl-6"
data-testid="nc-group-by-menu" data-testid="nc-group-by-menu"
> >
<div class="group-by-grid pb-1 mb-2 max-h-100 nc-scrollbar-md pr-5" @click.stop> <div class="group-by-grid pb-1 mb-2 max-h-100 nc-scrollbar-md pr-5" @click.stop>

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

@ -95,12 +95,12 @@ watch(columns, () => {
@click="isDropdownOpen = !isDropdownOpen" @click="isDropdownOpen = !isDropdownOpen"
> >
<GeneralIcon icon="search" class="ml-1 mr-2 h-3.5 w-3.5 text-gray-500 group-hover:text-black" /> <GeneralIcon icon="search" class="ml-1 mr-2 h-3.5 w-3.5 text-gray-500 group-hover:text-black" />
<div v-if="!isMobileMode" class="w-16 group-hover:w-12 text-[0.75rem] font-medium text-gray-400 truncate"> <div v-if="!isMobileMode" class="w-16 text-[0.75rem] font-medium text-gray-400 truncate">
{{ displayColumnLabel }} {{ displayColumnLabel }}
</div> </div>
<div <div
:class="{ :class="{
'hidden group-hover:block': !isMobileMode, 'opacity-0 group-hover:opacity-100': !isMobileMode,
'text-gray-700': isMobileMode, 'text-gray-700': isMobileMode,
}" }"
> >
@ -115,6 +115,7 @@ watch(columns, () => {
</div> </div>
<a-select <a-select
v-model:value="search.field" v-model:value="search.field"
v-e="['c:search:field:select:open']"
:open="isDropdownOpen" :open="isDropdownOpen"
size="small" size="small"
:dropdown-match-select-width="false" :dropdown-match-select-width="false"
@ -122,7 +123,7 @@ watch(columns, () => {
class="py-1 !absolute top-0 left-0 w-full h-full z-10 text-xs opacity-0" class="py-1 !absolute top-0 left-0 w-full h-full z-10 text-xs opacity-0"
@change="onPressEnter" @change="onPressEnter"
> >
<a-select-option v-for="op of columns" :key="op.value" :value="op.value"> <a-select-option v-for="op of columns" :key="op.value" v-e="['c:search:field:select']" :value="op.value">
<div class="text-[0.75rem] flex items-center -ml-1 gap-2"> <div class="text-[0.75rem] flex items-center -ml-1 gap-2">
<SmartsheetHeaderIcon class="text-sm" :column="op.column" /> <SmartsheetHeaderIcon class="text-sm" :column="op.column" />
{{ op.label }} {{ op.label }}
@ -135,7 +136,7 @@ watch(columns, () => {
v-model:value="search.query" v-model:value="search.query"
size="small" size="small"
class="text-xs w-40" class="text-xs w-40"
:placeholder="`${$t('general.searchIn')} ${columns?.find((column) => column.value === search.field)?.label}`" :placeholder="`${$t('general.searchIn')} ${displayColumnLabel}`"
:bordered="false" :bordered="false"
data-testid="search-data-input" data-testid="search-data-input"
@press-enter="onPressEnter" @press-enter="onPressEnter"

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

@ -158,13 +158,20 @@ watch(open, () => {
<a-select-option <a-select-option
v-for="(option, j) of getSortDirectionOptions(getColumnUidtByID(sort.fk_column_id))" v-for="(option, j) of getSortDirectionOptions(getColumnUidtByID(sort.fk_column_id))"
:key="j" :key="j"
v-e="['c:sort:operation:select']"
:value="option.value" :value="option.value"
> >
<span>{{ option.text }}</span> <span>{{ option.text }}</span>
</a-select-option> </a-select-option>
</NcSelect> </NcSelect>
<NcButton type="text" size="small" class="nc-sort-item-remove-btn !max-w-8" @click.stop="deleteSort(sort, i)"> <NcButton
v-e="['c:sort:delete']"
type="text"
size="small"
class="nc-sort-item-remove-btn !max-w-8"
@click.stop="deleteSort(sort, i)"
>
<component :is="iconMap.deleteListItem" /> <component :is="iconMap.deleteListItem" />
</NcButton> </NcButton>
</template> </template>
@ -176,7 +183,7 @@ watch(open, () => {
:trigger="['click']" :trigger="['click']"
overlay-class-name="nc-toolbar-dropdown" overlay-class-name="nc-toolbar-dropdown"
> >
<NcButton class="!text-brand-500" type="text" size="small" @click.stop="showCreateSort = true"> <NcButton v-e="['c:sort:add']" class="!text-brand-500" type="text" size="small" @click.stop="showCreateSort = true">
<div class="flex gap-1 items-center"> <div class="flex gap-1 items-center">
<component :is="iconMap.plus" /> <component :is="iconMap.plus" />
<!-- Add Sort Option --> <!-- Add Sort Option -->

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

@ -132,7 +132,7 @@ useMenuCloseOnEsc(open)
<template v-for="(dialog, type) in quickImportDialogs"> <template v-for="(dialog, type) in quickImportDialogs">
<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:actions:upload-${type}`]" v-e="[`a:upload:${type}`]"
class="nc-project-menu-item" class="nc-project-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:navdraw:preview-as']" class="nc-project-menu-item group"> <div v-e="['c:download']" class="nc-project-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" />

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

@ -5,20 +5,47 @@ const { isMobileMode } = useGlobal()
const { openedViewsTab, activeView } = storeToRefs(useViewsStore()) const { openedViewsTab, activeView } = storeToRefs(useViewsStore())
const { project } = storeToRefs(useProject())
const { activeTable } = storeToRefs(useTablesStore()) const { activeTable } = storeToRefs(useTablesStore())
const { isLeftSidebarOpen } = storeToRefs(useSidebarStore())
</script> </script>
<template> <template>
<div <div
class="flex flex-row font-medium items-center border-gray-50 mt-0.5" class="flex flex-row font-medium items-center border-gray-50 mt-0.5 transition-all duration-100"
:class="{ :class="{
'min-w-2/5 max-w-2/5': !isMobileMode && activeView?.type !== ViewTypes.KANBAN, 'min-w-36/100 max-w-36/100': !isMobileMode && activeView?.type !== ViewTypes.KANBAN && isLeftSidebarOpen,
'min-w-39/100 max-w-39/100': !isMobileMode && activeView?.type !== ViewTypes.KANBAN && !isLeftSidebarOpen,
'min-w-1/4 max-w-1/4': !isMobileMode && activeView?.type === ViewTypes.KANBAN, 'min-w-1/4 max-w-1/4': !isMobileMode && activeView?.type === ViewTypes.KANBAN,
'w-2/3 text-base ml-1.5': isMobileMode, 'w-2/3 text-base ml-1.5': isMobileMode,
}" }"
> >
<template v-if="!isMobileMode">
<NcTooltip class="ml-0.75 max-w-1/4">
<template #title>
{{ project?.title }}
</template>
<div class="flex flex-row items-center gap-x-1.5">
<GeneralProjectIcon
:meta="{ type: project?.type }"
class="!grayscale"
:style="{
filter: 'grayscale(100%) brightness(115%)',
}"
/>
<div class="hidden !2xl:(flex truncate ml-1)">
<span class="truncate text-gray-700">
{{ project?.title }}
</span>
</div>
</div>
</NcTooltip>
<div class="px-1.5 text-gray-500">/</div>
</template>
<template v-if="!(isMobileMode && !activeView?.is_default)"> <template v-if="!(isMobileMode && !activeView?.is_default)">
<LazyGeneralEmojiPicker :emoji="activeTable?.meta?.icon" readonly size="xsmall"> <LazyGeneralEmojiPicker v-if="isMobileMode" :emoji="activeTable?.meta?.icon" readonly size="xsmall">
<template #default> <template #default>
<MdiTable <MdiTable
class="min-w-5" class="min-w-5"
@ -29,35 +56,64 @@ const { activeTable } = storeToRefs(useTablesStore())
/> />
</template> </template>
</LazyGeneralEmojiPicker> </LazyGeneralEmojiPicker>
<NcTooltip
class="truncate nc-active-table-title"
:class="{
'max-w-1/2': isMobileMode || activeView?.is_default,
'max-w-20/100': !isMobileMode && !activeView?.is_default,
}"
>
<template #title>
{{ activeTable?.title }}
</template>
<span <span
class="text-ellipsis overflow-hidden pl-1 text-gray-500" class="text-ellipsis overflow-hidden text-gray-500 xs:ml-2"
:class="{ :class="{
'text-gray-500': !isMobileMode, 'text-gray-500': !isMobileMode,
'text-gray-700 max-w-1/2': isMobileMode, 'text-gray-700 font-medium': isMobileMode || activeView?.is_default,
}"
:style="{
wordBreak: 'keep-all',
whiteSpace: 'nowrap',
display: 'inline',
}" }"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
> >
{{ activeTable?.title }} {{ activeTable?.title }}
</span> </span>
</NcTooltip>
</template> </template>
<div v-if="!isMobileMode" class="px-2 text-gray-300">/</div> <div v-if="!isMobileMode" class="px-1 text-gray-500">/</div>
<template v-if="!(isMobileMode && activeView?.is_default)"> <template v-if="!(isMobileMode && activeView?.is_default)">
<LazyGeneralEmojiPicker :emoji="activeView?.meta?.icon" readonly size="xsmall"> <LazyGeneralEmojiPicker v-if="isMobileMode" :emoji="activeView?.meta?.icon" readonly size="xsmall">
<template #default> <template #default>
<GeneralViewIcon :meta="{ type: activeView?.type }" class="min-w-4.5 text-lg flex" /> <GeneralViewIcon :meta="{ type: activeView?.type }" class="min-w-4.5 text-lg flex" />
</template> </template>
</LazyGeneralEmojiPicker> </LazyGeneralEmojiPicker>
<NcTooltip
class="truncate nc-active-view-title"
:class="{
'max-w-2/5': !isMobileMode && activeView?.is_default,
'max-w-3/5': !isMobileMode && !activeView?.is_default,
'max-w-1/2': isMobileMode,
}"
>
<template #title>
{{ activeView?.is_default ? $t('title.defaultView') : activeView?.title }}
</template>
<span <span
class="truncate pl-1.25 text-gray-700" class="truncate xs:pl-1.25"
:class="{ :class="{
'max-w-28/100': !isMobileMode, 'max-w-28/100': !isMobileMode,
'text-gray-500': activeView?.is_default,
'text-gray-800 font-medium': !activeView?.is_default,
}" }"
> >
{{ activeView?.is_default ? $t('title.defaultView') : activeView?.title }} {{ activeView?.is_default ? $t('title.defaultView') : activeView?.title }}
</span> </span>
</NcTooltip>
</template> </template>
<LazySmartsheetToolbarReload v-if="openedViewsTab === 'view' && !isMobileMode" /> <LazySmartsheetToolbarReload v-if="openedViewsTab === 'view' && !isMobileMode" />

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

@ -9,6 +9,7 @@ const { onViewsTabChange } = useViewsStore()
<template> <template>
<div class="flex flex-row p-1 mx-3 mt-3 mb-3 bg-gray-100 rounded-lg gap-x-0.5 nc-view-sidebar-tab"> <div class="flex flex-row p-1 mx-3 mt-3 mb-3 bg-gray-100 rounded-lg gap-x-0.5 nc-view-sidebar-tab">
<div <div
v-e="['c:project:mode:data']"
class="tab" class="tab"
:class="{ :class="{
active: openedViewsTab === 'view', active: openedViewsTab === 'view',
@ -20,6 +21,7 @@ const { onViewsTabChange } = useViewsStore()
<div class="tab-title nc-tab">{{ $t('general.data') }}</div> <div class="tab-title nc-tab">{{ $t('general.data') }}</div>
</div> </div>
<div <div
v-e="['c:project:mode:details']"
class="tab" class="tab"
:class="{ :class="{
active: openedViewsTab !== 'view', active: openedViewsTab !== 'view',

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

@ -98,6 +98,7 @@ const localCellValue = computed<any[]>(() => {
<div class="block flex-shrink truncate"> <div class="block flex-shrink truncate">
<component <component
:is="isLocked || isUnderLookup ? 'span' : 'a'" :is="isLocked || isUnderLookup ? 'span' : 'a'"
v-e="['c:cell:links:modal:open']"
:title="textVal" :title="textVal"
class="text-center nc-datatype-link underline-transparent" class="text-center nc-datatype-link underline-transparent"
:class="{ '!text-gray-300': !textVal }" :class="{ '!text-gray-300': !textVal }"

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

@ -61,6 +61,7 @@ export default {
<template> <template>
<div <div
v-e="['c:row-expand:open']"
class="chip group mr-1 my-1 flex items-center rounded-[2px] flex-row" class="chip group mr-1 my-1 flex items-center rounded-[2px] flex-row"
:class="{ active, 'border-1 py-1 px-2': isAttachment(column) }" :class="{ active, 'border-1 py-1 px-2': isAttachment(column) }"
@click="openExpandedForm" @click="openExpandedForm"

2
packages/nc-gui/components/virtual-cell/components/ListChildItems.vue

@ -247,6 +247,7 @@ onKeyStroke('Escape', () => {
</p> </p>
<NcButton <NcButton
v-if="!readonly && childrenListCount < 1" v-if="!readonly && childrenListCount < 1"
v-e="['c:links:link']"
data-testid="nc-child-list-button-link-to" data-testid="nc-child-list-button-link-to"
@click="emit('attachRecord')" @click="emit('attachRecord')"
> >
@ -283,6 +284,7 @@ onKeyStroke('Escape', () => {
<NcButton v-if="!isForm" type="ghost" class="nc-close-btn" @click="vModel = false"> {{ $t('general.finish') }} </NcButton> <NcButton v-if="!isForm" type="ghost" class="nc-close-btn" @click="vModel = false"> {{ $t('general.finish') }} </NcButton>
<NcButton <NcButton
v-if="!readonly && childrenListCount > 0" v-if="!readonly && childrenListCount > 0"
v-e="['c:links:link']"
data-testid="nc-child-list-button-link-to" data-testid="nc-child-list-button-link-to"
@click="emit('attachRecord')" @click="emit('attachRecord')"
> >

1
packages/nc-gui/components/virtual-cell/components/ListItem.vue

@ -137,6 +137,7 @@ const attachments: ComputedRef<Attachment[]> = computed(() => {
</div> </div>
<NcButton <NcButton
v-if="!isForm && !isPublic" v-if="!isForm && !isPublic"
v-e="['c:row-expand:open']"
type="text" type="text"
size="lg" size="lg"
class="!px-2 nc-expand-item !group-hover:block !hidden !border-1 !shadow-sm !border-gray-200 !bg-white !absolute right-3 bottom-3" class="!px-2 nc-expand-item !group-hover:block !hidden !border-1 !shadow-sm !border-gray-200 !bg-white !absolute right-3 bottom-3"

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

@ -25,6 +25,8 @@ const injectedColumn = inject(ColumnInj)
const filterQueryRef = ref() const filterQueryRef = ref()
const { $e } = useNuxtApp()
const { const {
childrenExcludedList, childrenExcludedList,
isChildrenExcludedListLinked, isChildrenExcludedListLinked,
@ -58,6 +60,8 @@ const linkRow = async (row: Record<string, any>, id: number) => {
addLTARRef(row, injectedColumn?.value as ColumnType) addLTARRef(row, injectedColumn?.value as ColumnType)
isChildrenExcludedListLinked.value[id] = true isChildrenExcludedListLinked.value[id] = true
saveRow!() saveRow!()
$e('a:links:link')
} else { } else {
await link(row, {}, false, id) await link(row, {}, false, id)
} }
@ -68,6 +72,7 @@ const unlinkRow = async (row: Record<string, any>, id: number) => {
removeLTARRef(row, injectedColumn?.value as ColumnType) removeLTARRef(row, injectedColumn?.value as ColumnType)
isChildrenExcludedListLinked.value[id] = false isChildrenExcludedListLinked.value[id] = false
saveRow!() saveRow!()
$e('a:links:unlink')
} else { } else {
await unlink(row, {}, false, id) await unlink(row, {}, false, id)
} }
@ -193,6 +198,7 @@ onKeyStroke('Escape', () => {
<!-- Add new record --> <!-- Add new record -->
<NcButton <NcButton
v-if="!isPublic" v-if="!isPublic"
v-e="['c:row-expand:open']"
type="secondary" type="secondary"
size="xl" size="xl"
class="!text-brand-500" class="!text-brand-500"

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

@ -26,7 +26,13 @@ const upgradeWorkspace = async () => {
<template v-if="activeWorkspace.plan === WorkspacePlan.FREE"> <template v-if="activeWorkspace.plan === WorkspacePlan.FREE">
<div class="flex text-xl font-medium">Upgrade your workspace</div> <div class="flex text-xl font-medium">Upgrade your workspace</div>
<a-button type="primary" size="large" class="!rounded-md" :loading="isUpgrading" @click="upgradeWorkspace" <a-button
v-e="['c:workspace:settings:upgrade']"
type="primary"
size="large"
class="!rounded-md"
:loading="isUpgrading"
@click="upgradeWorkspace"
>Upgrade >Upgrade
</a-button> </a-button>
</template> </template>

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

@ -29,6 +29,7 @@ const centered = computed(() => props.centered ?? true)
<template> <template>
<NcButton <NcButton
v-if="isUIAllowed('projectCreate', { roles: workspaceRoles ?? orgRoles }) && !isSharedBase" v-if="isUIAllowed('projectCreate', { roles: workspaceRoles ?? orgRoles }) && !isSharedBase"
v-e="['c:base:create']"
type="text" type="text"
:size="size" :size="size"
:centered="centered" :centered="centered"

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

@ -123,6 +123,7 @@ const typeLabel = computed(() => {
<div class="flex flex-row justify-end mt-7 gap-x-2"> <div class="flex flex-row justify-end mt-7 gap-x-2">
<NcButton type="secondary" @click="dialogShow = false">Cancel</NcButton> <NcButton type="secondary" @click="dialogShow = false">Cancel</NcButton>
<NcButton <NcButton
v-e="['a:base:create']"
data-testid="docs-create-proj-dlg-create-btn" data-testid="docs-create-proj-dlg-create-btn"
:loading="creating" :loading="creating"
type="primary" type="primary"

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

@ -127,6 +127,7 @@ const onCancel = () => {
Cancel Cancel
</NcButton> </NcButton>
<NcButton <NcButton
v-e="['c:workspace:settings:rename']"
type="primary" type="primary"
html-type="submit" html-type="submit"
:disabled="isErrored || (form.title && form.title === activeWorkspace.title)" :disabled="isErrored || (form.title && form.title === activeWorkspace.title)"
@ -148,7 +149,13 @@ const onCancel = () => {
</div> </div>
<div class="flex flex-row w-full justify-end mt-8"> <div class="flex flex-row w-full justify-end mt-8">
<NcButton type="danger" :disabled="!isConfirmed" :loading="isDeleting" @click="onDelete"> <NcButton
v-e="['c:workspace:settings:delete']"
type="danger"
:disabled="!isConfirmed"
:loading="isDeleting"
@click="onDelete"
>
<template #loading> Deleting Workspace </template> <template #loading> Deleting Workspace </template>
Delete Workspace Delete Workspace
</NcButton> </NcButton>

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

@ -41,7 +41,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
const { project } = storeToRefs(useProject()) const { project } = storeToRefs(useProject())
const { $api } = useNuxtApp() const { $api, $e } = useNuxtApp()
const activeView = inject(ActiveViewInj, ref()) const activeView = inject(ActiveViewInj, ref())
@ -305,6 +305,8 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
await loadChildrenList() await loadChildrenList()
} }
onSuccess?.(row) onSuccess?.(row)
$e('a:links:delete-related-row')
} catch (e: any) { } catch (e: any) {
message.error(`${t('msg.error.deleteFailed')}: ${await extractSdkResponseErrorMsg(e)}`) message.error(`${t('msg.error.deleteFailed')}: ${await extractSdkResponseErrorMsg(e)}`)
} }
@ -375,6 +377,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
} }
reloadData?.(false) reloadData?.(false)
$e('a:links:unlink')
} }
const link = async ( const link = async (
@ -444,6 +447,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
} }
reloadData?.(false) reloadData?.(false)
$e('a:links:link')
} }
// watchers // watchers

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

@ -207,7 +207,6 @@ export function useViewData(
offset: (page - 1) * (paginationData.value.pageSize || appInfoDefaultLimit), offset: (page - 1) * (paginationData.value.pageSize || appInfoDefaultLimit),
where: where?.value, where: where?.value,
} as any) } as any)
$e('a:grid:pagination')
} }
const { const {

9
packages/nc-gui/lang/en.json

@ -419,6 +419,7 @@
"allTables": "All Tables", "allTables": "All Tables",
"members": "Members", "members": "Members",
"dataSources": "Data Sources", "dataSources": "Data Sources",
"connectDataSource": "Connect a Data Source",
"searchProjects": "Search Projects", "searchProjects": "Search Projects",
"createdBy": "Created By", "createdBy": "Created By",
"viewingAttachmentsOf": "Viewing Attachments of", "viewingAttachmentsOf": "Viewing Attachments of",
@ -435,14 +436,17 @@
"untitledToken": "Untitled token", "untitledToken": "Untitled token",
"tableName": "Table name", "tableName": "Table name",
"dashboardName": "Dashboard name", "dashboardName": "Dashboard name",
"noViews": "No views",
"createView": "Create a View", "createView": "Create a View",
"creatingView": "Creating View", "creatingView": "Creating View",
"duplicateView": "Duplicate view", "duplicateView": "Duplicate View",
"duplicateGridView": "Duplicate Grid View", "duplicateGridView": "Duplicate Grid View",
"createGridView": "Create Grid View",
"duplicateGalleryView": "Duplicate Gallery View", "duplicateGalleryView": "Duplicate Gallery View",
"createGalleryView": "Create Gallery View",
"duplicateFormView": "Duplicate Form View", "duplicateFormView": "Duplicate Form View",
"createFormView": "Create Form View",
"duplicateKanbanView": "Duplicate Kanban View", "duplicateKanbanView": "Duplicate Kanban View",
"createKanbanView": "Create Kanban View",
"viewName": "View name", "viewName": "View name",
"viewLink": "View Link", "viewLink": "View Link",
"columnName": "Column Name", "columnName": "Column Name",
@ -771,6 +775,7 @@
"startCommenting": "Start commenting!" "startCommenting": "Start commenting!"
}, },
"tooltip": { "tooltip": {
"reachedSourceLimit": "Limited to only one data source for the moment",
"saveChanges": "Save changes", "saveChanges": "Save changes",
"xcDB": "Create a new project", "xcDB": "Create a new project",
"extDB": "Supports MySQL, PostgreSQL, SQL Server & SQLite", "extDB": "Supports MySQL, PostgreSQL, SQL Server & SQLite",

4
packages/nc-gui/pages/signin.vue

@ -72,10 +72,6 @@ async function signIn() {
function resetError() { function resetError() {
if (error.value) error.value = null if (error.value) error.value = null
} }
onMounted(async () => {
await clearWorkspaces()
})
</script> </script>
<template> <template>

6
packages/nc-gui/plugins/tele.ts

@ -84,8 +84,12 @@ export default defineNuxtPlugin(async (nuxtApp) => {
} }
watch((nuxtApp.$state as ReturnType<typeof useGlobal>).token, (newToken, oldToken) => { watch((nuxtApp.$state as ReturnType<typeof useGlobal>).token, (newToken, oldToken) => {
try {
if (newToken && newToken !== oldToken) init(newToken) if (newToken && newToken !== oldToken) init(newToken)
else if (!newToken) socket.disconnect() else if (!newToken) socket?.disconnect()
} catch (e) {
console.error(e)
}
}) })
nuxtApp.provide('tele', tele) nuxtApp.provide('tele', tele)

3
packages/nc-gui/store/config.ts

@ -13,6 +13,8 @@ export const useConfigStore = defineStore('configStore', () => {
const isMobileMode = ref(isViewPortMobile()) const isMobileMode = ref(isViewPortMobile())
const projectPageTab = ref<'allTable' | 'collaborator' | 'data-source'>('allTable')
const onViewPortResize = () => { const onViewPortResize = () => {
isMobileMode.value = isViewPortMobile() isMobileMode.value = isViewPortMobile()
} }
@ -62,6 +64,7 @@ export const useConfigStore = defineStore('configStore', () => {
isMobileMode, isMobileMode,
isViewPortMobile, isViewPortMobile,
handleSidebarOpenOnMobileForNonViews, handleSidebarOpenOnMobileForNonViews,
projectPageTab,
} }
}) })

2
packages/nc-gui/store/projects.ts

@ -105,7 +105,7 @@ export const useProjects = defineStore('projectsStore', () => {
...(projects.value.get(project.id!) || {}), ...(projects.value.get(project.id!) || {}),
...project, ...project,
bases: [...(project.bases ?? projects.value.get(project.id!)?.bases ?? [])], bases: [...(project.bases ?? projects.value.get(project.id!)?.bases ?? [])],
isExpanded: route.value.params.projectId === project.id || projects.value.get(project.id!)?.isExpanded, isExpanded: true,
isLoading: false, isLoading: false,
}) })

15
packages/nc-gui/store/tables.ts

@ -76,6 +76,21 @@ export const useTablesStore = defineStore('tablesStore', () => {
includeM2M: includeM2M.value, includeM2M: includeM2M.value,
}) })
tables.list?.forEach((t) => {
let meta = t.meta
if (typeof meta === 'string') {
try {
meta = JSON.parse(meta)
} catch (e) {
console.error(e)
}
}
if (!meta) meta = {}
t.meta = meta
})
projectTables.value.set(projectId, tables.list || []) projectTables.value.set(projectId, tables.list || [])
} }

4
packages/nc-gui/utils/iconUtils.ts

@ -79,6 +79,8 @@ import Down from '~icons/material-symbols/keyboard-arrow-down-rounded'
import PhTriangleFill from '~icons/ph/triangle-fill' import PhTriangleFill from '~icons/ph/triangle-fill'
import LcSend from '~icons/lucide/send' import LcSend from '~icons/lucide/send'
import NcCommentHere from '~icons/nc-icons/comment-here' import NcCommentHere from '~icons/nc-icons/comment-here'
import NcAddDataSource from '~icons/nc-icons/add-data-source'
import NcDatabaseIcon from '~icons/nc-icons/database'
// Roles // Roles
import MaterialSymbolsManageAccountsOutline from '~icons/material-symbols/manage-accounts-outline' import MaterialSymbolsManageAccountsOutline from '~icons/material-symbols/manage-accounts-outline'
@ -347,6 +349,7 @@ export const iconMap = {
email: h('span', { class: 'material-symbols' }, 'email'), email: h('span', { class: 'material-symbols' }, 'email'),
sendEmail: h('span', { class: 'material-symbols' }, 'email'), sendEmail: h('span', { class: 'material-symbols' }, 'email'),
send: LcSend, send: LcSend,
dataSource: NcAddDataSource,
currency: h('span', { class: 'material-symbols' }, 'attach_money'), currency: h('span', { class: 'material-symbols' }, 'attach_money'),
percent: h('span', { class: 'material-symbols' }, 'percent'), percent: h('span', { class: 'material-symbols' }, 'percent'),
decimal: h('span', { class: 'material-symbols' }, 'decimal_increase'), decimal: h('span', { class: 'material-symbols' }, 'decimal_increase'),
@ -412,6 +415,7 @@ export const iconMap = {
heightExtra: NcIconsRowHeightExtraTall, heightExtra: NcIconsRowHeightExtraTall,
databaseSearch: MdiDatabaseSearch, databaseSearch: MdiDatabaseSearch,
layers: NcLayers, layers: NcLayers,
ncDatabase: NcDatabaseIcon,
magic: PhSparkleFill, magic: PhSparkleFill,
magic1: MdiMagicStaff, magic1: MdiMagicStaff,
workspace: h('span', { class: 'material-symbols' }, 'dataset'), workspace: h('span', { class: 'material-symbols' }, 'dataset'),

9
packages/nc-gui/windi.config.ts

@ -56,15 +56,18 @@ export default defineConfig({
}, },
extend: { extend: {
screens: { screens: {
xs: { 'xs': {
max: '480px', max: '480px',
}, },
sm: { 'sm': {
min: '480px', min: '480px',
}, },
md: { 'md': {
min: '820px', min: '820px',
}, },
'2xl': {
min: '1780px',
},
}, },
textColor: { textColor: {
primary: 'rgba(var(--color-primary), var(--tw-text-opacity))', primary: 'rgba(var(--color-primary), var(--tw-text-opacity))',

47
packages/nocodb/src/models/Model.ts

@ -221,6 +221,19 @@ export default class Model implements TableType {
(a.order != null ? a.order : Infinity) - (a.order != null ? a.order : Infinity) -
(b.order != null ? b.order : Infinity), (b.order != null ? b.order : Infinity),
); );
for (const model of modelList) {
if (model.meta?.hasNonDefaultViews === undefined) {
model.meta = {
...(model.meta ?? {}),
hasNonDefaultViews: await Model.getNonDefaultViewsCountAndReset(
{ modelId: model.id },
ncMeta,
),
};
}
}
return modelList.map((m) => new Model(m)); return modelList.map((m) => new Model(m));
} }
@ -255,6 +268,18 @@ export default class Model implements TableType {
await NocoCache.setList(CacheScope.MODEL, [project_id], modelList); await NocoCache.setList(CacheScope.MODEL, [project_id], modelList);
} }
for (const model of modelList) {
if (model.meta?.hasNonDefaultViews === undefined) {
model.meta = {
...(model.meta ?? {}),
hasNonDefaultViews: await Model.getNonDefaultViewsCountAndReset(
{ modelId: model.id },
ncMeta,
),
};
}
}
return modelList.map((m) => new Model(m)); return modelList.map((m) => new Model(m));
} }
@ -964,4 +989,26 @@ export default class Model implements TableType {
tableId, tableId,
); );
} }
static async getNonDefaultViewsCountAndReset(
{
modelId,
}: {
modelId: string;
},
_ncMeta = Noco.ncMeta,
) {
const model = await this.get(modelId);
let modelMeta = parseMetaProp(model);
const views = await View.list(modelId);
modelMeta = {
...(modelMeta ?? {}),
hasNonDefaultViews: views.length > 1,
};
await this.updateMeta(modelId, modelMeta);
return modelMeta?.hasNonDefaultViews;
}
} }

10
packages/nocodb/src/models/View.ts

@ -501,6 +501,11 @@ export default class View implements ViewType {
} }
} }
await Model.getNonDefaultViewsCountAndReset(
{ modelId: view.fk_model_id },
ncMeta,
);
return View.get(view_id, ncMeta); return View.get(view_id, ncMeta);
} }
@ -1078,6 +1083,11 @@ export default class View implements ViewType {
CacheScope.SINGLE_QUERY, CacheScope.SINGLE_QUERY,
`${view.fk_model_id}:${view.id}:*`, `${view.fk_model_id}:${view.id}:*`,
); );
await Model.getNonDefaultViewsCountAndReset(
{ modelId: view.fk_model_id },
ncMeta,
);
} }
private static extractViewColumnsTableName(view: View) { private static extractViewColumnsTableName(view: View) {

14
packages/nocodb/tests/unit/factory/table.ts

@ -45,4 +45,16 @@ const getAllTables = async ({ project }: { project: Project }) => {
return tables; return tables;
}; };
export { createTable, getTable, getAllTables }; const updateTable = async (
context,
{ table, args }: { table: Model; args: any },
) => {
const response = await request(context.app)
.patch(`/api/v1/db/meta/tables/${table.id}`)
.set('xc-auth', context.token)
.send(args);
return response.body;
};
export { createTable, getTable, getAllTables, updateTable };

16
packages/nocodb/tests/unit/factory/view.ts

@ -121,4 +121,18 @@ const updateView = async (
} }
}; };
export { createView, updateView, getView }; const deleteView = async (
context,
{
viewId,
}: {
viewId: string;
},
) => {
await request(context.app)
.delete(`/api/v1/db/meta/views/${viewId}`)
.set('xc-auth', context.token)
.expect(200);
};
export { createView, updateView, getView, deleteView };

1
packages/nocodb/tests/unit/rest/tests/attachment.test.ts

@ -14,7 +14,6 @@ function attachmentTests() {
beforeEach(async function () { beforeEach(async function () {
console.time('#### attachmentTests'); console.time('#### attachmentTests');
context = await init();
fs.writeFileSync(FILE_PATH, 'test', `utf-8`); fs.writeFileSync(FILE_PATH, 'test', `utf-8`);
context = await init(); context = await init();
console.timeEnd('#### attachmentTests'); console.timeEnd('#### attachmentTests');

81
packages/nocodb/tests/unit/rest/tests/table.test.ts

@ -1,11 +1,13 @@
import 'mocha'; import 'mocha';
import request from 'supertest'; import request from 'supertest';
import { expect } from 'chai';
import { createView, deleteView } from 'tests/unit/factory/view';
import { ViewTypes } from 'nocodb-sdk';
import init from '../../init'; import init from '../../init';
import { createTable, getAllTables } from '../../factory/table'; import { createTable, getAllTables, updateTable } from '../../factory/table';
import { createProject } from '../../factory/project'; import { createProject } from '../../factory/project';
import { defaultColumns } from '../../factory/column'; import { defaultColumns } from '../../factory/column';
import Model from '../../../../src/models/Model'; import Model from '../../../../src/models/Model';
import { expect } from 'chai';
// Test case list // Test case list
// 1. Get table list // 1. Get table list
@ -277,6 +279,81 @@ function tableTest() {
// new Error(); // new Error();
// }); // });
}); });
it('Add and delete view should update hasNonDefaultViews', async () => {
let response = await request(context.app)
.get(`/api/v1/db/meta/projects/${project.id}/tables`)
.set('xc-auth', context.token)
.send({})
.expect(200);
expect(response.body.list[0].meta.hasNonDefaultViews).to.be.false;
const view = await createView(context, {
table,
title: 'view1',
type: ViewTypes.GRID,
});
response = await request(context.app)
.get(`/api/v1/db/meta/projects/${project.id}/tables`)
.set('xc-auth', context.token)
.send({})
.expect(200);
expect(response.body.list[0].meta.hasNonDefaultViews).to.be.true;
await deleteView(context, { viewId: view.id });
response = await request(context.app)
.get(`/api/v1/db/meta/projects/${project.id}/tables`)
.set('xc-auth', context.token)
.send({})
.expect(200);
expect(response.body.list[0].meta.hasNonDefaultViews).to.be.false;
});
it('Project with empty meta should update hasNonDefaultViews', async () => {
let response = await request(context.app)
.get(`/api/v1/db/meta/projects/${project.id}/tables`)
.set('xc-auth', context.token)
.send({})
.expect(200);
expect(response.body.list[0].meta.hasNonDefaultViews).to.be.false;
const view = await createView(context, {
table,
title: 'view1',
type: ViewTypes.GRID,
});
await updateTable(context, {
table,
args: {
meta: {},
},
});
response = await request(context.app)
.get(`/api/v1/db/meta/projects/${project.id}/tables`)
.set('xc-auth', context.token)
.send({})
.expect(200);
expect(response.body.list[0].meta.hasNonDefaultViews).to.be.true;
await deleteView(context, { viewId: view.id });
response = await request(context.app)
.get(`/api/v1/db/meta/projects/${project.id}/tables`)
.set('xc-auth', context.token)
.send({})
.expect(200);
expect(response.body.list[0].meta.hasNonDefaultViews).to.be.false;
});
} }
export default async function () { export default async function () {

8
tests/playwright/pages/Dashboard/ProjectView/TablesViewPage.ts

@ -24,11 +24,11 @@ export class TablesViewPage extends BasePage {
await this.get().waitFor({ state: 'visible' }); await this.get().waitFor({ state: 'visible' });
if (role.toLowerCase() === 'creator' || role.toLowerCase() === 'owner') { if (role.toLowerCase() === 'creator' || role.toLowerCase() === 'owner') {
expect(await this.btn_addNewTable.isVisible()).toBeTruthy(); await expect(this.btn_addNewTable).toBeVisible();
expect(await this.btn_importData.isVisible()).toBeTruthy(); await expect(this.btn_importData).toBeVisible();
} else { } else {
expect(await this.btn_addNewTable.isVisible()).toBeFalsy(); await expect(this.btn_addNewTable).toHaveCount(0);
expect(await this.btn_importData.isVisible()).toBeFalsy(); await expect(this.btn_importData).toHaveCount(0);
} }
} }
} }

2
tests/playwright/pages/Dashboard/common/WorkspaceMenu/index.ts

@ -20,7 +20,7 @@ export class WorkspaceMenuObject extends BasePage {
async switchWorkspace({ workspaceTitle }: { workspaceTitle: string }) { async switchWorkspace({ workspaceTitle }: { workspaceTitle: string }) {
await this.toggle(); await this.toggle();
await this.rootPage.waitForTimeout(2500); await this.rootPage.waitForTimeout(2500);
await this.rootPage.getByTestId('nc-workspace-list').getByText(workspaceTitle).click({ await this.rootPage.locator('.ant-dropdown-menu').getByTestId('nc-workspace-list').getByText(workspaceTitle).click({
force: true, force: true,
}); });
await this.rootPage.keyboard.press('Escape'); await this.rootPage.keyboard.press('Escape');

2
tests/playwright/setup/knexHelper.ts

@ -3,7 +3,7 @@ import { promises as fs } from 'fs';
import { getKnexConfig } from '../tests/utils/config'; import { getKnexConfig } from '../tests/utils/config';
async function dropAndCreateDb(kn: Knex, dbName: string) { async function dropAndCreateDb(kn: Knex, dbName: string) {
await kn.raw(`DROP DATABASE IF EXISTS ??`, [dbName]); await kn.raw(`DROP DATABASE IF EXISTS ?? WITH (FORCE)`, [dbName]);
await kn.raw(`CREATE DATABASE ??`, [dbName]); await kn.raw(`CREATE DATABASE ??`, [dbName]);
} }

Loading…
Cancel
Save