Browse Source

Nc feat(nc-gui): table action menu dropdown should be same as view action menu dropdown (#8043)

* feat(nc-gui): table action menu dropdown should be same as view action menu dropdown

* fix(nc-gui): PR AI review changes

* fix(nc-gui): PR review changes

* fix(nc-gui): view action menu copy id hover css

* fix(test): update table context menu test cases

* fix(nc-gui): update table context menu

* fix(test): role access pw test fail issue
pull/8029/head
Ramesh Mane 9 months ago committed by GitHub
parent
commit
aefebe7f3c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 61
      packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
  2. 70
      packages/nc-gui/components/dashboard/TreeView/TableNode.vue
  3. 2
      packages/nc-gui/components/smartsheet/toolbar/ViewActionMenu.vue
  4. 2
      packages/nc-gui/lang/en.json
  5. 32
      tests/playwright/pages/Dashboard/Sidebar/TableNode/index.ts
  6. 8
      tests/playwright/pages/Dashboard/TreeView.ts

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

@ -390,6 +390,29 @@ const projectDelete = () => {
isProjectDeleteDialogVisible.value = true isProjectDeleteDialogVisible.value = true
$e('c:project:delete') $e('c:project:delete')
} }
// Tracks if the table ID has been successfully copied to the clipboard
const isTableIdCopied = ref(false)
let tableIdCopiedTimeout: NodeJS.Timeout
const onTableIdCopy = async () => {
if (tableIdCopiedTimeout) {
clearTimeout(tableIdCopiedTimeout)
}
try {
await copy(contextMenuTarget.value.id)
isTableIdCopied.value = true
tableIdCopiedTimeout = setTimeout(() => {
isTableIdCopied.value = false
clearTimeout(tableIdCopiedTimeout)
}, 5000)
} catch (e: any) {
message.error(e.message)
}
}
</script> </script>
<template> <template>
@ -744,16 +767,43 @@ const projectDelete = () => {
</div> </div>
</div> </div>
<template v-if="!isSharedBase" #overlay> <template v-if="!isSharedBase" #overlay>
<NcMenu class="!py-0 rounded text-sm"> <NcMenu
class="!py-0 rounded text-sm"
:class="{
'!min-w-70': contextMenuTarget.type === 'table',
}"
>
<template v-if="contextMenuTarget.type === 'base' && base.type === 'database'"></template> <template v-if="contextMenuTarget.type === 'base' && base.type === 'database'"></template>
<template v-else-if="contextMenuTarget.type === 'source'"></template> <template v-else-if="contextMenuTarget.type === 'source'"></template>
<template v-else-if="contextMenuTarget.type === 'table'"> <template v-else-if="contextMenuTarget.type === 'table'">
<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: contextMenuTarget.value?.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 v-if="isUIAllowed('tableRename') || isUIAllowed('tableDelete')">
<NcDivider />
<NcMenuItem v-if="isUIAllowed('tableRename')" @click="openRenameTableDialog(contextMenuTarget.value, true)"> <NcMenuItem v-if="isUIAllowed('tableRename')" @click="openRenameTableDialog(contextMenuTarget.value, true)">
<div v-e="['c:table:rename']" class="nc-base-option-item flex gap-2 items-center"> <div v-e="['c:table:rename']" class="nc-base-option-item flex gap-2 items-center">
<GeneralIcon icon="rename" class="text-gray-700" /> <GeneralIcon icon="rename" class="text-gray-700" />
{{ $t('general.rename') }} {{ $t('general.rename') }} {{ $t('objects.table') }}
</div> </div>
</NcMenuItem> </NcMenuItem>
@ -763,17 +813,18 @@ const projectDelete = () => {
> >
<div v-e="['c:table:duplicate']" class="nc-base-option-item flex gap-2 items-center"> <div v-e="['c:table:duplicate']" class="nc-base-option-item flex gap-2 items-center">
<GeneralIcon icon="duplicate" class="text-gray-700" /> <GeneralIcon icon="duplicate" class="text-gray-700" />
{{ $t('general.duplicate') }} {{ $t('general.duplicate') }} {{ $t('objects.table') }}
</div> </div>
</NcMenuItem> </NcMenuItem>
<NcDivider /> <NcDivider />
<NcMenuItem v-if="isUIAllowed('tableDelete')" class="!hover:bg-red-50" @click="tableDelete"> <NcMenuItem v-if="isUIAllowed('table-delete')" class="!hover:bg-red-50" @click="tableDelete">
<div class="nc-base-option-item flex gap-2 items-center text-red-600"> <div class="nc-base-option-item flex gap-2 items-center text-red-600">
<GeneralIcon icon="delete" /> <GeneralIcon icon="delete" />
{{ $t('general.delete') }} {{ $t('general.delete') }} {{ $t('objects.table') }}
</div> </div>
</NcMenuItem> </NcMenuItem>
</template> </template>
</template>
</NcMenu> </NcMenu>
</template> </template>
</NcDropdown> </NcDropdown>

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

@ -4,7 +4,7 @@ import { toRef } from '@vue/reactivity'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { ProjectRoleInj, TreeViewInj, useMagicKeys, useNuxtApp, useRoles, useTabs } from '#imports' import { ProjectRoleInj, TreeViewInj, useCopy, useMagicKeys, useNuxtApp, useRoles, useTabs } from '#imports'
import type { SidebarTableNode } from '~/lib' import type { SidebarTableNode } from '~/lib'
const props = withDefaults( const props = withDefaults(
@ -41,6 +41,8 @@ useTableNew({
const { meta: metaKey, control } = useMagicKeys() const { meta: metaKey, control } = useMagicKeys()
const { copy } = useCopy()
const baseRole = inject(ProjectRoleInj) const baseRole = inject(ProjectRoleInj)
provide(SidebarTableInj, table) provide(SidebarTableInj, table)
@ -90,6 +92,9 @@ const canUserEditEmote = computed(() => {
const isExpanded = ref(false) const isExpanded = ref(false)
const isLoading = ref(false) const isLoading = ref(false)
// Tracks if the table ID has been successfully copied to the clipboard
const isTableIdCopied = ref(false)
const onExpand = async () => { const onExpand = async () => {
if (isExpanded.value) { if (isExpanded.value) {
isExpanded.value = false isExpanded.value = false
@ -127,6 +132,25 @@ const onOpenTable = async () => {
isExpanded.value = true isExpanded.value = true
} }
} }
let tableIdCopiedTimeout: NodeJS.Timeout
const onTableIdCopy = async () => {
if (tableIdCopiedTimeout) {
clearTimeout(tableIdCopiedTimeout)
}
try {
await copy(table.value!.id!)
isTableIdCopied.value = true
tableIdCopiedTimeout = setTimeout(() => {
isTableIdCopied.value = false
clearTimeout(tableIdCopiedTimeout)
}, 5000)
} catch (e: any) {
message.error(e.message)
}
}
watch( watch(
() => activeView.value?.id, () => activeView.value?.id,
@ -276,12 +300,7 @@ watch(openedTableId, () => {
</NcTooltip> </NcTooltip>
<div class="flex flex-grow h-full"></div> <div class="flex flex-grow h-full"></div>
<div class="flex flex-row items-center"> <div class="flex flex-row items-center">
<div <div v-e="['c:table:option']">
v-if="
!isSharedBase && (isUIAllowed('tableRename', { roles: baseRole }) || isUIAllowed('tableDelete', { roles: baseRole }))
"
v-e="['c:table:option']"
>
<NcDropdown :trigger="['click']" class="nc-sidebar-node-btn" @click.stop> <NcDropdown :trigger="['click']" class="nc-sidebar-node-btn" @click.stop>
<MdiDotsHorizontal <MdiDotsHorizontal
data-testid="nc-sidebar-table-context-menu" data-testid="nc-sidebar-table-context-menu"
@ -289,7 +308,34 @@ watch(openedTableId, () => {
/> />
<template #overlay> <template #overlay>
<NcMenu> <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
v-if="
!isSharedBase &&
(isUIAllowed('tableRename', { roles: baseRole }) || isUIAllowed('tableDelete', { roles: baseRole }))
"
>
<NcDivider />
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('tableRename', { roles: baseRole })" v-if="isUIAllowed('tableRename', { roles: baseRole })"
:data-testid="`sidebar-table-rename-${table.title}`" :data-testid="`sidebar-table-rename-${table.title}`"
@ -297,7 +343,7 @@ watch(openedTableId, () => {
> >
<div v-e="['c:table:rename']" class="flex gap-2 items-center"> <div v-e="['c:table:rename']" class="flex gap-2 items-center">
<GeneralIcon icon="rename" class="text-gray-700" /> <GeneralIcon icon="rename" class="text-gray-700" />
{{ $t('general.rename') }} {{ $t('general.rename') }} {{ $t('objects.table') }}
</div> </div>
</NcMenuItem> </NcMenuItem>
@ -312,10 +358,11 @@ watch(openedTableId, () => {
> >
<div v-e="['c:table:duplicate']" class="flex gap-2 items-center"> <div v-e="['c:table:duplicate']" class="flex gap-2 items-center">
<GeneralIcon icon="duplicate" class="text-gray-700" /> <GeneralIcon icon="duplicate" class="text-gray-700" />
{{ $t('general.duplicate') }} {{ $t('general.duplicate') }} {{ $t('objects.table') }}
</div> </div>
</NcMenuItem> </NcMenuItem>
<NcDivider />
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('tableDelete', { roles: baseRole })" v-if="isUIAllowed('tableDelete', { roles: baseRole })"
:data-testid="`sidebar-table-delete-${table.title}`" :data-testid="`sidebar-table-delete-${table.title}`"
@ -324,9 +371,10 @@ watch(openedTableId, () => {
> >
<div v-e="['c:table:delete']" class="flex gap-2 items-center"> <div v-e="['c:table:delete']" class="flex gap-2 items-center">
<GeneralIcon icon="delete" /> <GeneralIcon icon="delete" />
{{ $t('general.delete') }} {{ $t('general.delete') }} {{ $t('objects.table') }}
</div> </div>
</NcMenuItem> </NcMenuItem>
</template>
</NcMenu> </NcMenu>
</template> </template>
</NcDropdown> </NcDropdown>

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

@ -153,7 +153,7 @@ const onDelete = async () => {
> >
<NcTooltip> <NcTooltip>
<template #title> {{ $t('labels.clickToCopyViewID') }} </template> <template #title> {{ $t('labels.clickToCopyViewID') }} </template>
<div class="flex items-center justify-between py-2 px-3 cursor-pointer hover:bg-gray-100 group" @click="onViewIdCopy"> <div class="flex items-center justify-between p-2 mx-1.5 rounded-md cursor-pointer hover:bg-gray-100 group" @click="onViewIdCopy">
<div class="flex text-xs font-bold text-gray-500 ml-1"> <div class="flex text-xs font-bold text-gray-500 ml-1">
{{ {{
$t('labels.viewIdColon', { $t('labels.viewIdColon', {

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

@ -468,6 +468,7 @@
"noToken": "No Token", "noToken": "No Token",
"tokenLimit": "Only one token per user is allowed", "tokenLimit": "Only one token per user is allowed",
"duplicateAttachment": "File with name {filename} already attached", "duplicateAttachment": "File with name {filename} already attached",
"tableIdColon": "TABLE ID: {tableId}",
"viewIdColon": "VIEW ID: {viewId}", "viewIdColon": "VIEW ID: {viewId}",
"toAddress": "To Address", "toAddress": "To Address",
"subject": "Subject", "subject": "Subject",
@ -506,6 +507,7 @@
"clickToHide": "Click to hide", "clickToHide": "Click to hide",
"clickToDownload": "Click to download", "clickToDownload": "Click to download",
"forRole": "for role", "forRole": "for role",
"clickToCopyTableID": "Click to copy Table ID",
"clickToCopyViewID": "Click to copy View ID", "clickToCopyViewID": "Click to copy View ID",
"viewMode": "View Mode", "viewMode": "View Mode",
"searchUsers": "Search Users", "searchUsers": "Search Users",

32
tests/playwright/pages/Dashboard/Sidebar/TableNode/index.ts

@ -36,22 +36,39 @@ export class SidebarTableNodeObject extends BasePage {
async verifyTableOptions({ async verifyTableOptions({
tableTitle, tableTitle,
isVisible, isVisible,
checkMenuOptions = true,
renameVisible, renameVisible,
duplicateVisible, duplicateVisible,
deleteVisible, deleteVisible,
}: { }: {
tableTitle: string; tableTitle: string;
isVisible: boolean; isVisible: boolean;
checkMenuOptions?: boolean;
renameVisible?: boolean; renameVisible?: boolean;
duplicateVisible?: boolean; duplicateVisible?: boolean;
deleteVisible?: boolean; deleteVisible?: boolean;
}) { }) {
const optionsLocator = await this.get({ if (isVisible) {
tableTitle, await this.clickOptions({ tableTitle });
}).getByTestId('nc-sidebar-table-context-menu'); await this.rootPage.getByTestId(`sidebar-table-context-menu-list-${tableTitle}`).waitFor({ state: 'visible' });
if (isVisible) await optionsLocator.isVisible();
else { await expect(
await expect(optionsLocator).toHaveCount(0); this.rootPage.getByTestId(`sidebar-table-context-menu-list-${tableTitle}`).locator('li.ant-dropdown-menu-item')
).not.toHaveCount(0);
if (!checkMenuOptions) {
// close table options context menu
await this.clickOptions({ tableTitle });
return;
}
} else {
await this.clickOptions({ tableTitle });
await this.rootPage.getByTestId(`sidebar-table-context-menu-list-${tableTitle}`).waitFor({ state: 'visible' });
await expect(
this.rootPage.getByTestId(`sidebar-table-context-menu-list-${tableTitle}`).locator('li.ant-dropdown-menu-item')
).toHaveCount(0);
await this.clickOptions({ tableTitle });
return; return;
} }
@ -69,5 +86,8 @@ export class SidebarTableNodeObject extends BasePage {
if (deleteVisible) await expect(deleteLocator).toBeVisible(); if (deleteVisible) await expect(deleteLocator).toBeVisible();
else await expect(deleteLocator).toHaveCount(0); else await expect(deleteLocator).toHaveCount(0);
// close table options context menu
await this.clickOptions({ tableTitle });
} }
} }

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

@ -287,9 +287,11 @@ export class TreeViewPage extends BasePage {
await expect(pjtNode.locator('[data-testid="nc-sidebar-context-menu"]')).toHaveCount(1); await expect(pjtNode.locator('[data-testid="nc-sidebar-context-menu"]')).toHaveCount(1);
// table context menu // table context menu
const tblNode = await this.getTable({ index: 0, tableTitle: param.tableTitle }); await this.dashboard.sidebar.tableNode.verifyTableOptions({
await tblNode.hover(); tableTitle: param.tableTitle,
await expect(tblNode.locator('.nc-tbl-context-menu')).toHaveCount(count); isVisible: !!count,
checkMenuOptions: false,
});
} }
} }

Loading…
Cancel
Save