Browse Source

Nc Fix: UI improvements - left sidebar (#8244)

* fix(nc-gui); update toolbar menu text grom GroupBy to Group

* fix(nc-gui): move sidebar base, table expand icon to the right side

* fix(nc-gui): sidebar base, table, view menu options padding issue

* fix(nc-gui): add background color for row on hover in grid view

* fix(nc-gui): reduce width of index column

* fix(nc-gui): on hover grid row bg opacity issue

* fix(nc-gui): reduce font size and grid cell height

* fix(nc-gui): sidebar view menu alignment issue on mobile screen

* fix(nc-gui): reduce font size

* fix(nc-gui): set column default width to 180px

* fix(nc-gui): keep only sidebar changes & revert all other changes

* fix(nc-gui): change table icon

* fix(nc-gui): trim base, table, view title while saving it

* fix(nc-gui): increate left sidebar max width

* fix(nc-gui): show truncated base/table/view name until standard end

* fix(nc-gui): oss sidebar base menu options alignment issue

* fix(nc-gui): use valid classname

* fix(nc-gui): sidebar external db source menu ui fixes

* fix(nc-gui): small changes

* chore(nc-gui): lint

* fix(nc-gui): pw test fail issue

* fix(nc-gui): trim title while creating base, table, view

* fix(nc-gui): some of the sidebar pw test fail issues

* fix(test): sidebar test fail issue

* fix(test): projectCollaboration test fail issue

* fix(nc-gui): change font size of view menu option view mode chip text

* fix(nc-gui): grayed out create view dropdown plus icon color

* fix(nc-gui): grayed out table icon and reduce width of default view context menu

* fix(nc-gui): remove copyright text from user menu

* fix(nc-gui):  chevron icon and show/hide sidebar icon should be gray in color
pull/8276/head
Ramesh Mane 7 months ago committed by GitHub
parent
commit
9cc1f3cb30
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 21
      packages/nc-gui/assets/nc-icons/record.svg
  2. 8
      packages/nc-gui/assets/style.scss
  3. 2
      packages/nc-gui/components/dashboard/Sidebar/Header.vue
  4. 12
      packages/nc-gui/components/dashboard/Sidebar/UserInfo.vue
  5. 4
      packages/nc-gui/components/dashboard/TreeView/CreateViewBtn.vue
  6. 364
      packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
  7. 4
      packages/nc-gui/components/dashboard/TreeView/TableList.vue
  8. 242
      packages/nc-gui/components/dashboard/TreeView/TableNode.vue
  9. 4
      packages/nc-gui/components/dashboard/TreeView/ViewsList.vue
  10. 16
      packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue
  11. 12
      packages/nc-gui/components/dashboard/View.vue
  12. 2
      packages/nc-gui/components/dlg/TableCreate.vue
  13. 14
      packages/nc-gui/components/dlg/TableRename.vue
  14. 4
      packages/nc-gui/components/dlg/ViewCreate.vue
  15. 2
      packages/nc-gui/components/erd/HistogramPanel.vue
  16. 2
      packages/nc-gui/components/general/OpenLeftSidebarBtn.vue
  17. 4
      packages/nc-gui/components/general/TableIcon.vue
  18. 2
      packages/nc-gui/components/smartsheet/toolbar/GroupByMenu.vue
  19. 16
      packages/nc-gui/components/smartsheet/toolbar/LockType.vue
  20. 4
      packages/nc-gui/components/workspace/CreateProjectDlg.vue
  21. 5
      packages/nc-gui/composables/useTableNew.ts
  22. 3
      packages/nc-gui/lang/en.json
  23. 31
      tests/playwright/pages/Dashboard/Sidebar/ProjectNode/index.ts
  24. 71
      tests/playwright/pages/Dashboard/TreeView.ts
  25. 31
      tests/playwright/pages/Dashboard/index.ts

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

@ -1,12 +1,13 @@
<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">
<g clip-path="url(#clip0_1613_80692)"> <path
<path d="M11.8571 5.96903L4.14285 10.4225" stroke="#374151" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/> d="M1.21396 7.35265C0.928679 7.19673 0.928679 6.80327 1.21396 6.64735L7.7893 3.05351C7.91986 2.98216 8.08012 2.98216 8.21067 3.05351L14.786 6.64735C15.0713 6.80327 15.0713 7.19673 14.786 7.35265L8.21067 10.9465C8.08012 11.0178 7.91986 11.0178 7.7893 10.9465L1.21396 7.35265Z"
<rect x="1.15184" width="9.06208" height="9.06208" rx="1.335" transform="matrix(0.866044 -0.499967 0.866044 0.499967 -0.845705 8.77156)" stroke="#374151" stroke-width="1.33"/> stroke="currentColor" stroke-width="1.33" />
<path d="M3.5 6.34009L11.2143 10.7935" stroke="#374151" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/> <path
</g> d="M8.21067 13.9465L14.786 10.3527C14.9287 10.2747 15 10.1374 15 10V7.3702C15 7.2939 14.918 7.24571 14.8513 7.28284L8.04867 11.0729C8.01841 11.0897 7.98159 11.0897 7.95133 11.0729L1.14867 7.28284C1.08202 7.24571 1 7.2939 1 7.3702V10C1 10.1374 1.07132 10.2747 1.21396 10.3527L7.7893 13.9465C7.91986 14.0179 8.08012 14.0179 8.21067 13.9465Z"
<defs> stroke="currentColor" stroke-width="1.33" />
<clipPath id="clip0_1613_80692"> <path d="M4.5 5.02069L11.5 9.06179" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round"
<rect width="16" height="16" fill="white"/> stroke-linejoin="round" />
</clipPath> <path d="M4.5 9.06152L11.5 5.02042" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round"
</defs> stroke-linejoin="round" />
<line x1="7.995" y1="11" x2="7.995" y2="14" stroke="currentColor" stroke-width="1.33" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 686 B

After

Width:  |  Height:  |  Size: 1.2 KiB

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

@ -720,12 +720,20 @@ input[type='number'] {
.nc-sidebar-node-btn:not(.nc-sidebar-expand) { .nc-sidebar-node-btn:not(.nc-sidebar-expand) {
@apply !xs:(hidden); @apply !xs:(hidden);
} }
.nc-sidebar-node-btn.nc-sidebar-expand {
@apply !xs:(flex-none border-1 border-gray-200 w-6.5 h-6.5 mr-1);
}
} }
.nc-button.ant-btn.nc-sidebar-node-btn { .nc-button.ant-btn.nc-sidebar-node-btn {
@apply opacity-0 group-hover:(opacity-100) text-gray-600 hover:(bg-gray-400 bg-opacity-20 text-gray-900) duration-100; @apply opacity-0 group-hover:(opacity-100) text-gray-600 hover:(bg-gray-400 bg-opacity-20 text-gray-900) duration-100;
} }
.nc-button.ant-btn.nc-sidebar-node-btn:not(.nc-sidebar-expand):not(.nc-sidebar-view-node-context-btn) {
@apply hidden group-hover:(inline-block);
}
.nc-button.ant-btn.nc-sidebar-node-btn.nc-sidebar-expand { .nc-button.ant-btn.nc-sidebar-node-btn.nc-sidebar-expand {
@apply xs:(opacity-100 hover:bg-gray-50); @apply xs:(opacity-100 hover:bg-gray-50);

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

@ -49,7 +49,7 @@ const showSidebarBtn = computed(() => !(isMobileMode.value && !activeViewTitleOr
<GeneralIcon <GeneralIcon
v-else v-else
icon="doubleLeftArrow" icon="doubleLeftArrow"
class="duration-150 transition-all !text-lg -mt-0.5" class="duration-150 transition-all !text-lg -mt-0.5 !text-gray-500/75"
:class="{ :class="{
'transform rotate-180': !isLeftSidebarOpen, 'transform rotate-180': !isLeftSidebarOpen,
}" }"

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

@ -54,17 +54,17 @@ onMounted(() => {
</script> </script>
<template> <template>
<div class="flex w-full flex-col p-1 border-gray-200 gap-y-1"> <div class="flex w-full flex-col py-0.9 px-1 border-gray-200 gap-y-1">
<NcDropdown v-model:visible="isMenuOpen" placement="topLeft" overlay-class-name="!min-w-64"> <NcDropdown v-model:visible="isMenuOpen" placement="topLeft" overlay-class-name="!min-w-64">
<div <div
class="flex flex-row py-2 px-3 gap-x-2 items-center hover:bg-gray-200 rounded-lg cursor-pointer h-10" class="flex flex-row py-1 px-3 gap-x-2 items-center hover:bg-gray-200 rounded-lg cursor-pointer h-8"
data-testid="nc-sidebar-userinfo" data-testid="nc-sidebar-userinfo"
> >
<GeneralUserIcon :email="user?.email" size="base" :name="user?.display_name" /> <GeneralUserIcon :email="user?.email" size="auto" :name="user?.display_name" />
<div class="flex truncate"> <div class="flex truncate">
{{ name ? name : user?.email }} {{ name ? name : user?.email }}
</div> </div>
<GeneralIcon icon="arrowUp" class="!min-w-5" /> <GeneralIcon icon="chevronDown" class="flex-none !min-w-5 transform rotate-180 !text-gray-500" />
</div> </div>
<template #overlay> <template #overlay>
<NcMenu data-testid="nc-sidebar-userinfo"> <NcMenu data-testid="nc-sidebar-userinfo">
@ -175,8 +175,7 @@ onMounted(() => {
</template> </template>
</NcDropdown> </NcDropdown>
<template v-if="isMobileMode"></template> <template v-if="isMobileMode || appInfo.ee"></template>
<div v-else-if="appInfo.ee" class="text-gray-500 text-xs pl-3 mt-1">© 2023 NocoDB. Inc</div>
<div v-else class="flex flex-row w-full justify-between pt-0.5 truncate"> <div v-else class="flex flex-row w-full justify-between pt-0.5 truncate">
<GeneralJoinCloud /> <GeneralJoinCloud />
</div> </div>
@ -189,7 +188,6 @@ onMounted(() => {
} }
.menu-icon { .menu-icon {
@apply w-4 h-4; @apply w-4 h-4;
line-height: 1rem;
font-size: 1rem; font-size: 1rem;
} }

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

@ -163,7 +163,7 @@ async function onOpenModal({
</div> </div>
<GeneralLoader v-if="toBeCreateType === ViewTypes.CALENDAR && isViewListLoading" /> <GeneralLoader v-if="toBeCreateType === ViewTypes.CALENDAR && isViewListLoading" />
<GeneralIcon v-else class="text-brand-400" icon="plus" /> <GeneralIcon v-else class="plus" icon="plus" />
</div> </div>
</NcMenuItem> </NcMenuItem>
</NcMenu> </NcMenu>
@ -181,7 +181,7 @@ async function onOpenModal({
} }
.plus { .plus {
@apply text-brand-400; @apply text-gray-500;
} }
</style> </style>

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

@ -134,6 +134,10 @@ const enableEditMode = () => {
} }
const updateProjectTitle = async () => { const updateProjectTitle = async () => {
if (tempTitle.value) {
tempTitle.value = tempTitle.value.trim()
}
if (!tempTitle.value) return if (!tempTitle.value) return
try { try {
@ -431,22 +435,8 @@ const onTableIdCopy = async () => {
'hover:bg-gray-200': !(activeProjectId === base.id && baseViewOpen), 'hover:bg-gray-200': !(activeProjectId === base.id && baseViewOpen),
}" }"
:data-testid="`nc-sidebar-base-title-${base.title}`" :data-testid="`nc-sidebar-base-title-${base.title}`"
class="nc-sidebar-node base-title-node h-7.25 flex-grow rounded-md group flex items-center w-full pr-1" class="nc-sidebar-node base-title-node h-7.25 flex-grow rounded-md group flex items-center w-full pr-1 pl-1.5"
> >
<NcButton
v-e="['c:base:expand']"
type="text"
size="xxsmall"
class="nc-sidebar-node-btn nc-sidebar-expand ml-0.75 !xs:visible"
@click="onProjectClick(base, true, true)"
>
<GeneralIcon
icon="triangleFill"
class="group-hover:visible cursor-pointer transform transition-transform duration-500 h-1.5 w-1.75 rotate-90 !xs:visible"
:class="{ '!rotate-180': base.isExpanded, '!visible': isOptionsOpen }"
/>
</NcButton>
<div class="flex items-center mr-1" @click="onProjectClick(base)"> <div class="flex items-center mr-1" @click="onProjectClick(base)">
<div class="flex items-center select-none w-6 h-full"> <div class="flex items-center select-none w-6 h-full">
<a-spin v-if="base.isLoading" class="!ml-1.25 !flex !flex-row !items-center !my-0.5 w-8" :indicator="indicator" /> <a-spin v-if="base.isLoading" class="!ml-1.25 !flex !flex-row !items-center !my-0.5 w-8" :indicator="indicator" />
@ -469,7 +459,7 @@ const onTableIdCopy = async () => {
v-if="editMode" v-if="editMode"
ref="input" ref="input"
v-model="tempTitle" v-model="tempTitle"
class="flex-grow leading-1 outline-0 ring-none capitalize !text-inherit !bg-transparent w-4/5" class="flex-grow leading-1 outline-0 ring-none capitalize !text-inherit !bg-transparent flex-1 mr-4"
:class="{ 'text-black font-semibold': activeProjectId === base.id && baseViewOpen && !isMobileMode }" :class="{ 'text-black font-semibold': activeProjectId === base.id && baseViewOpen && !isMobileMode }"
@click.stop @click.stop
@keyup.enter="updateProjectTitle" @keyup.enter="updateProjectTitle"
@ -478,154 +468,176 @@ const onTableIdCopy = async () => {
/> />
<NcTooltip <NcTooltip
v-else v-else
class="nc-sidebar-node-title capitalize text-ellipsis overflow-hidden select-none" class="nc-sidebar-node-title capitalize text-ellipsis overflow-hidden select-none flex-1"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }" :style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
:class="{ 'text-black font-semibold': activeProjectId === base.id && baseViewOpen }" :class="{ 'text-black font-semibold': activeProjectId === base.id && baseViewOpen }"
show-on-truncate-only show-on-truncate-only
@click="onProjectClick(base)"
> >
<template #title>{{ base.title }}</template> <template #title>{{ base.title }}</template>
<span @click="onProjectClick(base)"> <span>
{{ base.title }} {{ base.title }}
</span> </span>
</NcTooltip> </NcTooltip>
<div :class="{ 'flex flex-grow h-full': !editMode }" @click="onProjectClick(base)"></div>
<NcDropdown v-if="!isSharedBase" v-model:visible="isOptionsOpen" :trigger="['click']"> <template v-if="!editMode">
<NcButton <NcDropdown v-if="!isSharedBase" v-model:visible="isOptionsOpen" :trigger="['click']">
v-e="['c:base:options']" <NcButton
class="nc-sidebar-node-btn" v-e="['c:base:options']"
:class="{ '!text-black !opacity-100': isOptionsOpen }" class="nc-sidebar-node-btn"
data-testid="nc-sidebar-context-menu" :class="{ '!text-black !opacity-100 !inline-block': isOptionsOpen }"
type="text" data-testid="nc-sidebar-context-menu"
size="xxsmall" type="text"
@click.stop size="xxsmall"
> @click.stop
<GeneralIcon icon="threeDotHorizontal" class="text-xl w-4.75" />
</NcButton>
<template #overlay>
<NcMenu
class="nc-scrollbar-md"
:style="{
maxHeight: '70vh',
overflow: 'overlay',
}"
:data-testid="`nc-sidebar-base-${base.title}-options`"
@click="isOptionsOpen = false"
> >
<template v-if="!isSharedBase"> <GeneralIcon icon="threeDotHorizontal" class="text-xl w-4.75" />
<NcMenuItem v-if="isUIAllowed('baseRename')" data-testid="nc-sidebar-project-rename" @click="enableEditMode"> </NcButton>
<div v-e="['c:base:rename']" class="flex gap-2 items-center"> <template #overlay>
<GeneralIcon icon="rename" class="group-hover:text-black" /> <NcMenu
{{ $t('general.rename') }} class="nc-scrollbar-md"
</div> :style="{
</NcMenuItem> maxHeight: '70vh',
overflow: 'overlay',
}"
:data-testid="`nc-sidebar-base-${base.title}-options`"
@click="isOptionsOpen = false"
>
<template v-if="!isSharedBase">
<NcMenuItem v-if="isUIAllowed('baseRename')" data-testid="nc-sidebar-project-rename" @click="enableEditMode">
<div v-e="['c:base:rename']" class="flex gap-2 items-center">
<GeneralIcon icon="rename" class="group-hover:text-black" />
{{ $t('general.rename') }}
</div>
</NcMenuItem>
<NcMenuItem
v-if="isUIAllowed('baseDuplicate', { roles: [stringifyRolesObj(orgRoles), baseRole].join() })"
data-testid="nc-sidebar-base-duplicate"
@click="duplicateProject(base)"
>
<div v-e="['c:base:duplicate']" class="flex gap-2 items-center">
<GeneralIcon icon="duplicate" class="text-gray-700" />
{{ $t('general.duplicate') }}
</div>
</NcMenuItem>
<NcDivider v-if="['baseDuplicate', 'baseRename'].some((permission) => isUIAllowed(permission))" />
<!-- Copy Project Info -->
<NcMenuItem
v-if="!isEeUI"
key="copy"
data-testid="nc-sidebar-base-copy-base-info"
@click.stop="copyProjectInfo"
>
<div v-e="['c:base:copy-proj-info']" class="flex gap-2 items-center">
<GeneralIcon icon="copy" class="group-hover:text-black" />
{{ $t('activity.account.projInfo') }}
</div>
</NcMenuItem>
<!-- ERD View -->
<NcMenuItem
v-if="base?.sources?.[0]?.enabled"
key="erd"
data-testid="nc-sidebar-base-relations"
@click="openErdView(base?.sources?.[0])"
>
<div v-e="['c:base:erd']" class="flex gap-2 items-center">
<GeneralIcon icon="erd" />
{{ $t('title.relations') }}
</div>
</NcMenuItem>
<!-- Swagger: Rest APIs -->
<NcMenuItem
v-if="isUIAllowed('apiDocs')"
key="api"
data-testid="nc-sidebar-base-rest-apis"
@click.stop="
() => {
$e('c:base:api-docs')
openLink(`/api/v2/meta/bases/${base.id}/swagger`, appInfo.ncSiteUrl)
}
"
>
<div v-e="['c:base:api-docs']" class="flex gap-2 items-center">
<GeneralIcon icon="snippet" class="group-hover:text-black !max-w-3.9" />
{{ $t('activity.account.swagger') }}
</div>
</NcMenuItem>
</template>
<NcMenuItem <template v-if="base?.sources?.[0]?.enabled && showBaseOption">
v-if="isUIAllowed('baseDuplicate', { roles: [stringifyRolesObj(orgRoles), baseRole].join() })" <NcDivider />
data-testid="nc-sidebar-base-duplicate" <DashboardTreeViewBaseOptions v-model:base="base" :source="base.sources[0]" />
@click="duplicateProject(base)" </template>
>
<div v-e="['c:base:duplicate']" class="flex gap-2 items-center">
<GeneralIcon icon="duplicate" class="text-gray-700" />
{{ $t('general.duplicate') }}
</div>
</NcMenuItem>
<NcDivider v-if="['baseDuplicate', 'baseRename'].some((permission) => isUIAllowed(permission))" /> <NcDivider v-if="['baseMiscSettings', 'baseDelete'].some((permission) => isUIAllowed(permission))" />
<!-- Copy Project Info -->
<NcMenuItem <NcMenuItem
v-if="!isEeUI" v-if="isUIAllowed('baseMiscSettings')"
key="copy" key="teamAndSettings"
data-testid="nc-sidebar-base-copy-base-info" data-testid="nc-sidebar-base-settings"
@click.stop="copyProjectInfo" class="nc-sidebar-base-base-settings"
@click="toggleDialog(true, 'teamAndAuth', undefined, base.id)"
> >
<div v-e="['c:base:copy-proj-info']" class="flex gap-2 items-center"> <div v-e="['c:base:settings']" class="flex gap-2 items-center">
<GeneralIcon icon="copy" class="group-hover:text-black" /> <GeneralIcon icon="settings" class="group-hover:text-black" />
{{ $t('activity.account.projInfo') }} {{ $t('activity.settings') }}
</div> </div>
</NcMenuItem> </NcMenuItem>
<!-- ERD View -->
<NcMenuItem <NcMenuItem
v-if="base?.sources?.[0]?.enabled" v-if="isUIAllowed('baseDelete', { roles: [stringifyRolesObj(orgRoles), baseRole].join() })"
key="erd" data-testid="nc-sidebar-base-delete"
data-testid="nc-sidebar-base-relations" class="!text-red-500 !hover:bg-red-50"
@click="openErdView(base?.sources?.[0])" @click="projectDelete"
> >
<div v-e="['c:base:erd']" class="flex gap-2 items-center"> <div class="flex gap-2 items-center">
<GeneralIcon icon="erd" /> <GeneralIcon icon="delete" class="w-4" />
{{ $t('title.relations') }} {{ $t('general.delete') }}
</div> </div>
</NcMenuItem> </NcMenuItem>
</NcMenu>
</template>
</NcDropdown>
<!-- Swagger: Rest APIs --> <NcButton
<NcMenuItem v-if="isUIAllowed('tableCreate', { roles: baseRole })"
v-if="isUIAllowed('apiDocs')" v-e="['c:base:create-table']"
key="api" :disabled="!base?.sources?.[0]?.enabled"
data-testid="nc-sidebar-base-rest-apis" class="nc-sidebar-node-btn"
@click.stop=" size="xxsmall"
() => { type="text"
$e('c:base:api-docs') data-testid="nc-sidebar-add-base-entity"
openLink(`/api/v2/meta/bases/${base.id}/swagger`, appInfo.ncSiteUrl) :class="{
} '!text-black !inline-block !opacity-100': isAddNewProjectChildEntityLoading,
" '!inline-block !opacity-100': isOptionsOpen,
> }"
<div v-e="['c:base:api-docs']" class="flex gap-2 items-center"> :loading="isAddNewProjectChildEntityLoading"
<GeneralIcon icon="snippet" class="group-hover:text-black !max-w-3.9" /> @click.stop="addNewProjectChildEntity"
{{ $t('activity.account.swagger') }} >
</div> <GeneralIcon icon="plus" class="text-xl leading-5" style="-webkit-text-stroke: 0.15px" />
</NcMenuItem> </NcButton>
</template>
<template v-if="base?.sources?.[0]?.enabled && showBaseOption">
<NcDivider />
<DashboardTreeViewBaseOptions v-model:base="base" :source="base.sources[0]" />
</template>
<NcDivider v-if="['baseMiscSettings', 'baseDelete'].some((permission) => isUIAllowed(permission))" />
<NcMenuItem <NcButton
v-if="isUIAllowed('baseMiscSettings')" v-e="['c:base:expand']"
key="teamAndSettings" type="text"
data-testid="nc-sidebar-base-settings" size="xxsmall"
class="nc-sidebar-base-base-settings" class="nc-sidebar-node-btn nc-sidebar-expand !xs:opacity-100"
@click="toggleDialog(true, 'teamAndAuth', undefined, base.id)" :class="{
> '!opacity-100': isOptionsOpen,
<div v-e="['c:base:settings']" class="flex gap-2 items-center"> }"
<GeneralIcon icon="settings" class="group-hover:text-black" /> @click="onProjectClick(base, true, true)"
{{ $t('activity.settings') }} >
</div> <GeneralIcon
</NcMenuItem> icon="chevronDown"
<NcMenuItem class="group-hover:visible cursor-pointer transform transition-transform duration-500 rotate-270"
v-if="isUIAllowed('baseDelete', { roles: [stringifyRolesObj(orgRoles), baseRole].join() })" :class="{ '!rotate-180': base.isExpanded }"
data-testid="nc-sidebar-base-delete" />
class="!text-red-500 !hover:bg-red-50" </NcButton>
@click="projectDelete" </template>
>
<div class="flex gap-2 items-center">
<GeneralIcon icon="delete" class="w-4" />
{{ $t('general.delete') }}
</div>
</NcMenuItem>
</NcMenu>
</template>
</NcDropdown>
<NcButton
v-if="isUIAllowed('tableCreate', { roles: baseRole })"
v-e="['c:base:create-table']"
:disabled="!base?.sources?.[0]?.enabled"
class="nc-sidebar-node-btn"
size="xxsmall"
type="text"
data-testid="nc-sidebar-add-base-entity"
:class="{ '!text-black !visible': isAddNewProjectChildEntityLoading, '!visible': isOptionsOpen }"
:loading="isAddNewProjectChildEntityLoading"
@click.stop="addNewProjectChildEntity"
>
<GeneralIcon icon="plus" class="text-xl leading-5" style="-webkit-text-stroke: 0.15px" />
</NcButton>
</div> </div>
</div> </div>
@ -658,25 +670,29 @@ const onTableIdCopy = async () => {
ghost ghost
> >
<template #expandIcon="{ isActive }"> <template #expandIcon="{ isActive }">
<div <NcButton
class="nc-sidebar-expand nc-sidebar-node-btn flex flex-row items-center -mt-2 xs:(mt-3 border-1 border-gray-200 px-2.25 py-0.5 rounded-md !mr-0.25)" v-e="['c:external:base:expand']"
type="text"
size="xxsmall"
class="nc-sidebar-node-btn nc-sidebar-expand !xs:opacity-100"
:class="{ '!opacity-100 !inline-block': isBasesOptionsOpen[source!.id!] }"
> >
<GeneralIcon <GeneralIcon
icon="triangleFill" icon="chevronDown"
class="nc-sidebar-source-node-btns -mt-0.75 invisible xs:visible cursor-pointer transform transition-transform duration-500 h-1.5 w-1.5 text-gray-500 rotate-90" class="flex-none cursor-pointer transform transition-transform duration-500 rotate-270"
:class="{ '!rotate-180': isActive }" :class="{ '!rotate-180': isActive }"
/> />
</div> </NcButton>
</template> </template>
<a-collapse-panel :key="`collapse-${source.id}`"> <a-collapse-panel :key="`collapse-${source.id}`">
<template #header> <template #header>
<div class="nc-sidebar-node min-w-20 w-full flex flex-row group py-0.25"> <div class="nc-sidebar-node min-w-20 w-full h-full flex flex-row group py-0.25 pr-6.5 !mr-0">
<div <div
v-if="sourceIndex === 0" v-if="sourceIndex === 0"
class="source-context flex items-center gap-2 text-gray-800 nc-sidebar-node-title" class="source-context flex items-center gap-2 text-gray-800 nc-sidebar-node-title"
@contextmenu="setMenuContext('source', source)" @contextmenu="setMenuContext('source', source)"
> >
<GeneralBaseLogo class="min-w-4 !xs:(min-w-4.25 w-4.25 text-sm)" /> <GeneralBaseLogo class="flex-none min-w-4 !xs:(min-w-4.25 w-4.25 text-sm)" />
{{ $t('general.default') }} {{ $t('general.default') }}
</div> </div>
<div <div
@ -684,22 +700,29 @@ const onTableIdCopy = async () => {
class="source-context flex flex-grow items-center gap-1.75 text-gray-800 min-w-1/20 max-w-full" class="source-context flex flex-grow items-center gap-1.75 text-gray-800 min-w-1/20 max-w-full"
@contextmenu="setMenuContext('source', source)" @contextmenu="setMenuContext('source', source)"
> >
<GeneralBaseLogo class="min-w-4 !xs:(min-w-4.25 w-4.25 text-sm)" /> <GeneralBaseLogo
<div class="flex-none min-w-4 !xs:(min-w-4.25 w-4.25 text-sm) !text-gray-600 !group-hover:text-gray-800"
:data-testid="`nc-sidebar-base-${source.alias}`" />
class="nc-sidebar-node-title flex capitalize text-ellipsis overflow-hidden select-none" <NcTooltip
class="nc-sidebar-node-title capitalize text-ellipsis overflow-hidden select-none"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }" :style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
:class="{
'text-black font-semibold': activeProjectId === base.id && baseViewOpen && !isMobileMode,
}"
show-on-truncate-only
> >
{{ source.alias || '' }} <template #title> {{ source.alias || '' }}</template>
</div> <span :data-testid="`nc-sidebar-base-${source.alias}`">
<a-tooltip class="xs:(hidden)"> {{ source.alias || '' }}
</span>
</NcTooltip>
<NcTooltip class="xs:(hidden) flex items-center mr-1">
<template #title>{{ $t('objects.externalDb') }}</template> <template #title>{{ $t('objects.externalDb') }}</template>
<div>
<GeneralIcon icon="info" class="text-gray-400 -mt-0.5 hover:text-gray-700 mr-1" /> <GeneralIcon icon="info" class="flex-none text-gray-400 hover:text-gray-700 mr-1" />
</div> </NcTooltip>
</a-tooltip>
</div> </div>
<div class="flex flex-row items-center gap-x-0.25 w-12.25"> <div class="flex flex-row items-center gap-x-0.25">
<NcDropdown <NcDropdown
:visible="isBasesOptionsOpen[source!.id!]" :visible="isBasesOptionsOpen[source!.id!]"
:trigger="['click']" :trigger="['click']"
@ -708,7 +731,7 @@ const onTableIdCopy = async () => {
<NcButton <NcButton
v-e="['c:source:options']" v-e="['c:source:options']"
class="nc-sidebar-node-btn" class="nc-sidebar-node-btn"
:class="{ '!text-black !opacity-100': isBasesOptionsOpen[source!.id!] }" :class="{ '!text-black !opacity-100 !inline-block': isBasesOptionsOpen[source!.id!] }"
type="text" type="text"
size="xxsmall" size="xxsmall"
@click.stop="isBasesOptionsOpen[source!.id!] = !isBasesOptionsOpen[source!.id!]" @click.stop="isBasesOptionsOpen[source!.id!] = !isBasesOptionsOpen[source!.id!]"
@ -743,6 +766,7 @@ const onTableIdCopy = async () => {
type="text" type="text"
size="xxsmall" size="xxsmall"
class="nc-sidebar-node-btn" class="nc-sidebar-node-btn"
:class="{ '!opacity-100 !inline-block': isBasesOptionsOpen[source!.id!] }"
@click.stop="openTableCreateDialog(sourceIndex)" @click.stop="openTableCreateDialog(sourceIndex)"
> >
<GeneralIcon icon="plus" class="text-xl leading-5" style="-webkit-text-stroke: 0.15px" /> <GeneralIcon icon="plus" class="text-xl leading-5" style="-webkit-text-stroke: 0.15px" />
@ -770,7 +794,7 @@ const onTableIdCopy = async () => {
<NcMenu <NcMenu
class="!py-0 rounded text-sm" class="!py-0 rounded text-sm"
:class="{ :class="{
'!min-w-70': contextMenuTarget.type === 'table', '!min-w-62.5': contextMenuTarget.type === 'table',
}" }"
> >
<template v-if="contextMenuTarget.type === 'base' && base.type === 'database'"></template> <template v-if="contextMenuTarget.type === 'base' && base.type === 'database'"></template>
@ -845,7 +869,11 @@ const onTableIdCopy = async () => {
<style lang="scss" scoped> <style lang="scss" scoped>
:deep(.ant-collapse-header) { :deep(.ant-collapse-header) {
@apply !mx-0 !pl-8.75 !xs:(pl-8) !pr-0.5 !py-0.5 hover:bg-gray-200 xs:(hover:bg-gray-50 ) !rounded-md; @apply !mx-0 !pl-8.75 h-7.1 !xs:(pl-7 h-[3rem]) !pr-0.5 !py-0 hover:bg-gray-200 xs:(hover:bg-gray-50) !rounded-md;
.ant-collapse-arrow {
@apply !right-1 !xs:(flex-none border-1 border-gray-200 w-6.5 h-6.5 mr-1);
}
} }
:deep(.ant-collapse-item) { :deep(.ant-collapse-item) {
@ -856,7 +884,13 @@ const onTableIdCopy = async () => {
@apply !px-0 !pb-0 !pt-0.25; @apply !px-0 !pb-0 !pt-0.25;
} }
:deep(.ant-collapse-header:hover .nc-sidebar-source-node-btns) { :deep(.ant-collapse-header:hover) {
@apply visible; .nc-sidebar-node-btn {
@apply !opacity-100 !inline-block;
&:not(.nc-sidebar-expand) {
@apply !xs:hidden;
}
}
} }
</style> </style>

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

@ -137,8 +137,8 @@ const availableTables = computed(() => {
v-if="availableTables.length === 0" v-if="availableTables.length === 0"
class="py-0.5 text-gray-500" class="py-0.5 text-gray-500"
:class="{ :class="{
'ml-13.55': sourceIndex === 0, 'ml-8.5': sourceIndex === 0,
'ml-19.25': sourceIndex !== 0, 'ml-14.5 xs:(ml-15.25)': sourceIndex !== 0,
}" }"
> >
{{ $t('general.empty') }} {{ $t('general.empty') }}

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

@ -46,7 +46,7 @@ const { copy } = useCopy()
const baseRole = inject(ProjectRoleInj) const baseRole = inject(ProjectRoleInj)
provide(SidebarTableInj, table) provide(SidebarTableInj, table)
const { setMenuContext, openRenameTableDialog, duplicateTable } = inject(TreeViewInj)! const { setMenuContext, openRenameTableDialog: _openRenameTableDialog, duplicateTable: _duplicateTable } = inject(TreeViewInj)!
const { loadViews: _loadViews } = useViewsStore() const { loadViews: _loadViews } = useViewsStore()
const { activeView, activeViewTitleOrId, viewsByTable } = storeToRefs(useViewsStore()) const { activeView, activeViewTitleOrId, viewsByTable } = storeToRefs(useViewsStore())
@ -60,6 +60,8 @@ const openedTableId = computed(() => route.params.viewId)
const isTableDeleteDialogVisible = ref(false) const isTableDeleteDialogVisible = ref(false)
const isOptionsOpen = ref(false)
const setIcon = async (icon: string, table: TableType) => { const setIcon = async (icon: string, table: TableType) => {
try { try {
table.meta = { table.meta = {
@ -190,6 +192,21 @@ watch(openedTableId, () => {
}, 10000) }, 10000)
} }
}) })
const duplicateTable = (table: SidebarTableNode) => {
isOptionsOpen.value = false
_duplicateTable(table)
}
const openRenameTableDialog = (table: SidebarTableNode, sourceId: string) => {
isOptionsOpen.value = false
_openRenameTableDialog(table, !!sourceId)
}
const deleteTable = () => {
isOptionsOpen.value = false
isTableDeleteDialogVisible.value = true
}
</script> </script>
<template> <template>
@ -198,16 +215,16 @@ watch(openedTableId, () => {
:data-order="table.order" :data-order="table.order"
:data-id="table.id" :data-id="table.id"
:data-table-id="table.id" :data-table-id="table.id"
:class="[`nc-base-tree-tbl nc-base-tree-tbl-${table.title}`]" :class="[`nc-base-tree-tbl nc-base-tree-tbl-${table.title?.replaceAll(' ', '')}`]"
:data-active="openedTableId === table.id" :data-active="openedTableId === table.id"
> >
<div <div
v-e="['a:table:open']" v-e="['a:table:open']"
class="table-context flex items-center gap-1 h-full nc-tree-item-inner nc-sidebar-node pl-11 pr-0.75 mb-0.25 rounded-md h-7.1 w-full group cursor-pointer hover:bg-gray-200" class="table-context flex items-center gap-1 h-full nc-tree-item-inner nc-sidebar-node pr-0.75 mb-0.25 rounded-md h-7.1 w-full group cursor-pointer hover:bg-gray-200"
:class="{ :class="{
'hover:bg-gray-200': openedTableId !== table.id, 'hover:bg-gray-200': openedTableId !== table.id,
'pl-12 xs:(pl-14)': sourceIndex !== 0, 'pl-13.5': sourceIndex !== 0,
'pl-6.5': sourceIndex === 0, 'pl-7.5 xs:(pl-6)': sourceIndex === 0,
'!bg-primary-selected': isTableOpened, '!bg-primary-selected': isTableOpened,
}" }"
:data-testid="`nc-tbl-side-node-${table.title}`" :data-testid="`nc-tbl-side-node-${table.title}`"
@ -215,30 +232,10 @@ watch(openedTableId, () => {
@click="onOpenTable" @click="onOpenTable"
> >
<div class="flex flex-row h-full items-center"> <div class="flex flex-row h-full items-center">
<NcButton
v-e="['c:table:toggle-expand']"
type="text"
size="xxsmall"
class="nc-sidebar-node-btn nc-sidebar-expand"
@click.stop="onExpand"
>
<GeneralLoader
v-if="table.isViewsLoading"
class="flex w-4 h-4 !text-gray-600 !mt-0.75"
:class="{
'!visible': !isExpanded,
}"
/>
<GeneralIcon
v-else
icon="triangleFill"
class="nc-sidebar-source-node-btns group-hover:visible invisible cursor-pointer transform transition-transform duration-500 h-1.5 w-1.5 !text-gray-600 rotate-90"
:class="{ '!rotate-180': isExpanded }"
/>
</NcButton>
<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}`">
<GeneralLoader v-if="table.isViewsLoading" class="flex items-center w-6 h-full !text-gray-600" />
<div <div
v-else
v-e="['c:table:emoji-picker']" v-e="['c:table:emoji-picker']"
class="flex items-center nc-table-icon" class="flex items-center nc-table-icon"
:class="{ :class="{
@ -262,10 +259,11 @@ watch(openedTableId, () => {
<component <component
:is="iconMap.table" :is="iconMap.table"
v-if="table.type === 'table'" v-if="table.type === 'table'"
class="flex w-5 !text-gray-500 text-sm" class="w-4 text-sm"
:class="{ :class="{
'group-hover:text-gray-500': isUIAllowed('tableSort', { roles: baseRole }), '!group-hover:text-gray-700': isUIAllowed('tableSort', { roles: baseRole }),
'!text-black': openedTableId === table.id, '!text-gray-700': openedTableId === table.id,
'!text-gray-600/75': openedTableId !== table.id,
}" }"
/> />
@ -284,7 +282,7 @@ watch(openedTableId, () => {
</div> </div>
</div> </div>
<NcTooltip <NcTooltip
class="nc-tbl-title nc-sidebar-node-title text-ellipsis w-full overflow-hidden select-none" class="nc-tbl-title nc-sidebar-node-title text-ellipsis overflow-hidden select-none !flex-1"
show-on-truncate-only show-on-truncate-only
> >
<template #title>{{ table.title }}</template> <template #title>{{ table.title }}</template>
@ -298,88 +296,110 @@ watch(openedTableId, () => {
{{ table.title }} {{ table.title }}
</span> </span>
</NcTooltip> </NcTooltip>
<div class="flex flex-grow h-full"></div>
<div class="flex flex-row items-center">
<div v-e="['c:table:option']">
<NcDropdown :trigger="['click']" class="nc-sidebar-node-btn" @click.stop>
<MdiDotsHorizontal
data-testid="nc-sidebar-table-context-menu"
class="min-w-5.75 min-h-5.75 mt-0.2 mr-0.25 px-0.5 !text-gray-600 transition-opacity opacity-0 group-hover:opacity-100 nc-tbl-context-menu outline-0 rounded-md hover:(bg-gray-500 bg-opacity-15 !text-black)"
/>
<template #overlay>
<NcMenu class="!min-w-70" :data-testid="`sidebar-table-context-menu-list-${table.title}`">
<NcTooltip>
<template #title> {{ $t('labels.clickToCopyTableID') }} </template>
<div
class="flex items-center justify-between p-2 mx-1.5 rounded-md cursor-pointer hover:bg-gray-100 group"
@click.stop="onTableIdCopy"
>
<div class="flex text-xs font-bold text-gray-500 ml-1">
{{
$t('labels.tableIdColon', {
tableId: table?.id,
})
}}
</div>
<NcButton class="!group-hover:bg-gray-100" size="xsmall" type="secondary">
<GeneralIcon v-if="isTableIdCopied" class="max-h-4 min-w-4" icon="check" />
<GeneralIcon v-else class="max-h-4 min-w-4" else icon="copy" />
</NcButton>
</div>
</NcTooltip>
<template <NcDropdown v-model:visible="isOptionsOpen" :trigger="['click']" @click.stop>
v-if=" <NcButton
!isSharedBase && v-e="['c:table:option']"
(isUIAllowed('tableRename', { roles: baseRole }) || isUIAllowed('tableDelete', { roles: baseRole })) class="nc-sidebar-node-btn nc-tbl-context-menu text-gray-600"
" :class="{
> '!opacity-100 !inline-block': isOptionsOpen,
<NcDivider /> }"
<NcMenuItem data-testid="nc-sidebar-table-context-menu"
v-if="isUIAllowed('tableRename', { roles: baseRole })" type="text"
:data-testid="`sidebar-table-rename-${table.title}`" size="xxsmall"
@click="openRenameTableDialog(table, base.sources[sourceIndex].id)" @click.stop
> >
<div v-e="['c:table:rename']" class="flex gap-2 items-center"> <MdiDotsHorizontal class="!text-gray-600" />
<GeneralIcon icon="rename" class="text-gray-700" /> </NcButton>
{{ $t('general.rename') }} {{ $t('objects.table') }}
</div> <template #overlay>
</NcMenuItem> <NcMenu class="!min-w-62.5" :data-testid="`sidebar-table-context-menu-list-${table.title}`">
<NcTooltip>
<NcMenuItem <template #title> {{ $t('labels.clickToCopyTableID') }} </template>
v-if=" <div
isUIAllowed('tableDuplicate') && class="flex items-center justify-between p-2 mx-1.5 rounded-md cursor-pointer hover:bg-gray-100 group"
base.sources?.[sourceIndex] && @click.stop="onTableIdCopy"
(base.sources[sourceIndex].is_meta || base.sources[sourceIndex].is_local) >
" <div class="flex text-xs font-bold text-gray-500 ml-1">
:data-testid="`sidebar-table-duplicate-${table.title}`" {{
@click="duplicateTable(table)" $t('labels.tableIdColon', {
> tableId: table?.id,
<div v-e="['c:table:duplicate']" class="flex gap-2 items-center"> })
<GeneralIcon icon="duplicate" class="text-gray-700" /> }}
{{ $t('general.duplicate') }} {{ $t('objects.table') }} </div>
</div> <NcButton class="!group-hover:bg-gray-100" size="xsmall" type="secondary">
</NcMenuItem> <GeneralIcon v-if="isTableIdCopied" class="max-h-4 min-w-4" icon="check" />
<GeneralIcon v-else class="max-h-4 min-w-4" else icon="copy" />
<NcDivider /> </NcButton>
<NcMenuItem </div>
v-if="isUIAllowed('tableDelete', { roles: baseRole })" </NcTooltip>
:data-testid="`sidebar-table-delete-${table.title}`"
class="!text-red-500 !hover:bg-red-50" <template
@click="isTableDeleteDialogVisible = true" v-if="
> !isSharedBase &&
<div v-e="['c:table:delete']" class="flex gap-2 items-center"> (isUIAllowed('tableRename', { roles: baseRole }) || isUIAllowed('tableDelete', { roles: baseRole }))
<GeneralIcon icon="delete" /> "
{{ $t('general.delete') }} {{ $t('objects.table') }} >
</div> <NcDivider />
</NcMenuItem> <NcMenuItem
</template> v-if="isUIAllowed('tableRename', { roles: baseRole })"
</NcMenu> :data-testid="`sidebar-table-rename-${table.title}`"
@click="openRenameTableDialog(table, base.sources[sourceIndex].id)"
>
<div v-e="['c:table:rename']" class="flex gap-2 items-center">
<GeneralIcon icon="rename" class="text-gray-700" />
{{ $t('general.rename') }} {{ $t('objects.table') }}
</div>
</NcMenuItem>
<NcMenuItem
v-if="
isUIAllowed('tableDuplicate') &&
base.sources?.[sourceIndex] &&
(base.sources[sourceIndex].is_meta || base.sources[sourceIndex].is_local)
"
:data-testid="`sidebar-table-duplicate-${table.title}`"
@click="duplicateTable(table)"
>
<div v-e="['c:table:duplicate']" class="flex gap-2 items-center">
<GeneralIcon icon="duplicate" class="text-gray-700" />
{{ $t('general.duplicate') }} {{ $t('objects.table') }}
</div>
</NcMenuItem>
<NcDivider />
<NcMenuItem
v-if="isUIAllowed('tableDelete', { roles: baseRole })"
:data-testid="`sidebar-table-delete-${table.title}`"
class="!text-red-500 !hover:bg-red-50"
@click="deleteTable"
>
<div v-e="['c:table:delete']" class="flex gap-2 items-center">
<GeneralIcon icon="delete" />
{{ $t('general.delete') }} {{ $t('objects.table') }}
</div>
</NcMenuItem>
</template> </template>
</NcDropdown> </NcMenu>
</div> </template>
</div> </NcDropdown>
<NcButton
v-e="['c:table:toggle-expand']"
type="text"
size="xxsmall"
class="nc-sidebar-node-btn nc-sidebar-expand"
:class="{
'!opacity-100 !visible': isOptionsOpen,
}"
@click.stop="onExpand"
>
<GeneralIcon
icon="chevronDown"
class="nc-sidebar-source-node-btns cursor-pointer transform transition-transform duration-500 !text-gray-600 rotate-270"
:class="{ '!rotate-180': isExpanded }"
/>
</NcButton>
</div> </div>
<DlgTableDelete <DlgTableDelete
v-if="table.id && base?.id" v-if="table.id && base?.id"

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

@ -413,8 +413,8 @@ function onOpenModal({
v-if="isUIAllowed('viewCreateOrEdit')" v-if="isUIAllowed('viewCreateOrEdit')"
:align-left-level="isDefaultSource ? 1 : 2" :align-left-level="isDefaultSource ? 1 : 2"
:class="{ :class="{
'!pl-18 !xs:(pl-19.75)': isDefaultSource, '!pl-13.3 !xs:(pl-13.5)': isDefaultSource,
'!pl-23.5 !xs:(pl-27)': !isDefaultSource, '!pl-18.6 !xs:(pl-20)': !isDefaultSource,
}" }"
> >
<div <div

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

@ -158,6 +158,10 @@ async function onRename() {
isDropdownOpen.value = false isDropdownOpen.value = false
if (!isEditing.value) return if (!isEditing.value) return
if (_title.value) {
_title.value = _title.value.trim()
}
const isValid = props.onValidate({ ...vModel.value, title: _title.value! }) const isValid = props.onValidate({ ...vModel.value, title: _title.value! })
if (isValid !== true) { if (isValid !== true) {
@ -217,8 +221,8 @@ watch(isDropdownOpen, async () => {
<a-menu-item <a-menu-item
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-13.5 !xs:(pl-12)': isDefaultBase,
'!pl-23.5 !xs:(pl-27)': !isDefaultBase, '!pl-19 ': !isDefaultBase,
}" }"
:data-testid="`view-sidebar-view-${vModel.alias || vModel.title}`" :data-testid="`view-sidebar-view-${vModel.alias || vModel.title}`"
@dblclick.stop="onDblClick" @dblclick.stop="onDblClick"
@ -248,7 +252,7 @@ watch(isDropdownOpen, async () => {
v-if="isEditing" v-if="isEditing"
:ref="focusInput" :ref="focusInput"
v-model:value="_title" v-model:value="_title"
class="!bg-transparent !border-0 !ring-0 !outline-transparent !border-transparent !pl-0" class="!bg-transparent !border-0 !ring-0 !outline-transparent !border-transparent !pl-0 !flex-1 mr-4"
:class="{ :class="{
'font-medium': activeView?.id === vModel.id, 'font-medium': activeView?.id === vModel.id,
}" }"
@ -267,7 +271,6 @@ watch(isDropdownOpen, async () => {
{{ vModel.alias || vModel.title }} {{ vModel.alias || vModel.title }}
</div> </div>
</NcTooltip> </NcTooltip>
<div class="flex-1" />
<template v-if="!isEditing && !isLocked"> <template v-if="!isEditing && !isLocked">
<NcDropdown v-model:visible="isDropdownOpen" overlay-class-name="!rounded-lg"> <NcDropdown v-model:visible="isDropdownOpen" overlay-class-name="!rounded-lg">
@ -275,11 +278,12 @@ watch(isDropdownOpen, async () => {
v-e="['c:view:option']" 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 opacity-100) nc-sidebar-view-node-context-btn"
:class="{ :class="{
'!visible': isDropdownOpen, '!visible !opacity-100': isDropdownOpen,
}" }"
@click.stop="isDropdownOpen = !isDropdownOpen" @click.stop="isDropdownOpen = !isDropdownOpen"
@dblclick.stop
> >
<GeneralIcon icon="threeDotHorizontal" class="text-xl w-4.75" /> <GeneralIcon icon="threeDotHorizontal" class="text-xl w-4.75" />
</NcButton> </NcButton>

12
packages/nc-gui/components/dashboard/View.vue

@ -143,8 +143,8 @@ function onResize(widthPercent: any) {
sideBarSize.value.old = ((16 * fontSize) / viewportWidth.value) * 100 sideBarSize.value.old = ((16 * fontSize) / viewportWidth.value) * 100
if (isLeftSidebarOpen.value) sideBarSize.value.current = sideBarSize.value.old if (isLeftSidebarOpen.value) sideBarSize.value.current = sideBarSize.value.old
return return
} else if (widthRem > 23.5) { } else if (widthRem > 35) {
sideBarSize.value.old = ((23.5 * fontSize) / viewportWidth.value) * 100 sideBarSize.value.old = ((35 * fontSize) / viewportWidth.value) * 100
if (isLeftSidebarOpen.value) sideBarSize.value.current = sideBarSize.value.old if (isLeftSidebarOpen.value) sideBarSize.value.current = sideBarSize.value.old
return return
@ -155,7 +155,7 @@ function onResize(widthPercent: any) {
} }
const normalizedWidth = computed(() => { const normalizedWidth = computed(() => {
const maxSize = remToPx(23.5) const maxSize = remToPx(35)
const minSize = remToPx(16) const minSize = remToPx(16)
if (sidebarWidth.value > maxSize) { if (sidebarWidth.value > maxSize) {
return maxSize return maxSize
@ -178,15 +178,15 @@ const normalizedWidth = computed(() => {
<Pane <Pane
min-size="15%" min-size="15%"
:size="mobileNormalizedSidebarSize" :size="mobileNormalizedSidebarSize"
max-size="40%" max-size="60%"
class="nc-sidebar-splitpane !sm:max-w-94 relative !overflow-visible flex" class="nc-sidebar-splitpane !sm:max-w-140 relative !overflow-visible flex"
:style="{ :style="{
width: `${mobileNormalizedSidebarSize}%`, width: `${mobileNormalizedSidebarSize}%`,
}" }"
> >
<div <div
ref="wrapperRef" ref="wrapperRef"
class="nc-sidebar-wrapper relative flex flex-col h-full justify-center !sm:(max-w-94) absolute overflow-visible" class="nc-sidebar-wrapper relative flex flex-col h-full justify-center !sm:(max-w-140) absolute overflow-visible"
:class="{ :class="{
'mobile': isMobileMode, 'mobile': isMobileMode,
'minimized-height': !isLeftSidebarOpen, 'minimized-height': !isLeftSidebarOpen,

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

@ -139,7 +139,7 @@ onMounted(() => {
<NcModal v-model:visible="dialogShow" :header="$t('activity.createTable')" size="small" @keydown.esc="dialogShow = false"> <NcModal v-model:visible="dialogShow" :header="$t('activity.createTable')" size="small" @keydown.esc="dialogShow = false">
<template #header> <template #header>
<div class="flex flex-row items-center gap-x-2"> <div class="flex flex-row items-center gap-x-2">
<GeneralIcon icon="table" /> <GeneralIcon icon="table" class="!text-gray-600/75" />
{{ $t('activity.createTable') }} {{ $t('activity.createTable') }}
</div> </div>
</template> </template>

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

@ -87,11 +87,10 @@ const validators = computed(() => {
{ {
validator: (rule: any, value: any) => { validator: (rule: any, value: any) => {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
if (/^\s+|\s+$/.test(value)) {
return reject(new Error('Leading or trailing whitespace not allowed in table name'))
}
if ( if (
!(tables?.value || []).every((t) => t.id === tableMeta.id || t.title.toLowerCase() !== (value || '').toLowerCase()) !(tables?.value || []).every(
(t) => t.id === tableMeta.id || t.title.toLowerCase() !== (value?.trim() || '').toLowerCase(),
)
) { ) {
return reject(new Error('Duplicate table alias')) return reject(new Error('Duplicate table alias'))
} }
@ -123,6 +122,11 @@ watchEffect(
const renameTable = async (undo = false, disableTitleDiffCheck?: boolean | undefined) => { const renameTable = async (undo = false, disableTitleDiffCheck?: boolean | undefined) => {
if (!tableMeta) return if (!tableMeta) return
if (formState.title) {
formState.title = formState.title.trim()
}
if (formState.title === tableMeta.title && !disableTitleDiffCheck) return if (formState.title === tableMeta.title && !disableTitleDiffCheck) return
loading.value = true loading.value = true
@ -216,7 +220,7 @@ const renameTable = async (undo = false, disableTitleDiffCheck?: boolean | undef
<NcButton <NcButton
key="submit" key="submit"
type="primary" type="primary"
:disabled="validateInfos.title.validateStatus === 'error' || formState.title === tableMeta.title" :disabled="validateInfos.title.validateStatus === 'error' || formState.title?.trim() === tableMeta.title"
label="Rename Table" label="Rename Table"
loading-label="Renaming Table" loading-label="Renaming Table"
:loading="loading" :loading="loading"

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

@ -162,6 +162,10 @@ async function onSubmit() {
console.error(e) console.error(e)
} }
if (form.title) {
form.title = form.title.trim()
}
if (isValid && form.type) { if (isValid && form.type) {
if (!tableId.value) return if (!tableId.value) return

2
packages/nc-gui/components/erd/HistogramPanel.vue

@ -10,7 +10,7 @@ import { iconMap } from '#imports'
> >
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex items-center gap-1.5 p-2"> <div class="flex items-center gap-1.5 p-2">
<component :is="iconMap.table" /> <component :is="iconMap.table" class="!text-gray-600/75" />
<div>{{ $t('objects.table') }}</div> <div>{{ $t('objects.table') }}</div>
</div> </div>

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

@ -32,7 +32,7 @@ const onClick = () => {
> >
<div class="flex items-center text-inherit"> <div class="flex items-center text-inherit">
<GeneralIcon v-if="isMobileMode" icon="menu" class="text-lg -mt-0.25" /> <GeneralIcon v-if="isMobileMode" icon="menu" class="text-lg -mt-0.25" />
<GeneralIcon v-else icon="doubleRightArrow" class="duration-150 transition-all !text-lg -mt-0.25" /> <GeneralIcon v-else icon="doubleRightArrow" class="duration-150 transition-all !text-lg -mt-0.25 !text-gray-500/75" />
</div> </div>
</NcButton> </NcButton>
</NcTooltip> </NcTooltip>

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

@ -17,6 +17,6 @@ const { meta: tableMeta } = defineProps<{
readonly readonly
/> />
<component :is="iconMap.eye" v-else-if="tableMeta?.type === 'view'" class="w-5 mx-0.75" /> <component :is="iconMap.eye" v-else-if="tableMeta?.type === 'view'" class="w-4 mx-0.75" />
<component :is="iconMap.table" v-else class="w-5 mx-0.5" /> <component :is="iconMap.table" v-else class="w-4 mx-0.5 !text-gray-600/75" />
</template> </template>

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

@ -234,7 +234,7 @@ watch(meta, async () => {
<component :is="iconMap.group" class="h-4 w-4" /> <component :is="iconMap.group" class="h-4 w-4" />
<!-- Group By --> <!-- Group By -->
<span v-if="!isMobileMode" class="text-capitalize !text-sm font-medium">{{ $t('activity.groupBy') }}</span> <span v-if="!isMobileMode" class="text-capitalize !text-sm font-medium">{{ $t('activity.group') }}</span>
<span v-if="groupedByColumnIds?.length" class="bg-brand-50 text-brand-500 py-1 px-2 text-md rounded-md">{{ <span v-if="groupedByColumnIds?.length" class="bg-brand-50 text-brand-500 py-1 px-2 text-md rounded-md">{{
groupedByColumnIds.length groupedByColumnIds.length

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

@ -33,8 +33,20 @@ const selectedView = inject(ActiveViewInj)
<div :class="{ 'show-tick': !hideTick }"> <div :class="{ 'show-tick': !hideTick }">
<div class="flex flex-col gap-y-1"> <div class="flex flex-col gap-y-1">
<div class="flex items-center gap-2 flex-grow"> <div class="flex items-center gap-2 flex-grow">
<component :is="types[type].icon" class="!w-4 !min-w-4 text-inherit !h-4" /> <component
<div class="flex"> :is="types[type].icon"
class="flex-none"
:class="{
'!w-3 h-3': hideTick,
'!w-4 h-4': !hideTick,
}"
/>
<div
class="flex"
:class="{
'text-xs py-0.7': hideTick,
}"
>
{{ $t(types[type].title) }} {{ $t(types[type].title) }}
</div> </div>
<div v-if="!hideTick" class="flex flex-grow"></div> <div v-if="!hideTick" class="flex flex-grow"></div>

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

@ -55,6 +55,10 @@ const formState = ref({
const creating = ref(false) const creating = ref(false)
const createProject = async () => { const createProject = async () => {
if (formState.value.title) {
formState.value.title = formState.value.title.trim()
}
creating.value = true creating.value = true
try { try {
const base = await _createProject({ const base = await _createProject({

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

@ -146,6 +146,11 @@ export function useTableNew(param: { onTableCreate?: (tableMeta: TableType) => v
const createTable = async () => { const createTable = async () => {
const { onTableCreate, baseId } = param const { onTableCreate, baseId } = param
if (table.title) {
table.title = table.title.trim()
}
let { sourceId } = param let { sourceId } = param
if (!(baseId in bases.value)) { if (!(baseId in bases.value)) {

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

@ -946,7 +946,8 @@
"hidden": "Hide pre-filled fields", "hidden": "Hide pre-filled fields",
"lockedFieldTooltip": "Pre-filled value" "lockedFieldTooltip": "Pre-filled value"
}, },
"getPreFilledLink": "Get Pre-filled Link" "getPreFilledLink": "Get Pre-filled Link",
"group": "Group"
}, },
"tooltip": { "tooltip": {
"reachedSourceLimit": "Limited to only one data source for the moment", "reachedSourceLimit": "Limited to only one data source for the moment",

31
tests/playwright/pages/Dashboard/Sidebar/ProjectNode/index.ts

@ -12,7 +12,7 @@ export class SidebarProjectNodeObject extends BasePage {
} }
get({ baseTitle }: { baseTitle: string }) { get({ baseTitle }: { baseTitle: string }) {
return this.sidebar.get().getByTestId(`nc-sidebar-base-${baseTitle}`); return this.sidebar.get().getByTestId(`nc-sidebar-base-title-${baseTitle}`);
} }
async click({ baseTitle }: { baseTitle: string }) { async click({ baseTitle }: { baseTitle: string }) {
@ -22,6 +22,10 @@ export class SidebarProjectNodeObject extends BasePage {
} }
async clickOptions({ baseTitle }: { baseTitle: string }) { async clickOptions({ baseTitle }: { baseTitle: string }) {
await this.get({
baseTitle,
}).hover();
await this.get({ await this.get({
baseTitle, baseTitle,
}) })
@ -30,14 +34,21 @@ export class SidebarProjectNodeObject extends BasePage {
} }
async verifyTableAddBtn({ baseTitle, visible }: { baseTitle: string; visible: boolean }) { async verifyTableAddBtn({ baseTitle, visible }: { baseTitle: string; visible: boolean }) {
const addBtn = await this.get({ await this.get({
baseTitle,
}).waitFor({ state: 'visible' });
await this.get({
baseTitle,
}).scrollIntoViewIfNeeded();
await this.get({
baseTitle,
}).hover();
const addBtn = this.get({
baseTitle, baseTitle,
}).getByTestId('nc-sidebar-add-base-entity'); }).getByTestId('nc-sidebar-add-base-entity');
if (visible) { if (visible) {
await addBtn.hover({
force: true,
});
await expect(addBtn).toBeVisible(); await expect(addBtn).toBeVisible();
} else await expect(addBtn).toHaveCount(0); } else await expect(addBtn).toHaveCount(0);
} }
@ -65,6 +76,16 @@ export class SidebarProjectNodeObject extends BasePage {
deleteVisible?: boolean; deleteVisible?: boolean;
copyProjectInfoVisible?: boolean; copyProjectInfoVisible?: boolean;
}) { }) {
await this.get({
baseTitle,
}).waitFor({ state: 'visible' });
await this.get({
baseTitle,
}).scrollIntoViewIfNeeded();
await this.get({
baseTitle,
}).hover();
const renameLocator = await this.rootPage const renameLocator = await this.rootPage
.getByTestId(`nc-sidebar-base-${baseTitle}-options`) .getByTestId(`nc-sidebar-base-${baseTitle}-options`)
.getByTestId('nc-sidebar-base-rename'); .getByTestId('nc-sidebar-base-rename');

71
tests/playwright/pages/Dashboard/TreeView.ts

@ -27,11 +27,14 @@ export class TreeViewPage extends BasePage {
.locator('[data-testid="nc-sidebar-add-base-entity"]'); .locator('[data-testid="nc-sidebar-add-base-entity"]');
} }
private getProjectContextMenu({ baseTitle }: { baseTitle: string }) { private async openProjectContextMenu({ baseTitle }: { baseTitle: string }) {
return this.dashboard await this.dashboard.get().getByTestId(`nc-sidebar-base-title-${baseTitle}`).hover();
await this.dashboard
.get() .get()
.getByTestId(`nc-sidebar-base-title-${baseTitle}`) .getByTestId(`nc-sidebar-base-title-${baseTitle}`)
.locator('[data-testid="nc-sidebar-context-menu"]'); .locator('[data-testid="nc-sidebar-context-menu"]')
.click();
} }
async isVisible() { async isVisible() {
@ -61,7 +64,9 @@ export class TreeViewPage extends BasePage {
} }
async focusTable({ title }: { title: string }) { async focusTable({ title }: { title: string }) {
await this.get().locator(`.nc-base-tree-tbl-${title}`).focus(); await this.get()
.locator(`.nc-base-tree-tbl-${title.replace(/ /g, '')}`)
.focus();
} }
async openBase({ title }: { title: string }) { async openBase({ title }: { title: string }) {
@ -78,6 +83,16 @@ export class TreeViewPage extends BasePage {
return this.get().locator('.nc-tree-item').nth(index); return this.get().locator('.nc-tree-item').nth(index);
} }
async waitForTableOptions({ title }: { title: string }) {
const tableTitle = title.replace(/ /g, '');
await this.get().locator(`.nc-base-tree-tbl-${tableTitle}`).waitFor({ state: 'visible' });
await this.get().locator(`.nc-base-tree-tbl-${tableTitle}`).scrollIntoViewIfNeeded();
await this.get().locator(`.nc-base-tree-tbl-${tableTitle}`).getByTestId(`nc-tbl-title-${title}`).hover();
}
// assumption: first view rendered is always GRID // assumption: first view rendered is always GRID
// //
async openTable({ async openTable({
@ -164,10 +179,11 @@ export class TreeViewPage extends BasePage {
} }
async deleteTable({ title }: { title: string }) { async deleteTable({ title }: { title: string }) {
await this.get().locator(`.nc-base-tree-tbl-${title}`).waitFor({ state: 'visible' }); const tableTitle = title.replace(/ /g, '');
await this.get().locator(`.nc-base-tree-tbl-${title}`).scrollIntoViewIfNeeded(); await this.waitForTableOptions({ title });
await this.get().locator(`.nc-base-tree-tbl-${title}`).locator('.nc-tbl-context-menu').click();
await this.get().locator(`.nc-base-tree-tbl-${tableTitle}`).locator('.nc-tbl-context-menu').click();
await this.rootPage.locator('.ant-dropdown').locator('.nc-menu-item:has-text("Delete")').click(); await this.rootPage.locator('.ant-dropdown').locator('.nc-menu-item:has-text("Delete")').click();
await this.waitForResponse({ await this.waitForResponse({
@ -186,9 +202,11 @@ export class TreeViewPage extends BasePage {
} }
async renameTable({ title, newTitle }: { title: string; newTitle: string }) { async renameTable({ title, newTitle }: { title: string; newTitle: string }) {
await this.get().locator(`.nc-base-tree-tbl-${title}`).waitFor({ state: 'visible' }); const tableTitle = title.replace(/ /g, '');
await this.get().locator(`.nc-base-tree-tbl-${title}`).scrollIntoViewIfNeeded();
await this.get().locator(`.nc-base-tree-tbl-${title}`).locator('.nc-tbl-context-menu').click(); await this.waitForTableOptions({ title });
await this.get().locator(`.nc-base-tree-tbl-${tableTitle}`).locator('.nc-tbl-context-menu').click();
await this.rootPage.locator('.ant-dropdown').locator('.nc-menu-item:has-text("Rename")').click(); await this.rootPage.locator('.ant-dropdown').locator('.nc-menu-item:has-text("Rename")').click();
await this.dashboard.get().locator('[placeholder="Enter table name"]').fill(newTitle); await this.dashboard.get().locator('[placeholder="Enter table name"]').fill(newTitle);
@ -204,8 +222,7 @@ export class TreeViewPage extends BasePage {
} }
async baseSettings({ title }: { title?: string }) { async baseSettings({ title }: { title?: string }) {
await this.getProjectContextMenu({ baseTitle: title }).hover(); await this.openProjectContextMenu({ baseTitle: title });
await this.getProjectContextMenu({ baseTitle: title }).click();
const settingsMenu = this.dashboard.get().locator('.ant-dropdown-menu.nc-scrollbar-md'); const settingsMenu = this.dashboard.get().locator('.ant-dropdown-menu.nc-scrollbar-md');
await settingsMenu.locator(`.nc-sidebar-base-base-settings`).click(); await settingsMenu.locator(`.nc-sidebar-base-base-settings`).click();
} }
@ -213,8 +230,7 @@ export class TreeViewPage extends BasePage {
async quickImport({ title, baseTitle, context }: { title: string; baseTitle: string; context: NcContext }) { async quickImport({ title, baseTitle, context }: { title: string; baseTitle: string; context: NcContext }) {
baseTitle = this.scopedProjectTitle({ title: baseTitle, context }); baseTitle = this.scopedProjectTitle({ title: baseTitle, context });
await this.getProjectContextMenu({ baseTitle }).hover(); await this.openProjectContextMenu({ baseTitle });
await this.getProjectContextMenu({ baseTitle }).click();
const importMenu = this.dashboard.get().locator('.ant-dropdown-menu'); const importMenu = this.dashboard.get().locator('.ant-dropdown-menu');
await importMenu.locator(`.nc-sub-menu:has-text("Import Data")`).click(); await importMenu.locator(`.nc-sub-menu:has-text("Import Data")`).click();
await this.rootPage.locator(`.ant-dropdown-menu-item:has-text("${title}")`).waitFor(); await this.rootPage.locator(`.ant-dropdown-menu-item:has-text("${title}")`).waitFor();
@ -222,18 +238,24 @@ export class TreeViewPage extends BasePage {
} }
async changeTableIcon({ title, icon, iconDisplay }: { title: string; icon: string; iconDisplay?: string }) { async changeTableIcon({ title, icon, iconDisplay }: { title: string; icon: string; iconDisplay?: string }) {
await this.get().locator(`.nc-base-tree-tbl-${title} .nc-table-icon`).click(); const tableTitle = title.replace(/ /g, '');
await this.get().locator(`.nc-base-tree-tbl-${tableTitle} .nc-table-icon`).click();
await this.rootPage.locator('.emoji-mart-search > input').fill(icon); await this.rootPage.locator('.emoji-mart-search > input').fill(icon);
const emojiList = this.rootPage.locator('[id="emoji-mart-list"]'); const emojiList = this.rootPage.locator('[id="emoji-mart-list"]');
await emojiList.locator('button').first().click(); await emojiList.locator('button').first().click();
await expect( await expect(
this.get().locator(`.nc-base-tree-tbl-${title}`).locator(`.nc-table-icon:has-text("${iconDisplay}")`) this.get().locator(`.nc-base-tree-tbl-${tableTitle}`).locator(`.nc-table-icon:has-text("${iconDisplay}")`)
).toHaveCount(1); ).toHaveCount(1);
} }
async duplicateTable(title: string, includeData = true, includeViews = true) { async duplicateTable(title: string, includeData = true, includeViews = true) {
await this.get().locator(`.nc-base-tree-tbl-${title}`).locator('.nc-icon.ant-dropdown-trigger').click(); const tableTitle = title.replace(/ /g, '');
await this.waitForTableOptions({ title });
await this.get().locator(`.nc-base-tree-tbl-${tableTitle}`).locator('.nc-icon.ant-dropdown-trigger').click();
await this.dashboard.get().locator('div.nc-base-menu-item:has-text("Duplicate")').click(); await this.dashboard.get().locator('div.nc-base-menu-item:has-text("Duplicate")').click();
// Find the checkbox element with the label "Include data" // Find the checkbox element with the label "Include data"
@ -261,9 +283,11 @@ export class TreeViewPage extends BasePage {
async verifyTabIcon({ title, icon, iconDisplay }: { title: string; icon: string; iconDisplay?: string }) { async verifyTabIcon({ title, icon, iconDisplay }: { title: string; icon: string; iconDisplay?: string }) {
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000));
await this.rootPage.locator(`.nc-base-tree-tbl-${title}`).waitFor({ state: 'visible' }); const tableTitle = title.replace(/ /g, '');
await this.rootPage.locator(`.nc-base-tree-tbl-${tableTitle}`).waitFor({ state: 'visible' });
await expect( await expect(
this.get().locator(`.nc-base-tree-tbl-${title}`).locator(`.nc-table-icon:has-text("${iconDisplay}")`) this.get().locator(`.nc-base-tree-tbl-${tableTitle}`).locator(`.nc-table-icon:has-text("${iconDisplay}")`)
).toHaveCount(1); ).toHaveCount(1);
} }
@ -322,8 +346,7 @@ export class TreeViewPage extends BasePage {
param.title = this.scopedProjectTitle({ title: param.title, context: param.context }); param.title = this.scopedProjectTitle({ title: param.title, context: param.context });
param.newTitle = this.scopedProjectTitle({ title: param.newTitle, context: param.context }); param.newTitle = this.scopedProjectTitle({ title: param.newTitle, context: param.context });
await this.getProjectContextMenu({ baseTitle: param.title }).hover(); await this.openProjectContextMenu({ baseTitle: param.title });
await this.getProjectContextMenu({ baseTitle: param.title }).click();
const contextMenu = this.dashboard.get().locator('.ant-dropdown-menu.nc-scrollbar-md:visible').last(); const contextMenu = this.dashboard.get().locator('.ant-dropdown-menu.nc-scrollbar-md:visible').last();
await contextMenu.waitFor(); await contextMenu.waitFor();
await contextMenu.locator(`.ant-dropdown-menu-item:has-text("Rename")`).click(); await contextMenu.locator(`.ant-dropdown-menu-item:has-text("Rename")`).click();
@ -337,8 +360,7 @@ export class TreeViewPage extends BasePage {
async deleteProject(param: { title: string; context: NcContext }) { async deleteProject(param: { title: string; context: NcContext }) {
param.title = this.scopedProjectTitle({ title: param.title, context: param.context }); param.title = this.scopedProjectTitle({ title: param.title, context: param.context });
await this.getProjectContextMenu({ baseTitle: param.title }).hover(); await this.openProjectContextMenu({ baseTitle: param.title });
await this.getProjectContextMenu({ baseTitle: param.title }).click();
const contextMenu = this.dashboard.get().locator('.ant-dropdown-menu.nc-scrollbar-md:visible').last(); const contextMenu = this.dashboard.get().locator('.ant-dropdown-menu.nc-scrollbar-md:visible').last();
await contextMenu.waitFor(); await contextMenu.waitFor();
await contextMenu.locator(`.ant-dropdown-menu-item:has-text("Delete")`).click(); await contextMenu.locator(`.ant-dropdown-menu-item:has-text("Delete")`).click();
@ -349,8 +371,7 @@ export class TreeViewPage extends BasePage {
async duplicateProject(param: { title: string; context: NcContext }) { async duplicateProject(param: { title: string; context: NcContext }) {
param.title = this.scopedProjectTitle({ title: param.title, context: param.context }); param.title = this.scopedProjectTitle({ title: param.title, context: param.context });
await this.getProjectContextMenu({ baseTitle: param.title }).hover(); await this.openProjectContextMenu({ baseTitle: param.title });
await this.getProjectContextMenu({ baseTitle: param.title }).click();
const contextMenu = this.dashboard.get().locator('.ant-dropdown-menu.nc-scrollbar-md:visible'); const contextMenu = this.dashboard.get().locator('.ant-dropdown-menu.nc-scrollbar-md:visible');
await contextMenu.waitFor(); await contextMenu.waitFor();
await contextMenu.locator(`.ant-dropdown-menu-item:has-text("Duplicate")`).click(); await contextMenu.locator(`.ant-dropdown-menu-item:has-text("Duplicate")`).click();

31
tests/playwright/pages/Dashboard/index.ts

@ -33,7 +33,6 @@ import { CalendarPage } from './Calendar';
export class DashboardPage extends BasePage { export class DashboardPage extends BasePage {
readonly base: any; readonly base: any;
readonly tablesSideBar: Locator; readonly tablesSideBar: Locator;
readonly baseMenuLink: Locator;
readonly workspaceMenuLink: Locator; readonly workspaceMenuLink: Locator;
readonly tabBar: Locator; readonly tabBar: Locator;
readonly treeView: TreeViewPage; readonly treeView: TreeViewPage;
@ -69,10 +68,6 @@ export class DashboardPage extends BasePage {
this.base = base; this.base = base;
this.tablesSideBar = rootPage.locator('.nc-treeview-container'); this.tablesSideBar = rootPage.locator('.nc-treeview-container');
this.workspaceMenuLink = rootPage.getByTestId('nc-base-menu'); this.workspaceMenuLink = rootPage.getByTestId('nc-base-menu');
this.baseMenuLink = rootPage
.locator(`.base-title-node:has-text("${base.title}")`)
.locator('[data-testid="nc-sidebar-context-menu"]')
.first();
this.tabBar = rootPage.locator('.nc-tab-bar'); this.tabBar = rootPage.locator('.nc-tab-bar');
this.treeView = new TreeViewPage(this, base); this.treeView = new TreeViewPage(this, base);
this.grid = new GridPage(this); this.grid = new GridPage(this);
@ -114,27 +109,37 @@ export class DashboardPage extends BasePage {
return this.rootPage.locator(`div.nc-base-menu-item:has-text("${title}")`); return this.rootPage.locator(`div.nc-base-menu-item:has-text("${title}")`);
} }
async clickOnBaseMenuLink() {
const baseMenuLocator = this.rootPage.locator(`.base-title-node:has-text("${this.base.title}")`).first();
await baseMenuLocator.waitFor({ state: 'visible' });
await baseMenuLocator.scrollIntoViewIfNeeded();
await baseMenuLocator.hover();
await baseMenuLocator.locator('[data-testid="nc-sidebar-context-menu"]').first().click();
}
async verifyTeamAndSettingsLinkIsVisible() { async verifyTeamAndSettingsLinkIsVisible() {
await this.baseMenuLink.click(); await this.clickOnBaseMenuLink();
const teamAndSettingsLink = this.getProjectMenuLink({ title: ' Team & Settings' }); const teamAndSettingsLink = this.getProjectMenuLink({ title: ' Team & Settings' });
await expect(teamAndSettingsLink).toBeVisible(); await expect(teamAndSettingsLink).toBeVisible();
await this.baseMenuLink.click(); await this.clickOnBaseMenuLink();
} }
async verifyTeamAndSettingsLinkIsNotVisible() { async verifyTeamAndSettingsLinkIsNotVisible() {
await this.baseMenuLink.click(); await this.clickOnBaseMenuLink();
const teamAndSettingsLink = this.getProjectMenuLink({ title: ' Team & Settings' }); const teamAndSettingsLink = this.getProjectMenuLink({ title: ' Team & Settings' });
await expect(teamAndSettingsLink).not.toBeVisible(); await expect(teamAndSettingsLink).not.toBeVisible();
await this.baseMenuLink.click(); await this.clickOnBaseMenuLink();
} }
async gotoSettings() { async gotoSettings() {
await this.baseMenuLink.click(); await this.clickOnBaseMenuLink();
await this.rootPage.locator('.ant-dropdown').locator(`.nc-menu-item:has-text("Settings")`).click(); await this.rootPage.locator('.ant-dropdown').locator(`.nc-menu-item:has-text("Settings")`).click();
} }
async gotoProjectSubMenu({ title }: { title: string }) { async gotoProjectSubMenu({ title }: { title: string }) {
await this.baseMenuLink.click(); await this.clickOnBaseMenuLink();
await this.rootPage.locator(`div.nc-base-menu-item:has-text("${title}")`).click(); await this.rootPage.locator(`div.nc-base-menu-item:has-text("${title}")`).click();
} }
@ -179,10 +184,10 @@ export class DashboardPage extends BasePage {
// When a tab is opened, it is not always immediately visible. // When a tab is opened, it is not always immediately visible.
async toggleMobileMode() { async toggleMobileMode() {
await this.baseMenuLink.click(); await this.clickOnBaseMenuLink();
const projMenu = this.rootPage.locator('.nc-dropdown-base-menu'); const projMenu = this.rootPage.locator('.nc-dropdown-base-menu');
await projMenu.locator('[data-menu-id="mobile-mode"]:visible').click(); await projMenu.locator('[data-menu-id="mobile-mode"]:visible').click();
await this.baseMenuLink.click(); await this.clickOnBaseMenuLink();
} }
async signOut() { async signOut() {

Loading…
Cancel
Save