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. 122
      packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
  7. 4
      packages/nc-gui/components/dashboard/TreeView/TableList.vue
  8. 104
      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">
<g clip-path="url(#clip0_1613_80692)">
<path d="M11.8571 5.96903L4.14285 10.4225" stroke="#374151" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="1.15184" width="9.06208" height="9.06208" rx="1.335" transform="matrix(0.866044 -0.499967 0.866044 0.499967 -0.845705 8.77156)" stroke="#374151" stroke-width="1.33"/>
<path d="M3.5 6.34009L11.2143 10.7935" stroke="#374151" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_1613_80692">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
<path
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"
stroke="currentColor" stroke-width="1.33" />
<path
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"
stroke="currentColor" stroke-width="1.33" />
<path d="M4.5 5.02069L11.5 9.06179" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round"
stroke-linejoin="round" />
<path d="M4.5 9.06152L11.5 5.02042" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round"
stroke-linejoin="round" />
<line x1="7.995" y1="11" x2="7.995" y2="14" stroke="currentColor" stroke-width="1.33" />
</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) {
@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 {
@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 {
@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
v-else
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="{
'transform rotate-180': !isLeftSidebarOpen,
}"

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

@ -54,17 +54,17 @@ onMounted(() => {
</script>
<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">
<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"
>
<GeneralUserIcon :email="user?.email" size="base" :name="user?.display_name" />
<GeneralUserIcon :email="user?.email" size="auto" :name="user?.display_name" />
<div class="flex truncate">
{{ name ? name : user?.email }}
</div>
<GeneralIcon icon="arrowUp" class="!min-w-5" />
<GeneralIcon icon="chevronDown" class="flex-none !min-w-5 transform rotate-180 !text-gray-500" />
</div>
<template #overlay>
<NcMenu data-testid="nc-sidebar-userinfo">
@ -175,8 +175,7 @@ onMounted(() => {
</template>
</NcDropdown>
<template v-if="isMobileMode"></template>
<div v-else-if="appInfo.ee" class="text-gray-500 text-xs pl-3 mt-1">© 2023 NocoDB. Inc</div>
<template v-if="isMobileMode || appInfo.ee"></template>
<div v-else class="flex flex-row w-full justify-between pt-0.5 truncate">
<GeneralJoinCloud />
</div>
@ -189,7 +188,6 @@ onMounted(() => {
}
.menu-icon {
@apply w-4 h-4;
line-height: 1rem;
font-size: 1rem;
}

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

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

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

@ -134,6 +134,10 @@ const enableEditMode = () => {
}
const updateProjectTitle = async () => {
if (tempTitle.value) {
tempTitle.value = tempTitle.value.trim()
}
if (!tempTitle.value) return
try {
@ -431,22 +435,8 @@ const onTableIdCopy = async () => {
'hover:bg-gray-200': !(activeProjectId === base.id && baseViewOpen),
}"
: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 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" />
@ -469,7 +459,7 @@ const onTableIdCopy = async () => {
v-if="editMode"
ref="input"
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 }"
@click.stop
@keyup.enter="updateProjectTitle"
@ -478,23 +468,24 @@ const onTableIdCopy = async () => {
/>
<NcTooltip
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' }"
:class="{ 'text-black font-semibold': activeProjectId === base.id && baseViewOpen }"
show-on-truncate-only
@click="onProjectClick(base)"
>
<template #title>{{ base.title }}</template>
<span @click="onProjectClick(base)">
<span>
{{ base.title }}
</span>
</NcTooltip>
<div :class="{ 'flex flex-grow h-full': !editMode }" @click="onProjectClick(base)"></div>
<template v-if="!editMode">
<NcDropdown v-if="!isSharedBase" v-model:visible="isOptionsOpen" :trigger="['click']">
<NcButton
v-e="['c:base:options']"
class="nc-sidebar-node-btn"
:class="{ '!text-black !opacity-100': isOptionsOpen }"
:class="{ '!text-black !opacity-100 !inline-block': isOptionsOpen }"
data-testid="nc-sidebar-context-menu"
type="text"
size="xxsmall"
@ -620,12 +611,33 @@ const onTableIdCopy = async () => {
size="xxsmall"
type="text"
data-testid="nc-sidebar-add-base-entity"
:class="{ '!text-black !visible': isAddNewProjectChildEntityLoading, '!visible': isOptionsOpen }"
:class="{
'!text-black !inline-block !opacity-100': isAddNewProjectChildEntityLoading,
'!inline-block !opacity-100': isOptionsOpen,
}"
:loading="isAddNewProjectChildEntityLoading"
@click.stop="addNewProjectChildEntity"
>
<GeneralIcon icon="plus" class="text-xl leading-5" style="-webkit-text-stroke: 0.15px" />
</NcButton>
<NcButton
v-e="['c:base:expand']"
type="text"
size="xxsmall"
class="nc-sidebar-node-btn nc-sidebar-expand !xs:opacity-100"
:class="{
'!opacity-100': isOptionsOpen,
}"
@click="onProjectClick(base, true, true)"
>
<GeneralIcon
icon="chevronDown"
class="group-hover:visible cursor-pointer transform transition-transform duration-500 rotate-270"
:class="{ '!rotate-180': base.isExpanded }"
/>
</NcButton>
</template>
</div>
</div>
@ -658,25 +670,29 @@ const onTableIdCopy = async () => {
ghost
>
<template #expandIcon="{ isActive }">
<div
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)"
<NcButton
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
icon="triangleFill"
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"
icon="chevronDown"
class="flex-none cursor-pointer transform transition-transform duration-500 rotate-270"
:class="{ '!rotate-180': isActive }"
/>
</div>
</NcButton>
</template>
<a-collapse-panel :key="`collapse-${source.id}`">
<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
v-if="sourceIndex === 0"
class="source-context flex items-center gap-2 text-gray-800 nc-sidebar-node-title"
@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') }}
</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"
@contextmenu="setMenuContext('source', source)"
>
<GeneralBaseLogo class="min-w-4 !xs:(min-w-4.25 w-4.25 text-sm)" />
<div
:data-testid="`nc-sidebar-base-${source.alias}`"
class="nc-sidebar-node-title flex capitalize text-ellipsis overflow-hidden select-none"
<GeneralBaseLogo
class="flex-none min-w-4 !xs:(min-w-4.25 w-4.25 text-sm) !text-gray-600 !group-hover:text-gray-800"
/>
<NcTooltip
class="nc-sidebar-node-title capitalize text-ellipsis overflow-hidden select-none"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
:class="{
'text-black font-semibold': activeProjectId === base.id && baseViewOpen && !isMobileMode,
}"
show-on-truncate-only
>
<template #title> {{ source.alias || '' }}</template>
<span :data-testid="`nc-sidebar-base-${source.alias}`">
{{ source.alias || '' }}
</div>
<a-tooltip class="xs:(hidden)">
</span>
</NcTooltip>
<NcTooltip class="xs:(hidden) flex items-center mr-1">
<template #title>{{ $t('objects.externalDb') }}</template>
<div>
<GeneralIcon icon="info" class="text-gray-400 -mt-0.5 hover:text-gray-700 mr-1" />
</div>
</a-tooltip>
<GeneralIcon icon="info" class="flex-none text-gray-400 hover:text-gray-700 mr-1" />
</NcTooltip>
</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
:visible="isBasesOptionsOpen[source!.id!]"
:trigger="['click']"
@ -708,7 +731,7 @@ const onTableIdCopy = async () => {
<NcButton
v-e="['c:source:options']"
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"
size="xxsmall"
@click.stop="isBasesOptionsOpen[source!.id!] = !isBasesOptionsOpen[source!.id!]"
@ -743,6 +766,7 @@ const onTableIdCopy = async () => {
type="text"
size="xxsmall"
class="nc-sidebar-node-btn"
:class="{ '!opacity-100 !inline-block': isBasesOptionsOpen[source!.id!] }"
@click.stop="openTableCreateDialog(sourceIndex)"
>
<GeneralIcon icon="plus" class="text-xl leading-5" style="-webkit-text-stroke: 0.15px" />
@ -770,7 +794,7 @@ const onTableIdCopy = async () => {
<NcMenu
class="!py-0 rounded text-sm"
:class="{
'!min-w-70': contextMenuTarget.type === 'table',
'!min-w-62.5': contextMenuTarget.type === 'table',
}"
>
<template v-if="contextMenuTarget.type === 'base' && base.type === 'database'"></template>
@ -845,7 +869,11 @@ const onTableIdCopy = async () => {
<style lang="scss" scoped>
: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) {
@ -856,7 +884,13 @@ const onTableIdCopy = async () => {
@apply !px-0 !pb-0 !pt-0.25;
}
:deep(.ant-collapse-header:hover .nc-sidebar-source-node-btns) {
@apply visible;
:deep(.ant-collapse-header:hover) {
.nc-sidebar-node-btn {
@apply !opacity-100 !inline-block;
&:not(.nc-sidebar-expand) {
@apply !xs:hidden;
}
}
}
</style>

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

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

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

@ -46,7 +46,7 @@ const { copy } = useCopy()
const baseRole = inject(ProjectRoleInj)
provide(SidebarTableInj, table)
const { setMenuContext, openRenameTableDialog, duplicateTable } = inject(TreeViewInj)!
const { setMenuContext, openRenameTableDialog: _openRenameTableDialog, duplicateTable: _duplicateTable } = inject(TreeViewInj)!
const { loadViews: _loadViews } = useViewsStore()
const { activeView, activeViewTitleOrId, viewsByTable } = storeToRefs(useViewsStore())
@ -60,6 +60,8 @@ const openedTableId = computed(() => route.params.viewId)
const isTableDeleteDialogVisible = ref(false)
const isOptionsOpen = ref(false)
const setIcon = async (icon: string, table: TableType) => {
try {
table.meta = {
@ -190,6 +192,21 @@ watch(openedTableId, () => {
}, 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>
<template>
@ -198,16 +215,16 @@ watch(openedTableId, () => {
:data-order="table.order"
:data-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"
>
<div
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="{
'hover:bg-gray-200': openedTableId !== table.id,
'pl-12 xs:(pl-14)': sourceIndex !== 0,
'pl-6.5': sourceIndex === 0,
'pl-13.5': sourceIndex !== 0,
'pl-7.5 xs:(pl-6)': sourceIndex === 0,
'!bg-primary-selected': isTableOpened,
}"
:data-testid="`nc-tbl-side-node-${table.title}`"
@ -215,30 +232,10 @@ watch(openedTableId, () => {
@click="onOpenTable"
>
<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}`">
<GeneralLoader v-if="table.isViewsLoading" class="flex items-center w-6 h-full !text-gray-600" />
<div
v-else
v-e="['c:table:emoji-picker']"
class="flex items-center nc-table-icon"
:class="{
@ -262,10 +259,11 @@ watch(openedTableId, () => {
<component
:is="iconMap.table"
v-if="table.type === 'table'"
class="flex w-5 !text-gray-500 text-sm"
class="w-4 text-sm"
:class="{
'group-hover:text-gray-500': isUIAllowed('tableSort', { roles: baseRole }),
'!text-black': openedTableId === table.id,
'!group-hover:text-gray-700': isUIAllowed('tableSort', { roles: baseRole }),
'!text-gray-700': openedTableId === table.id,
'!text-gray-600/75': openedTableId !== table.id,
}"
/>
@ -284,7 +282,7 @@ watch(openedTableId, () => {
</div>
</div>
<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
>
<template #title>{{ table.title }}</template>
@ -298,17 +296,24 @@ watch(openedTableId, () => {
{{ table.title }}
</span>
</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
<NcDropdown v-model:visible="isOptionsOpen" :trigger="['click']" @click.stop>
<NcButton
v-e="['c:table:option']"
class="nc-sidebar-node-btn nc-tbl-context-menu text-gray-600"
:class="{
'!opacity-100 !inline-block': isOptionsOpen,
}"
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)"
/>
type="text"
size="xxsmall"
@click.stop
>
<MdiDotsHorizontal class="!text-gray-600" />
</NcButton>
<template #overlay>
<NcMenu class="!min-w-70" :data-testid="`sidebar-table-context-menu-list-${table.title}`">
<NcMenu class="!min-w-62.5" :data-testid="`sidebar-table-context-menu-list-${table.title}`">
<NcTooltip>
<template #title> {{ $t('labels.clickToCopyTableID') }} </template>
<div
@ -367,7 +372,7 @@ watch(openedTableId, () => {
v-if="isUIAllowed('tableDelete', { roles: baseRole })"
:data-testid="`sidebar-table-delete-${table.title}`"
class="!text-red-500 !hover:bg-red-50"
@click="isTableDeleteDialogVisible = true"
@click="deleteTable"
>
<div v-e="['c:table:delete']" class="flex gap-2 items-center">
<GeneralIcon icon="delete" />
@ -378,8 +383,23 @@ watch(openedTableId, () => {
</NcMenu>
</template>
</NcDropdown>
</div>
</div>
<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>
<DlgTableDelete
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')"
:align-left-level="isDefaultSource ? 1 : 2"
:class="{
'!pl-18 !xs:(pl-19.75)': isDefaultSource,
'!pl-23.5 !xs:(pl-27)': !isDefaultSource,
'!pl-13.3 !xs:(pl-13.5)': isDefaultSource,
'!pl-18.6 !xs:(pl-20)': !isDefaultSource,
}"
>
<div

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

@ -158,6 +158,10 @@ async function onRename() {
isDropdownOpen.value = false
if (!isEditing.value) return
if (_title.value) {
_title.value = _title.value.trim()
}
const isValid = props.onValidate({ ...vModel.value, title: _title.value! })
if (isValid !== true) {
@ -217,8 +221,8 @@ watch(isDropdownOpen, async () => {
<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="{
'!pl-18 !xs:(pl-19.75)': isDefaultBase,
'!pl-23.5 !xs:(pl-27)': !isDefaultBase,
'!pl-13.5 !xs:(pl-12)': isDefaultBase,
'!pl-19 ': !isDefaultBase,
}"
:data-testid="`view-sidebar-view-${vModel.alias || vModel.title}`"
@dblclick.stop="onDblClick"
@ -248,7 +252,7 @@ watch(isDropdownOpen, async () => {
v-if="isEditing"
:ref="focusInput"
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="{
'font-medium': activeView?.id === vModel.id,
}"
@ -267,7 +271,6 @@ watch(isDropdownOpen, async () => {
{{ vModel.alias || vModel.title }}
</div>
</NcTooltip>
<div class="flex-1" />
<template v-if="!isEditing && !isLocked">
<NcDropdown v-model:visible="isDropdownOpen" overlay-class-name="!rounded-lg">
@ -275,11 +278,12 @@ watch(isDropdownOpen, async () => {
v-e="['c:view:option']"
type="text"
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="{
'!visible': isDropdownOpen,
'!visible !opacity-100': isDropdownOpen,
}"
@click.stop="isDropdownOpen = !isDropdownOpen"
@dblclick.stop
>
<GeneralIcon icon="threeDotHorizontal" class="text-xl w-4.75" />
</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
if (isLeftSidebarOpen.value) sideBarSize.value.current = sideBarSize.value.old
return
} else if (widthRem > 23.5) {
sideBarSize.value.old = ((23.5 * fontSize) / viewportWidth.value) * 100
} else if (widthRem > 35) {
sideBarSize.value.old = ((35 * fontSize) / viewportWidth.value) * 100
if (isLeftSidebarOpen.value) sideBarSize.value.current = sideBarSize.value.old
return
@ -155,7 +155,7 @@ function onResize(widthPercent: any) {
}
const normalizedWidth = computed(() => {
const maxSize = remToPx(23.5)
const maxSize = remToPx(35)
const minSize = remToPx(16)
if (sidebarWidth.value > maxSize) {
return maxSize
@ -178,15 +178,15 @@ const normalizedWidth = computed(() => {
<Pane
min-size="15%"
:size="mobileNormalizedSidebarSize"
max-size="40%"
class="nc-sidebar-splitpane !sm:max-w-94 relative !overflow-visible flex"
max-size="60%"
class="nc-sidebar-splitpane !sm:max-w-140 relative !overflow-visible flex"
:style="{
width: `${mobileNormalizedSidebarSize}%`,
}"
>
<div
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="{
'mobile': isMobileMode,
'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">
<template #header>
<div class="flex flex-row items-center gap-x-2">
<GeneralIcon icon="table" />
<GeneralIcon icon="table" class="!text-gray-600/75" />
{{ $t('activity.createTable') }}
</div>
</template>

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

@ -87,11 +87,10 @@ const validators = computed(() => {
{
validator: (rule: any, value: any) => {
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 (
!(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'))
}
@ -123,6 +122,11 @@ watchEffect(
const renameTable = async (undo = false, disableTitleDiffCheck?: boolean | undefined) => {
if (!tableMeta) return
if (formState.title) {
formState.title = formState.title.trim()
}
if (formState.title === tableMeta.title && !disableTitleDiffCheck) return
loading.value = true
@ -216,7 +220,7 @@ const renameTable = async (undo = false, disableTitleDiffCheck?: boolean | undef
<NcButton
key="submit"
type="primary"
:disabled="validateInfos.title.validateStatus === 'error' || formState.title === tableMeta.title"
:disabled="validateInfos.title.validateStatus === 'error' || formState.title?.trim() === tableMeta.title"
label="Rename Table"
loading-label="Renaming Table"
:loading="loading"

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

@ -162,6 +162,10 @@ async function onSubmit() {
console.error(e)
}
if (form.title) {
form.title = form.title.trim()
}
if (isValid && form.type) {
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 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>

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

@ -32,7 +32,7 @@ const onClick = () => {
>
<div class="flex items-center text-inherit">
<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>
</NcButton>
</NcTooltip>

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

@ -17,6 +17,6 @@ const { meta: tableMeta } = defineProps<{
readonly
/>
<component :is="iconMap.eye" v-else-if="tableMeta?.type === 'view'" class="w-5 mx-0.75" />
<component :is="iconMap.table" v-else class="w-5 mx-0.5" />
<component :is="iconMap.eye" v-else-if="tableMeta?.type === 'view'" class="w-4 mx-0.75" />
<component :is="iconMap.table" v-else class="w-4 mx-0.5 !text-gray-600/75" />
</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" />
<!-- 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">{{
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="flex flex-col gap-y-1">
<div class="flex items-center gap-2 flex-grow">
<component :is="types[type].icon" class="!w-4 !min-w-4 text-inherit !h-4" />
<div class="flex">
<component
: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) }}
</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 createProject = async () => {
if (formState.value.title) {
formState.value.title = formState.value.title.trim()
}
creating.value = true
try {
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 { onTableCreate, baseId } = param
if (table.title) {
table.title = table.title.trim()
}
let { sourceId } = param
if (!(baseId in bases.value)) {

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

@ -946,7 +946,8 @@
"hidden": "Hide pre-filled fields",
"lockedFieldTooltip": "Pre-filled value"
},
"getPreFilledLink": "Get Pre-filled Link"
"getPreFilledLink": "Get Pre-filled Link",
"group": "Group"
},
"tooltip": {
"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 }) {
return this.sidebar.get().getByTestId(`nc-sidebar-base-${baseTitle}`);
return this.sidebar.get().getByTestId(`nc-sidebar-base-title-${baseTitle}`);
}
async click({ baseTitle }: { baseTitle: string }) {
@ -22,6 +22,10 @@ export class SidebarProjectNodeObject extends BasePage {
}
async clickOptions({ baseTitle }: { baseTitle: string }) {
await this.get({
baseTitle,
}).hover();
await this.get({
baseTitle,
})
@ -30,14 +34,21 @@ export class SidebarProjectNodeObject extends BasePage {
}
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,
}).getByTestId('nc-sidebar-add-base-entity');
if (visible) {
await addBtn.hover({
force: true,
});
await expect(addBtn).toBeVisible();
} else await expect(addBtn).toHaveCount(0);
}
@ -65,6 +76,16 @@ export class SidebarProjectNodeObject extends BasePage {
deleteVisible?: 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
.getByTestId(`nc-sidebar-base-${baseTitle}-options`)
.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"]');
}
private getProjectContextMenu({ baseTitle }: { baseTitle: string }) {
return this.dashboard
private async openProjectContextMenu({ baseTitle }: { baseTitle: string }) {
await this.dashboard.get().getByTestId(`nc-sidebar-base-title-${baseTitle}`).hover();
await this.dashboard
.get()
.getByTestId(`nc-sidebar-base-title-${baseTitle}`)
.locator('[data-testid="nc-sidebar-context-menu"]');
.locator('[data-testid="nc-sidebar-context-menu"]')
.click();
}
async isVisible() {
@ -61,7 +64,9 @@ export class TreeViewPage extends BasePage {
}
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 }) {
@ -78,6 +83,16 @@ export class TreeViewPage extends BasePage {
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
//
async openTable({
@ -164,10 +179,11 @@ export class TreeViewPage extends BasePage {
}
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.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("Delete")').click();
await this.waitForResponse({
@ -186,9 +202,11 @@ export class TreeViewPage extends BasePage {
}
async renameTable({ title, newTitle }: { title: string; newTitle: string }) {
await this.get().locator(`.nc-base-tree-tbl-${title}`).waitFor({ state: 'visible' });
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();
const tableTitle = title.replace(/ /g, '');
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.dashboard.get().locator('[placeholder="Enter table name"]').fill(newTitle);
@ -204,8 +222,7 @@ export class TreeViewPage extends BasePage {
}
async baseSettings({ title }: { title?: string }) {
await this.getProjectContextMenu({ baseTitle: title }).hover();
await this.getProjectContextMenu({ baseTitle: title }).click();
await this.openProjectContextMenu({ baseTitle: title });
const settingsMenu = this.dashboard.get().locator('.ant-dropdown-menu.nc-scrollbar-md');
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 }) {
baseTitle = this.scopedProjectTitle({ title: baseTitle, context });
await this.getProjectContextMenu({ baseTitle }).hover();
await this.getProjectContextMenu({ baseTitle }).click();
await this.openProjectContextMenu({ baseTitle });
const importMenu = this.dashboard.get().locator('.ant-dropdown-menu');
await importMenu.locator(`.nc-sub-menu:has-text("Import Data")`).click();
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 }) {
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);
const emojiList = this.rootPage.locator('[id="emoji-mart-list"]');
await emojiList.locator('button').first().click();
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);
}
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();
// 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 }) {
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(
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);
}
@ -322,8 +346,7 @@ export class TreeViewPage extends BasePage {
param.title = this.scopedProjectTitle({ title: param.title, context: param.context });
param.newTitle = this.scopedProjectTitle({ title: param.newTitle, context: param.context });
await this.getProjectContextMenu({ baseTitle: param.title }).hover();
await this.getProjectContextMenu({ baseTitle: param.title }).click();
await this.openProjectContextMenu({ baseTitle: param.title });
const contextMenu = this.dashboard.get().locator('.ant-dropdown-menu.nc-scrollbar-md:visible').last();
await contextMenu.waitFor();
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 }) {
param.title = this.scopedProjectTitle({ title: param.title, context: param.context });
await this.getProjectContextMenu({ baseTitle: param.title }).hover();
await this.getProjectContextMenu({ baseTitle: param.title }).click();
await this.openProjectContextMenu({ baseTitle: param.title });
const contextMenu = this.dashboard.get().locator('.ant-dropdown-menu.nc-scrollbar-md:visible').last();
await contextMenu.waitFor();
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 }) {
param.title = this.scopedProjectTitle({ title: param.title, context: param.context });
await this.getProjectContextMenu({ baseTitle: param.title }).hover();
await this.getProjectContextMenu({ baseTitle: param.title }).click();
await this.openProjectContextMenu({ baseTitle: param.title });
const contextMenu = this.dashboard.get().locator('.ant-dropdown-menu.nc-scrollbar-md:visible');
await contextMenu.waitFor();
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 {
readonly base: any;
readonly tablesSideBar: Locator;
readonly baseMenuLink: Locator;
readonly workspaceMenuLink: Locator;
readonly tabBar: Locator;
readonly treeView: TreeViewPage;
@ -69,10 +68,6 @@ export class DashboardPage extends BasePage {
this.base = base;
this.tablesSideBar = rootPage.locator('.nc-treeview-container');
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.treeView = new TreeViewPage(this, base);
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}")`);
}
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() {
await this.baseMenuLink.click();
await this.clickOnBaseMenuLink();
const teamAndSettingsLink = this.getProjectMenuLink({ title: ' Team & Settings' });
await expect(teamAndSettingsLink).toBeVisible();
await this.baseMenuLink.click();
await this.clickOnBaseMenuLink();
}
async verifyTeamAndSettingsLinkIsNotVisible() {
await this.baseMenuLink.click();
await this.clickOnBaseMenuLink();
const teamAndSettingsLink = this.getProjectMenuLink({ title: ' Team & Settings' });
await expect(teamAndSettingsLink).not.toBeVisible();
await this.baseMenuLink.click();
await this.clickOnBaseMenuLink();
}
async gotoSettings() {
await this.baseMenuLink.click();
await this.clickOnBaseMenuLink();
await this.rootPage.locator('.ant-dropdown').locator(`.nc-menu-item:has-text("Settings")`).click();
}
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();
}
@ -179,10 +184,10 @@ export class DashboardPage extends BasePage {
// When a tab is opened, it is not always immediately visible.
async toggleMobileMode() {
await this.baseMenuLink.click();
await this.clickOnBaseMenuLink();
const projMenu = this.rootPage.locator('.nc-dropdown-base-menu');
await projMenu.locator('[data-menu-id="mobile-mode"]:visible').click();
await this.baseMenuLink.click();
await this.clickOnBaseMenuLink();
}
async signOut() {

Loading…
Cancel
Save