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 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,35 +767,63 @@ 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'">
<NcMenuItem v-if="isUIAllowed('tableRename')" @click="openRenameTableDialog(contextMenuTarget.value, true)"> <NcTooltip>
<div v-e="['c:table:rename']" class="nc-base-option-item flex gap-2 items-center"> <template #title> {{ $t('labels.clickToCopyTableID') }} </template>
<GeneralIcon icon="rename" class="text-gray-700" /> <div
{{ $t('general.rename') }} 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> </div>
</NcMenuItem> </NcTooltip>
<NcMenuItem <template v-if="isUIAllowed('tableRename') || isUIAllowed('tableDelete')">
v-if="isUIAllowed('tableDuplicate') && (contextMenuBase?.is_meta || contextMenuBase?.is_local)" <NcDivider />
@click="duplicateTable(contextMenuTarget.value)" <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:duplicate']" class="nc-base-option-item flex gap-2 items-center"> <GeneralIcon icon="rename" class="text-gray-700" />
<GeneralIcon icon="duplicate" class="text-gray-700" /> {{ $t('general.rename') }} {{ $t('objects.table') }}
{{ $t('general.duplicate') }} </div>
</div> </NcMenuItem>
</NcMenuItem>
<NcDivider /> <NcMenuItem
<NcMenuItem v-if="isUIAllowed('tableDelete')" class="!hover:bg-red-50" @click="tableDelete"> v-if="isUIAllowed('tableDuplicate') && (contextMenuBase?.is_meta || contextMenuBase?.is_local)"
<div class="nc-base-option-item flex gap-2 items-center text-red-600"> @click="duplicateTable(contextMenuTarget.value)"
<GeneralIcon icon="delete" /> >
{{ $t('general.delete') }} <div v-e="['c:table:duplicate']" class="nc-base-option-item flex gap-2 items-center">
</div> <GeneralIcon icon="duplicate" class="text-gray-700" />
</NcMenuItem> {{ $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> </template>
</NcMenu> </NcMenu>
</template> </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 { 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,44 +308,73 @@ watch(openedTableId, () => {
/> />
<template #overlay> <template #overlay>
<NcMenu> <NcMenu class="!min-w-70" :data-testid="`sidebar-table-context-menu-list-${table.title}`">
<NcMenuItem <NcTooltip>
v-if="isUIAllowed('tableRename', { roles: baseRole })" <template #title> {{ $t('labels.clickToCopyTableID') }} </template>
:data-testid="`sidebar-table-rename-${table.title}`" <div
@click="openRenameTableDialog(table, base.sources[sourceIndex].id)" class="flex items-center justify-between p-2 mx-1.5 rounded-md cursor-pointer hover:bg-gray-100 group"
> @click.stop="onTableIdCopy"
<div v-e="['c:table:rename']" class="flex gap-2 items-center"> >
<GeneralIcon icon="rename" class="text-gray-700" /> <div class="flex text-xs font-bold text-gray-500 ml-1">
{{ $t('general.rename') }} {{
$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> </div>
</NcMenuItem> </NcTooltip>
<NcMenuItem <template
v-if=" v-if="
isUIAllowed('tableDuplicate') && !isSharedBase &&
base.sources?.[sourceIndex] && (isUIAllowed('tableRename', { roles: baseRole }) || isUIAllowed('tableDelete', { roles: baseRole }))
(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"> <NcDivider />
<GeneralIcon icon="duplicate" class="text-gray-700" /> <NcMenuItem
{{ $t('general.duplicate') }} v-if="isUIAllowed('tableRename', { roles: baseRole })"
</div> :data-testid="`sidebar-table-rename-${table.title}`"
</NcMenuItem> @click="openRenameTableDialog(table, base.sources[sourceIndex].id)"
>
<NcMenuItem <div v-e="['c:table:rename']" class="flex gap-2 items-center">
v-if="isUIAllowed('tableDelete', { roles: baseRole })" <GeneralIcon icon="rename" class="text-gray-700" />
:data-testid="`sidebar-table-delete-${table.title}`" {{ $t('general.rename') }} {{ $t('objects.table') }}
class="!text-red-500 !hover:bg-red-50" </div>
@click="isTableDeleteDialogVisible = true" </NcMenuItem>
>
<div v-e="['c:table:delete']" class="flex gap-2 items-center"> <NcMenuItem
<GeneralIcon icon="delete" /> v-if="
{{ $t('general.delete') }} isUIAllowed('tableDuplicate') &&
</div> base.sources?.[sourceIndex] &&
</NcMenuItem> (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> </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