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. 95
      packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
  2. 128
      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

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

@ -390,6 +390,29 @@ const projectDelete = () => {
isProjectDeleteDialogVisible.value = true
$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>
<template>
@ -744,35 +767,63 @@ const projectDelete = () => {
</div>
</div>
<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-else-if="contextMenuTarget.type === 'source'"></template>
<template v-else-if="contextMenuTarget.type === 'table'">
<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">
<GeneralIcon icon="rename" class="text-gray-700" />
{{ $t('general.rename') }}
<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>
</NcMenuItem>
</NcTooltip>
<NcMenuItem
v-if="isUIAllowed('tableDuplicate') && (contextMenuBase?.is_meta || contextMenuBase?.is_local)"
@click="duplicateTable(contextMenuTarget.value)"
>
<div v-e="['c:table:duplicate']" class="nc-base-option-item flex gap-2 items-center">
<GeneralIcon icon="duplicate" class="text-gray-700" />
{{ $t('general.duplicate') }}
</div>
</NcMenuItem>
<NcDivider />
<NcMenuItem v-if="isUIAllowed('tableDelete')" class="!hover:bg-red-50" @click="tableDelete">
<div class="nc-base-option-item flex gap-2 items-center text-red-600">
<GeneralIcon icon="delete" />
{{ $t('general.delete') }}
</div>
</NcMenuItem>
<template v-if="isUIAllowed('tableRename') || isUIAllowed('tableDelete')">
<NcDivider />
<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">
<GeneralIcon icon="rename" class="text-gray-700" />
{{ $t('general.rename') }} {{ $t('objects.table') }}
</div>
</NcMenuItem>
<NcMenuItem
v-if="isUIAllowed('tableDuplicate') && (contextMenuBase?.is_meta || contextMenuBase?.is_local)"
@click="duplicateTable(contextMenuTarget.value)"
>
<div v-e="['c:table:duplicate']" class="nc-base-option-item 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('table-delete')" class="!hover:bg-red-50" @click="tableDelete">
<div class="nc-base-option-item flex gap-2 items-center text-red-600">
<GeneralIcon icon="delete" />
{{ $t('general.delete') }} {{ $t('objects.table') }}
</div>
</NcMenuItem>
</template>
</template>
</NcMenu>
</template>

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

@ -4,7 +4,7 @@ import { toRef } from '@vue/reactivity'
import { message } from 'ant-design-vue'
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'
const props = withDefaults(
@ -41,6 +41,8 @@ useTableNew({
const { meta: metaKey, control } = useMagicKeys()
const { copy } = useCopy()
const baseRole = inject(ProjectRoleInj)
provide(SidebarTableInj, table)
@ -90,6 +92,9 @@ const canUserEditEmote = computed(() => {
const isExpanded = 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 () => {
if (isExpanded.value) {
isExpanded.value = false
@ -127,6 +132,25 @@ const onOpenTable = async () => {
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(
() => activeView.value?.id,
@ -276,12 +300,7 @@ watch(openedTableId, () => {
</NcTooltip>
<div class="flex flex-grow h-full"></div>
<div class="flex flex-row items-center">
<div
v-if="
!isSharedBase && (isUIAllowed('tableRename', { roles: baseRole }) || isUIAllowed('tableDelete', { roles: baseRole }))
"
v-e="['c:table:option']"
>
<div v-e="['c:table:option']">
<NcDropdown :trigger="['click']" class="nc-sidebar-node-btn" @click.stop>
<MdiDotsHorizontal
data-testid="nc-sidebar-table-context-menu"
@ -289,44 +308,73 @@ watch(openedTableId, () => {
/>
<template #overlay>
<NcMenu>
<NcMenuItem
v-if="isUIAllowed('tableRename', { roles: baseRole })"
: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') }}
<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>
</NcMenuItem>
</NcTooltip>
<NcMenuItem
<template
v-if="
isUIAllowed('tableDuplicate') &&
base.sources?.[sourceIndex] &&
(base.sources[sourceIndex].is_meta || base.sources[sourceIndex].is_local)
!isSharedBase &&
(isUIAllowed('tableRename', { roles: baseRole }) || isUIAllowed('tableDelete', { roles: baseRole }))
"
: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') }}
</div>
</NcMenuItem>
<NcMenuItem
v-if="isUIAllowed('tableDelete', { roles: baseRole })"
:data-testid="`sidebar-table-delete-${table.title}`"
class="!text-red-500 !hover:bg-red-50"
@click="isTableDeleteDialogVisible = true"
>
<div v-e="['c:table:delete']" class="flex gap-2 items-center">
<GeneralIcon icon="delete" />
{{ $t('general.delete') }}
</div>
</NcMenuItem>
<NcDivider />
<NcMenuItem
v-if="isUIAllowed('tableRename', { roles: baseRole })"
: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="isTableDeleteDialogVisible = true"
>
<div v-e="['c:table:delete']" class="flex gap-2 items-center">
<GeneralIcon icon="delete" />
{{ $t('general.delete') }} {{ $t('objects.table') }}
</div>
</NcMenuItem>
</template>
</NcMenu>
</template>
</NcDropdown>

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

@ -153,7 +153,7 @@ const onDelete = async () => {
>
<NcTooltip>
<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">
{{
$t('labels.viewIdColon', {

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

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

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

@ -36,22 +36,39 @@ export class SidebarTableNodeObject extends BasePage {
async verifyTableOptions({
tableTitle,
isVisible,
checkMenuOptions = true,
renameVisible,
duplicateVisible,
deleteVisible,
}: {
tableTitle: string;
isVisible: boolean;
checkMenuOptions?: boolean;
renameVisible?: boolean;
duplicateVisible?: boolean;
deleteVisible?: boolean;
}) {
const optionsLocator = await this.get({
tableTitle,
}).getByTestId('nc-sidebar-table-context-menu');
if (isVisible) await optionsLocator.isVisible();
else {
await expect(optionsLocator).toHaveCount(0);
if (isVisible) {
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')
).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;
}
@ -69,5 +86,8 @@ export class SidebarTableNodeObject extends BasePage {
if (deleteVisible) await expect(deleteLocator).toBeVisible();
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);
// table context menu
const tblNode = await this.getTable({ index: 0, tableTitle: param.tableTitle });
await tblNode.hover();
await expect(tblNode.locator('.nc-tbl-context-menu')).toHaveCount(count);
await this.dashboard.sidebar.tableNode.verifyTableOptions({
tableTitle: param.tableTitle,
isVisible: !!count,
checkMenuOptions: false,
});
}
}

Loading…
Cancel
Save