Browse Source

Merge pull request #7207 from nocodb/feat/paste-option-in-table-context-menu

feat: paste option in cell right click context menu
pull/7222/head
Raju Udava 12 months ago committed by GitHub
parent
commit
188dfda9ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      packages/nc-gui/components/smartsheet/details/Fields.vue
  2. 17
      packages/nc-gui/components/smartsheet/grid/Table.vue
  3. 2
      packages/nc-gui/components/tabs/Smartsheet.vue
  4. 39
      packages/nc-gui/composables/usePaste.ts
  5. 6
      packages/nc-gui/lang/en.json
  6. 1
      packages/nc-gui/utils/iconUtils.ts
  7. 10
      tests/playwright/pages/Dashboard/Grid/index.ts
  8. 25
      tests/playwright/tests/db/general/cellSelection.spec.ts

2
packages/nc-gui/components/smartsheet/details/Fields.vue

@ -1081,7 +1081,7 @@ const onFieldOptionUpdate = () => {
</Draggable> </Draggable>
</div> </div>
<Transition v-if="!changingField" name="slide-fade"> <Transition v-if="!changingField" name="slide-fade">
<div class="border-gray-200 border-l-1 nc-scrollbar-md nc-fields-height !overflow-y-auto"> <div v-if="!changingField" class="border-gray-200 border-l-1 nc-scrollbar-md nc-fields-height !overflow-y-auto">
<SmartsheetColumnEditOrAddProvider <SmartsheetColumnEditOrAddProvider
v-if="activeField" v-if="activeField"
class="p-4 w-[25rem]" class="p-4 w-[25rem]"

17
packages/nc-gui/components/smartsheet/grid/Table.vue

@ -34,6 +34,7 @@ import {
useI18n, useI18n,
useMultiSelect, useMultiSelect,
useNuxtApp, useNuxtApp,
usePaste,
useRoles, useRoles,
useRoute, useRoute,
useSmartsheetStoreOrThrow, useSmartsheetStoreOrThrow,
@ -168,6 +169,8 @@ const predictNextFormulas = async () => {
await _predictNextFormulas(meta) await _predictNextFormulas(meta)
} }
const { paste } = usePaste(true)
// #Refs // #Refs
const rowRefs = ref<any[]>() const rowRefs = ref<any[]>()
@ -1694,6 +1697,20 @@ onKeyStroke('ArrowDown', onDown)
</div> </div>
</NcMenuItem> </NcMenuItem>
<NcMenuItem
v-if="contextMenuTarget"
class="nc-base-menu-item"
data-testid="context-menu-item-paste"
:disabled="isSystemColumn(fields[contextMenuTarget.col])"
@click="paste"
>
<div v-e="['a:row:paste']" class="flex gap-2 items-center">
<GeneralIcon icon="paste" />
<!-- Paste -->
{{ $t('general.paste') }}
</div>
</NcMenuItem>
<!-- Clear cell --> <!-- Clear cell -->
<NcMenuItem <NcMenuItem
v-if=" v-if="

2
packages/nc-gui/components/tabs/Smartsheet.vue

@ -171,7 +171,7 @@ watch([activeViewTitleOrId, activeTableId], () => {
<LazySmartsheetToolbar v-if="!isForm" /> <LazySmartsheetToolbar v-if="!isForm" />
<div class="flex flex-row w-full" style="height: calc(100% - var(--topbar-height))"> <div class="flex flex-row w-full" style="height: calc(100% - var(--topbar-height))">
<Transition name="layout" mode="out-in"> <Transition name="layout" mode="out-in">
<div class="flex flex-1 min-h-0 w-3/4"> <div v-if="openedViewsTab === 'view'" class="flex flex-1 min-h-0 w-3/4">
<div class="h-full flex-1 min-w-0 min-h-0 bg-white"> <div class="h-full flex-1 min-w-0 min-h-0 bg-white">
<LazySmartsheetGrid v-if="isGrid || !meta || !activeView" ref="grid" /> <LazySmartsheetGrid v-if="isGrid || !meta || !activeView" ref="grid" />

39
packages/nc-gui/composables/usePaste.ts

@ -0,0 +1,39 @@
import { useI18n, message } from '#imports'
export const usePaste = (showDialogIfFailed = false) => {
const { t } = useI18n()
const paste = async (): Promise<boolean> => {
try {
// Check if the Clipboard API is supported
if (!navigator.clipboard) return false
// Read text from the clipboard
const clipboardText = await navigator.clipboard.readText()
// Create a new paste event
const pasteEvent = new Event('paste', {
bubbles: false,
cancelable: true,
})
// Attach clipboard data to the event
const clipboardData = {
getData: () => clipboardText || '',
}
Object.defineProperty(pasteEvent, 'clipboardData', { value: clipboardData })
// Dispatch the event on the document
document.dispatchEvent(pasteEvent)
return true
} catch (e) {
if (!showDialogIfFailed) throw new Error(t('msg.error.pasteFromClipboardError'))
message.error(t('msg.error.pasteFromClipboardError'))
return false
}
}
return { paste }
}

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

@ -188,7 +188,8 @@
"useSurveyMode": "Use Survey Mode", "useSurveyMode": "Use Survey Mode",
"shift": "Shift", "shift": "Shift",
"enter": "Enter", "enter": "Enter",
"seconds": "Seconds" "seconds": "Seconds",
"paste": "Paste"
}, },
"objects": { "objects": {
"workspace": "Workspace", "workspace": "Workspace",
@ -1283,7 +1284,8 @@
"duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed", "duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed",
"fieldRequired": "{value} cannot be empty.", "fieldRequired": "{value} cannot be empty.",
"projectNotAccessible": "Base not accessible", "projectNotAccessible": "Base not accessible",
"copyToClipboardError": "Failed to copy to clipboard" "copyToClipboardError": "Failed to copy to clipboard",
"pasteFromClipboardError": "Failed to paste from clipboard"
}, },
"toast": { "toast": {
"exportMetadata": "Base metadata exported successfully", "exportMetadata": "Base metadata exported successfully",

1
packages/nc-gui/utils/iconUtils.ts

@ -452,6 +452,7 @@ export const iconMap = {
role_no_access: NoAccess, role_no_access: NoAccess,
commentHere: NcCommentHere, commentHere: NcCommentHere,
fileImage: FileImageIcon, fileImage: FileImageIcon,
paste: h('span', { class: 'material-symbols' }, 'content_paste'),
} }
export const getMdiIcon = (type: string): any => { export const getMdiIcon = (type: string): any => {

10
tests/playwright/pages/Dashboard/Grid/index.ts

@ -450,4 +450,14 @@ export class GridPage extends BasePage {
} }
return text; return text;
} }
async pasteWithMouse({ index, columnHeader }: CellProps) {
await this.cell.get({ index, columnHeader }).scrollIntoViewIfNeeded();
await this.cell.get({ index, columnHeader }).click({ button: 'right' });
await this.get().page().getByTestId('context-menu-item-paste').click();
// kludge: wait for paste to complete
await this.rootPage.waitForTimeout(1000);
}
} }

25
tests/playwright/tests/db/general/cellSelection.spec.ts

@ -28,16 +28,34 @@ test.describe('Verify cell selection', () => {
expect(await grid.selectedCount()).toBe(9); expect(await grid.selectedCount()).toBe(9);
await dashboard.closeAllTabs(); await dashboard.closeAllTabs();
// #2 when copied with clipboard, it copies correct text // #2 when copied with clipboard, it copies correct text and paste
const verifyPastedData = async ({ index }: { index: number }): Promise<void> => {
// FirstName column
let cellText: string[] = ['MARY', 'PATRICIA'];
for (let i = index; i <= index + 1; i++) {
await grid.cell.verify({ index: i, columnHeader: 'FirstName', value: cellText[i - index] });
}
// LastName column
cellText = ['SMITH', 'JOHNSON'];
for (let i = index; i <= index + 1; i++) {
await grid.cell.verify({ index: i, columnHeader: 'LastName', value: cellText[i - index] });
}
};
await dashboard.treeView.openTable({ title: 'Customer' }); await dashboard.treeView.openTable({ title: 'Customer' });
await grid.selectRange({ await grid.selectRange({
start: { index: 0, columnHeader: 'FirstName' }, start: { index: 0, columnHeader: 'FirstName' },
end: { index: 1, columnHeader: 'LastName' }, end: { index: 1, columnHeader: 'LastName' },
}); });
expect(await grid.copyWithKeyboard()).toBe('MARY\tSMITH\n' + 'PATRICIA\tJOHNSON'); expect(await grid.copyWithKeyboard()).toBe('MARY\tSMITH\n' + 'PATRICIA\tJOHNSON');
await grid.pasteWithMouse({ index: 2, columnHeader: 'FirstName' });
await verifyPastedData({ index: 2 });
await dashboard.closeAllTabs(); await dashboard.closeAllTabs();
// #3 when copied with mouse, it copies correct text // #3 when copied with mouse, it copies correct text and paste
await dashboard.treeView.openTable({ title: 'Customer' }); await dashboard.treeView.openTable({ title: 'Customer' });
await grid.selectRange({ await grid.selectRange({
start: { index: 0, columnHeader: 'FirstName' }, start: { index: 0, columnHeader: 'FirstName' },
@ -46,6 +64,9 @@ test.describe('Verify cell selection', () => {
expect(await grid.copyWithMouse({ index: 0, columnHeader: 'FirstName' })).toBe( expect(await grid.copyWithMouse({ index: 0, columnHeader: 'FirstName' })).toBe(
'MARY\tSMITH\n' + 'PATRICIA\tJOHNSON' 'MARY\tSMITH\n' + 'PATRICIA\tJOHNSON'
); );
await grid.pasteWithMouse({ index: 4, columnHeader: 'FirstName' });
await verifyPastedData({ index: 4 });
await dashboard.closeAllTabs(); await dashboard.closeAllTabs();
}); });

Loading…
Cancel
Save