Browse Source

feat(gui): copying belongs to column

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/4514/head
Pranav C 2 years ago
parent
commit
04ff0b0163
  1. 464
      packages/nc-gui/components/smartsheet/Grid.vue
  2. 40
      packages/nc-gui/composables/useMultiSelect/index.ts

464
packages/nc-gui/components/smartsheet/Grid.vue

@ -151,8 +151,8 @@ const getContainerScrollForElement = (
relativePos.right + (offset?.right || 0) > 0 relativePos.right + (offset?.right || 0) > 0
? container.scrollLeft + relativePos.right + (offset?.right || 0) ? container.scrollLeft + relativePos.right + (offset?.right || 0)
: relativePos.left - (offset?.left || 0) < 0 : relativePos.left - (offset?.left || 0) < 0
? container.scrollLeft + relativePos.left - (offset?.left || 0) ? container.scrollLeft + relativePos.left - (offset?.left || 0)
: container.scrollLeft : container.scrollLeft
/* /*
* If the element is below the container, scroll down (positive) * If the element is below the container, scroll down (positive)
@ -162,14 +162,15 @@ const getContainerScrollForElement = (
relativePos.bottom + (offset?.bottom || 0) > 0 relativePos.bottom + (offset?.bottom || 0) > 0
? container.scrollTop + relativePos.bottom + (offset?.bottom || 0) ? container.scrollTop + relativePos.bottom + (offset?.bottom || 0)
: relativePos.top - (offset?.top || 0) < 0 : relativePos.top - (offset?.top || 0) < 0
? container.scrollTop + relativePos.top - (offset?.top || 0) ? container.scrollTop + relativePos.top - (offset?.top || 0)
: container.scrollTop : container.scrollTop
return scroll return scroll
} }
const { selectCell, startSelectRange, endSelectRange, clearSelectedRange, copyValue, isCellSelected, selectedCell } = const { selectCell, startSelectRange, endSelectRange, clearSelectedRange, copyValue, isCellSelected, selectedCell } =
useMultiSelect( useMultiSelect(
meta,
fields, fields,
data, data,
$$(editEnabled), $$(editEnabled),
@ -188,98 +189,98 @@ const { selectCell, startSelectRange, endSelectRange, clearSelectedRange, copyVa
return true return true
} }
// skip keyboard event handling if there is a drawer / modal // skip keyboard event handling if there is a drawer / modal
if (isDrawerOrModalExist()) { if (isDrawerOrModalExist()) {
return true
}
const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey
const altOrOptionKey = e.altKey
if (e.key === ' ') {
if (selectedCell.row !== null && !editEnabled) {
e.preventDefault()
const row = data.value[selectedCell.row]
expandForm(row)
return true
}
} else if (e.key === 'Escape') {
if (editEnabled) {
editEnabled = false
return true
}
} else if (e.key === 'Enter') {
if (e.shiftKey) {
// add a line break for types like LongText / JSON
return true
}
if (editEnabled) {
editEnabled = false
return true return true
} }
}
if (cmdOrCtrl) { const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey
switch (e.key) { const altOrOptionKey = e.altKey
case 'ArrowUp': if (e.key === ' ') {
if (selectedCell.row !== null && !editEnabled) {
e.preventDefault() e.preventDefault()
selectedCell.row = 0 const row = data.value[selectedCell.row]
selectedCell.col = selectedCell.col ?? 0 expandForm(row)
scrollToCell?.()
editEnabled = false
return true return true
case 'ArrowDown': }
e.preventDefault() } else if (e.key === 'Escape') {
selectedCell.row = data.value.length - 1 if (editEnabled) {
selectedCell.col = selectedCell.col ?? 0
scrollToCell?.()
editEnabled = false editEnabled = false
return true return true
case 'ArrowRight': }
e.preventDefault() } else if (e.key === 'Enter') {
selectedCell.row = selectedCell.row ?? 0 if (e.shiftKey) {
selectedCell.col = fields.value?.length - 1 // add a line break for types like LongText / JSON
scrollToCell?.()
editEnabled = false
return true return true
case 'ArrowLeft': }
e.preventDefault() if (editEnabled) {
selectedCell.row = selectedCell.row ?? 0
selectedCell.col = 0
scrollToCell?.()
editEnabled = false editEnabled = false
return true return true
}
} }
}
if (altOrOptionKey) { if (cmdOrCtrl) {
switch (e.keyCode) { switch (e.key) {
case 82: { case 'ArrowUp':
// ALT + R e.preventDefault()
if (isAddingEmptyRowAllowed) { selectedCell.row = 0
addEmptyRow() selectedCell.col = selectedCell.col ?? 0
} scrollToCell?.()
break editEnabled = false
return true
case 'ArrowDown':
e.preventDefault()
selectedCell.row = data.value.length - 1
selectedCell.col = selectedCell.col ?? 0
scrollToCell?.()
editEnabled = false
return true
case 'ArrowRight':
e.preventDefault()
selectedCell.row = selectedCell.row ?? 0
selectedCell.col = fields.value?.length - 1
scrollToCell?.()
editEnabled = false
return true
case 'ArrowLeft':
e.preventDefault()
selectedCell.row = selectedCell.row ?? 0
selectedCell.col = 0
scrollToCell?.()
editEnabled = false
return true
} }
case 67: { }
// ALT + C
if (isAddingColumnAllowed) { if (altOrOptionKey) {
addColumnDropdown.value = true switch (e.keyCode) {
case 82: {
// ALT + R
if (isAddingEmptyRowAllowed) {
addEmptyRow()
}
break
}
case 67: {
// ALT + C
if (isAddingColumnAllowed) {
addColumnDropdown.value = true
}
break
} }
break
} }
} }
} },
}, async (ctx: { row: number; col?: number, updatedColumnTitle?: string }) => {
async (ctx: { row: number; col: number }) => {
const rowObj = data.value[ctx.row] const rowObj = data.value[ctx.row]
const columnObj = fields.value[ctx.col] const columnObj = (ctx.col !== null && ctx.col !== undefined) ? fields.value[ctx.col] : null
if (isVirtualCol(columnObj)) { if (!ctx.updatedColumnTitle && isVirtualCol(columnObj)) {
return return
} }
// update/save cell value // update/save cell value
await updateOrSaveRow(rowObj, columnObj.title) await updateOrSaveRow(rowObj, ctx.updatedColumnTitle || columnObj.title)
}, },
) )
@ -643,104 +644,105 @@ const closeAddColumnDropdown = () => {
@contextmenu="showContextMenu" @contextmenu="showContextMenu"
> >
<thead ref="tableHead"> <thead ref="tableHead">
<tr class="nc-grid-header border-1 bg-gray-100 sticky top[-1px]"> <tr class="nc-grid-header border-1 bg-gray-100 sticky top[-1px]">
<th data-testid="grid-id-column"> <th data-testid="grid-id-column">
<div class="w-full h-full bg-gray-100 flex min-w-[70px] pl-5 pr-1 items-center" data-testid="nc-check-all"> <div class="w-full h-full bg-gray-100 flex min-w-[70px] pl-5 pr-1 items-center"
<template v-if="!readOnly"> data-testid="nc-check-all">
<div class="nc-no-label text-gray-500" :class="{ hidden: selectedAllRecords }">#</div> <template v-if="!readOnly">
<div <div class="nc-no-label text-gray-500" :class="{ hidden: selectedAllRecords }">#</div>
:class="{ hidden: !selectedAllRecords, flex: selectedAllRecords }" <div
class="nc-check-all w-full items-center" :class="{ hidden: !selectedAllRecords, flex: selectedAllRecords }"
> class="nc-check-all w-full items-center"
<a-checkbox v-model:checked="selectedAllRecords" /> >
<a-checkbox v-model:checked="selectedAllRecords" />
<span class="flex-1" /> <span class="flex-1" />
</div> </div>
</template> </template>
<template v-else> <template v-else>
<div class="text-gray-500">#</div> <div class="text-gray-500">#</div>
</template> </template>
</div> </div>
</th> </th>
<th <th
v-for="col in fields" v-for="col in fields"
:key="col.title" :key="col.title"
v-xc-ver-resize v-xc-ver-resize
:data-col="col.id" :data-col="col.id"
:data-title="col.title" :data-title="col.title"
@xcresize="onresize(col.id, $event)" @xcresize="onresize(col.id, $event)"
@xcresizing="onXcResizing(col.title, $event)" @xcresizing="onXcResizing(col.title, $event)"
@xcresized="resizingCol = null" @xcresized="resizingCol = null"
> >
<div class="w-full h-full bg-gray-100 flex items-center"> <div class="w-full h-full bg-gray-100 flex items-center">
<LazySmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" :hide-menu="readOnly" /> <LazySmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" :hide-menu="readOnly" />
<LazySmartsheetHeaderCell v-else :column="col" :hide-menu="readOnly" /> <LazySmartsheetHeaderCell v-else :column="col" :hide-menu="readOnly" />
</div> </div>
</th> </th>
<th <th
v-if="isAddingColumnAllowed" v-if="isAddingColumnAllowed"
v-e="['c:column:add']" v-e="['c:column:add']"
class="cursor-pointer" class="cursor-pointer"
@click.stop="addColumnDropdown = true" @click.stop="addColumnDropdown = true"
>
<a-dropdown
v-model:visible="addColumnDropdown"
:trigger="['click']"
overlay-class-name="nc-dropdown-grid-add-column"
> >
<a-dropdown <div class="h-full w-[60px] flex items-center justify-center">
v-model:visible="addColumnDropdown" <MdiPlus class="text-sm nc-column-add" />
:trigger="['click']" </div>
overlay-class-name="nc-dropdown-grid-add-column"
>
<div class="h-full w-[60px] flex items-center justify-center">
<MdiPlus class="text-sm nc-column-add" />
</div>
<template #overlay> <template #overlay>
<SmartsheetColumnEditOrAddProvider <SmartsheetColumnEditOrAddProvider
v-if="addColumnDropdown" v-if="addColumnDropdown"
:column-position="columnOrder" :column-position="columnOrder"
@submit="closeAddColumnDropdown" @submit="closeAddColumnDropdown"
@cancel="closeAddColumnDropdown" @cancel="closeAddColumnDropdown"
@click.stop @click.stop
@keydown.stop @keydown.stop
/> />
</template> </template>
</a-dropdown> </a-dropdown>
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody ref="tbodyEl"> <tbody ref="tbodyEl">
<LazySmartsheetRow v-for="(row, rowIndex) of data" ref="rowRefs" :key="rowIndex" :row="row"> <LazySmartsheetRow v-for="(row, rowIndex) of data" ref="rowRefs" :key="rowIndex" :row="row">
<template #default="{ state }"> <template #default="{ state }">
<tr class="nc-grid-row" :data-testid="`grid-row-${rowIndex}`"> <tr class="nc-grid-row" :data-testid="`grid-row-${rowIndex}`">
<td key="row-index" class="caption nc-grid-cell pl-5 pr-1" :data-testid="`cell-Id-${rowIndex}`"> <td key="row-index" class="caption nc-grid-cell pl-5 pr-1" :data-testid="`cell-Id-${rowIndex}`">
<div class="items-center flex gap-1 min-w-[55px]"> <div class="items-center flex gap-1 min-w-[55px]">
<div <div
v-if="!readOnly || !isLocked" v-if="!readOnly || !isLocked"
class="nc-row-no text-xs text-gray-500" class="nc-row-no text-xs text-gray-500"
:class="{ toggle: !readOnly, hidden: row.rowMeta.selected }" :class="{ toggle: !readOnly, hidden: row.rowMeta.selected }"
> >
{{ rowIndex + 1 }} {{ rowIndex + 1 }}
</div> </div>
<div <div
v-if="!readOnly" v-if="!readOnly"
:class="{ hidden: !row.rowMeta.selected, flex: row.rowMeta.selected }" :class="{ hidden: !row.rowMeta.selected, flex: row.rowMeta.selected }"
class="nc-row-expand-and-checkbox" class="nc-row-expand-and-checkbox"
> >
<a-checkbox v-model:checked="row.rowMeta.selected" /> <a-checkbox v-model:checked="row.rowMeta.selected" />
</div> </div>
<span class="flex-1" /> <span class="flex-1" />
<div <div
v-if="!readOnly || hasRole('commenter', true) || hasRole('viewer', true)" v-if="!readOnly || hasRole('commenter', true) || hasRole('viewer', true)"
class="nc-expand" class="nc-expand"
:data-testid="`nc-expand-${rowIndex}`" :data-testid="`nc-expand-${rowIndex}`"
:class="{ 'nc-comment': row.rowMeta?.commentCount }" :class="{ 'nc-comment': row.rowMeta?.commentCount }"
> >
<a-spin <a-spin
v-if="row.rowMeta.saving" v-if="row.rowMeta.saving"
class="!flex items-center" class="!flex items-center"
:data-testid="`row-save-spinner-${rowIndex}`" :data-testid="`row-save-spinner-${rowIndex}`"
/> />
<template v-else> <template v-else>
<span <span
v-if="row.rowMeta?.commentCount" v-if="row.rowMeta?.commentCount"
class="py-1 px-3 rounded-full text-xs cursor-pointer select-none transform hover:(scale-110)" class="py-1 px-3 rounded-full text-xs cursor-pointer select-none transform hover:(scale-110)"
@ -749,84 +751,84 @@ const closeAddColumnDropdown = () => {
> >
{{ row.rowMeta.commentCount }} {{ row.rowMeta.commentCount }}
</span> </span>
<div <div
v-else v-else
class="cursor-pointer flex items-center border-1 active:ring rounded p-1 hover:(bg-primary bg-opacity-10)" class="cursor-pointer flex items-center border-1 active:ring rounded p-1 hover:(bg-primary bg-opacity-10)"
> >
<MdiArrowExpand <MdiArrowExpand
v-e="['c:row-expand']" v-e="['c:row-expand']"
class="select-none transform hover:(text-accent scale-120) nc-row-expand" class="select-none transform hover:(text-accent scale-120) nc-row-expand"
@click="expandForm(row, state)" @click="expandForm(row, state)"
/> />
</div> </div>
</template> </template>
</div>
</div> </div>
</td> </div>
<SmartsheetTableDataCell </td>
v-for="(columnObj, colIndex) of fields" <SmartsheetTableDataCell
:key="columnObj.id" v-for="(columnObj, colIndex) of fields"
class="cell relative cursor-pointer nc-grid-cell" :key="columnObj.id"
:class="{ class="cell relative cursor-pointer nc-grid-cell"
:class="{
'active': hasEditPermission && isCellSelected(rowIndex, colIndex), 'active': hasEditPermission && isCellSelected(rowIndex, colIndex),
'nc-required-cell': isColumnRequiredAndNull(columnObj, row.row), 'nc-required-cell': isColumnRequiredAndNull(columnObj, row.row),
}" }"
:data-testid="`cell-${columnObj.title}-${rowIndex}`" :data-testid="`cell-${columnObj.title}-${rowIndex}`"
:data-key="rowIndex + columnObj.id" :data-key="rowIndex + columnObj.id"
:data-col="columnObj.id" :data-col="columnObj.id"
:data-title="columnObj.title" :data-title="columnObj.title"
@click="selectCell(rowIndex, colIndex)" @click="selectCell(rowIndex, colIndex)"
@dblclick="makeEditable(row, columnObj)" @dblclick="makeEditable(row, columnObj)"
@mousedown="startSelectRange($event, rowIndex, colIndex)" @mousedown="startSelectRange($event, rowIndex, colIndex)"
@mouseover="endSelectRange(rowIndex, colIndex)" @mouseover="endSelectRange(rowIndex, colIndex)"
@contextmenu="showContextMenu($event, { row: rowIndex, col: colIndex })" @contextmenu="showContextMenu($event, { row: rowIndex, col: colIndex })"
> >
<div v-if="!switchingTab" class="w-full h-full"> <div v-if="!switchingTab" class="w-full h-full">
<LazySmartsheetVirtualCell <LazySmartsheetVirtualCell
v-if="isVirtualCol(columnObj)" v-if="isVirtualCol(columnObj)"
v-model="row.row[columnObj.title]" v-model="row.row[columnObj.title]"
:column="columnObj" :column="columnObj"
:active="selectedCell.col === colIndex && selectedCell.row === rowIndex" :active="selectedCell.col === colIndex && selectedCell.row === rowIndex"
:row="row" :row="row"
@navigate="onNavigate" @navigate="onNavigate"
/> />
<LazySmartsheetCell <LazySmartsheetCell
v-else v-else
v-model="row.row[columnObj.title]" v-model="row.row[columnObj.title]"
:column="columnObj" :column="columnObj"
:edit-enabled=" :edit-enabled="
!!hasEditPermission && !!editEnabled && selectedCell.col === colIndex && selectedCell.row === rowIndex !!hasEditPermission && !!editEnabled && selectedCell.col === colIndex && selectedCell.row === rowIndex
" "
:row-index="rowIndex" :row-index="rowIndex"
:active="selectedCell.col === colIndex && selectedCell.row === rowIndex" :active="selectedCell.col === colIndex && selectedCell.row === rowIndex"
@update:edit-enabled="editEnabled = $event" @update:edit-enabled="editEnabled = $event"
@save="updateOrSaveRow(row, columnObj.title, state)" @save="updateOrSaveRow(row, columnObj.title, state)"
@navigate="onNavigate" @navigate="onNavigate"
@cancel="editEnabled = false" @cancel="editEnabled = false"
/> />
</div> </div>
</SmartsheetTableDataCell> </SmartsheetTableDataCell>
</tr> </tr>
</template> </template>
</LazySmartsheetRow> </LazySmartsheetRow>
<tr v-if="isAddingEmptyRowAllowed"> <tr v-if="isAddingEmptyRowAllowed">
<td <td
v-e="['c:row:add:grid-bottom']" v-e="['c:row:add:grid-bottom']"
:colspan="visibleColLength + 1" :colspan="visibleColLength + 1"
class="text-left pointer nc-grid-add-new-cell cursor-pointer" class="text-left pointer nc-grid-add-new-cell cursor-pointer"
@click="addEmptyRow()" @click="addEmptyRow()"
> >
<div class="px-2 w-full flex items-center text-gray-500"> <div class="px-2 w-full flex items-center text-gray-500">
<MdiPlus class="text-pint-500 text-xs ml-2 text-primary" /> <MdiPlus class="text-pint-500 text-xs ml-2 text-primary" />
<span class="ml-1"> <span class="ml-1">
{{ $t('activity.addRow') }} {{ $t('activity.addRow') }}
</span> </span>
</div> </div>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

40
packages/nc-gui/composables/useMultiSelect/index.ts

@ -1,8 +1,10 @@
import type { MaybeRef } from '@vueuse/core' import type { MaybeRef } from '@vueuse/core'
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { isVirtualCol, RelationTypes, UITypes } from 'nocodb-sdk' import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
import type { Cell } from './cellRange' import type { Cell } from './cellRange'
import { CellRange } from './cellRange' import { CellRange } from './cellRange'
import { useMetas } from '~/composables/useMetas'
import { extractPkFromRow } from '~/utils'
import { copyTable, message, reactive, ref, unref, useCopy, useEventListener, useI18n } from '#imports' import { copyTable, message, reactive, ref, unref, useCopy, useEventListener, useI18n } from '#imports'
import type { Row } from '~/lib' import type { Row } from '~/lib'
@ -39,6 +41,7 @@ function convertCellData(args: { from: UITypes; to: UITypes; value: any }) {
* Utility to help with multi-selecting rows/cells in the smartsheet * Utility to help with multi-selecting rows/cells in the smartsheet
*/ */
export function useMultiSelect( export function useMultiSelect(
_meta: MaybeRef<TableType>,
fields: MaybeRef<ColumnType[]>, fields: MaybeRef<ColumnType[]>,
data: MaybeRef<Row[]>, data: MaybeRef<Row[]>,
_editEnabled: MaybeRef<boolean>, _editEnabled: MaybeRef<boolean>,
@ -49,10 +52,14 @@ export function useMultiSelect(
keyEventHandler?: Function, keyEventHandler?: Function,
syncCellData?: Function, syncCellData?: Function,
) { ) {
const meta = ref(_meta)
const { t } = useI18n() const { t } = useI18n()
const { copy } = useCopy() const { copy } = useCopy()
const { getMeta } = useMetas()
let clipboardContext = $ref<{ value: any; uidt: UITypes } | null>(null) let clipboardContext = $ref<{ value: any; uidt: UITypes } | null>(null)
const editEnabled = ref(_editEnabled) const editEnabled = ref(_editEnabled)
@ -266,9 +273,35 @@ export function useMultiSelect(
await copyValue() await copyValue()
break break
case 86: case 86:
if(isVirtualCol(columnObj) && (columnObj.uidt !== UITypes.LinkToAnotherRecord || (columnObj.colOptions as LinkToAnotherRecordType)?.type !== RelationTypes.BELONGS_TO)) { // handle belongs to column
if (
columnObj.uidt === UITypes.LinkToAnotherRecord &&
(columnObj.colOptions as LinkToAnotherRecordType)?.type === RelationTypes.BELONGS_TO
) {
rowObj.row[columnObj.title!] = convertCellData({
value: clipboardContext.value,
from: clipboardContext.uidt,
to: columnObj.uidt as UITypes,
})
e.preventDefault()
const foreignKeyColumn: ColumnType = meta.value?.columns.find(
(c) => c.id === (columnObj.colOptions as LinkToAnotherRecordType)?.fk_child_column_id,
)
const relatedTableMeta = await getMeta((columnObj.colOptions as LinkToAnotherRecordType)?.fk_related_model_id!)
rowObj.row[foreignKeyColumn.title!] = extractPkFromRow(clipboardContext.value, relatedTableMeta!.columns!)
// makeEditable(rowObj, columnObj)
return syncCellData?.({ ...selectedCell, updatedColumnTitle: foreignKeyColumn.title })
}
// if it's a virtual column excluding belongs to cell type skip paste
if (isVirtualCol(columnObj)) {
return message.info(t('msg.info.cannotPasteHere')) return message.info(t('msg.info.cannotPasteHere'))
} }
// const clipboardText = await getClipboardData() // const clipboardText = await getClipboardData()
if (clipboardContext) { if (clipboardContext) {
rowObj.row[columnObj.title!] = convertCellData({ rowObj.row[columnObj.title!] = convertCellData({
@ -304,7 +337,6 @@ export function useMultiSelect(
} }
break break
} }
} }
useEventListener(document, 'keydown', onKeyDown) useEventListener(document, 'keydown', onKeyDown)

Loading…
Cancel
Save