|
|
|
import type { MaybeRef } from '@vueuse/core'
|
|
|
|
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
|
|
|
|
import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
|
|
|
|
import type { Cell } from './cellRange'
|
|
|
|
import { CellRange } from './cellRange'
|
|
|
|
import convertCellData from '~/composables/useMultiSelect/convertCellData'
|
|
|
|
import {
|
|
|
|
copyTable,
|
|
|
|
extractPkFromRow,
|
|
|
|
extractSdkResponseErrorMsg,
|
|
|
|
message,
|
|
|
|
reactive,
|
|
|
|
ref,
|
|
|
|
unref,
|
|
|
|
useCopy,
|
|
|
|
useEventListener,
|
|
|
|
useI18n,
|
|
|
|
useMetas,
|
|
|
|
useProject,
|
|
|
|
} from '#imports'
|
|
|
|
import type { Row } from '~/lib'
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Utility to help with multi-selecting rows/cells in the smartsheet
|
|
|
|
*/
|
|
|
|
export function useMultiSelect(
|
|
|
|
_meta: MaybeRef<TableType>,
|
|
|
|
fields: MaybeRef<ColumnType[]>,
|
|
|
|
data: MaybeRef<Row[]>,
|
|
|
|
_editEnabled: MaybeRef<boolean>,
|
|
|
|
isPkAvail: MaybeRef<boolean | undefined>,
|
|
|
|
clearCell: Function,
|
|
|
|
makeEditable: Function,
|
|
|
|
scrollToActiveCell?: (row?: number | null, col?: number | null) => void,
|
|
|
|
keyEventHandler?: Function,
|
|
|
|
syncCellData?: Function,
|
|
|
|
) {
|
|
|
|
const meta = ref(_meta)
|
|
|
|
|
|
|
|
const { t } = useI18n()
|
|
|
|
|
|
|
|
const { copy } = useCopy()
|
|
|
|
|
|
|
|
const { getMeta } = useMetas()
|
|
|
|
|
|
|
|
const { isMysql } = useProject()
|
|
|
|
|
|
|
|
let clipboardContext = $ref<{ value: any; uidt: UITypes } | null>(null)
|
|
|
|
|
|
|
|
const editEnabled = ref(_editEnabled)
|
|
|
|
|
|
|
|
const selectedCell = reactive<Cell>({ row: null, col: null })
|
|
|
|
const selectedRange = reactive(new CellRange())
|
|
|
|
let isMouseDown = $ref(false)
|
|
|
|
|
|
|
|
const columnLength = $computed(() => unref(fields)?.length)
|
|
|
|
|
|
|
|
async function copyValue(ctx?: Cell) {
|
|
|
|
try {
|
|
|
|
if (!selectedRange.isEmpty()) {
|
|
|
|
const cprows = unref(data).slice(selectedRange.start.row, selectedRange.end.row + 1) // slice the selected rows for copy
|
|
|
|
const cpcols = unref(fields).slice(selectedRange.start.col, selectedRange.end.col + 1) // slice the selected cols for copy
|
|
|
|
|
|
|
|
await copyTable(cprows, cpcols)
|
|
|
|
message.success(t('msg.info.copiedToClipboard'))
|
|
|
|
} else {
|
|
|
|
// if copy was called with context (right click position) - copy value from context
|
|
|
|
// else if there is just one selected cell, copy it's value
|
|
|
|
const cpRow = ctx?.row ?? selectedCell?.row
|
|
|
|
const cpCol = ctx?.col ?? selectedCell?.col
|
|
|
|
|
|
|
|
if (cpRow != null && cpCol != null) {
|
|
|
|
const rowObj = unref(data)[cpRow]
|
|
|
|
const columnObj = unref(fields)[cpCol]
|
|
|
|
|
|
|
|
let textToCopy = (columnObj.title && rowObj.row[columnObj.title]) || ''
|
|
|
|
|
|
|
|
if (columnObj.uidt === UITypes.Checkbox) {
|
|
|
|
textToCopy = !!textToCopy
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof textToCopy === 'object') {
|
|
|
|
textToCopy = JSON.stringify(textToCopy)
|
|
|
|
}
|
|
|
|
await copy(textToCopy)
|
|
|
|
message.success(t('msg.info.copiedToClipboard'))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch {
|
|
|
|
message.error(t('msg.error.copyToClipboardError'))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function selectCell(row: number, col: number) {
|
|
|
|
selectedRange.clear()
|
|
|
|
if (selectedCell.row === row && selectedCell.col === col) return
|
|
|
|
editEnabled.value = false
|
|
|
|
selectedCell.row = row
|
|
|
|
selectedCell.col = col
|
|
|
|
}
|
|
|
|
|
|
|
|
function endSelectRange(row: number, col: number) {
|
|
|
|
if (!isMouseDown) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
selectedCell.row = null
|
|
|
|
selectedCell.col = null
|
|
|
|
selectedRange.endRange({ row, col })
|
|
|
|
}
|
|
|
|
|
|
|
|
function isCellSelected(row: number, col: number) {
|
|
|
|
if (selectedCell?.row === row && selectedCell?.col === col) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
if (selectedRange.isEmpty()) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
col >= selectedRange.start.col &&
|
|
|
|
col <= selectedRange.end.col &&
|
|
|
|
row >= selectedRange.start.row &&
|
|
|
|
row <= selectedRange.end.row
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
function startSelectRange(event: MouseEvent, row: number, col: number) {
|
|
|
|
// if there was a right click on selected range, don't restart the selection
|
|
|
|
const leftClickButton = 0
|
|
|
|
if (event?.button !== leftClickButton && isCellSelected(row, col)) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (unref(editEnabled)) {
|
|
|
|
event.preventDefault()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
isMouseDown = true
|
|
|
|
selectedRange.clear()
|
|
|
|
selectedRange.startRange({ row, col })
|
|
|
|
}
|
|
|
|
|
|
|
|
useEventListener(document, 'mouseup', (e) => {
|
|
|
|
// if the editEnabled is false prevent the mouseup event for not select text
|
|
|
|
if (!unref(editEnabled)) {
|
|
|
|
e.preventDefault()
|
|
|
|
}
|
|
|
|
|
|
|
|
isMouseDown = false
|
|
|
|
})
|
|
|
|
|
|
|
|
const onKeyDown = async (e: KeyboardEvent) => {
|
|
|
|
// invoke the keyEventHandler if provided and return if it returns true
|
|
|
|
if (await keyEventHandler?.(e)) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!selectedRange.isEmpty()) {
|
|
|
|
// In case the user press tabs or arrows keys
|
|
|
|
selectedCell.row = selectedRange.start.row
|
|
|
|
selectedCell.col = selectedRange.start.col
|
|
|
|
}
|
|
|
|
|
|
|
|
if (selectedCell.row === null || selectedCell.col === null) return
|
|
|
|
|
|
|
|
/** on tab key press navigate through cells */
|
|
|
|
switch (e.key) {
|
|
|
|
case 'Tab':
|
|
|
|
e.preventDefault()
|
|
|
|
selectedRange.clear()
|
|
|
|
|
|
|
|
if (e.shiftKey) {
|
|
|
|
if (selectedCell.col > 0) {
|
|
|
|
selectedCell.col--
|
|
|
|
editEnabled.value = false
|
|
|
|
} else if (selectedCell.row > 0) {
|
|
|
|
selectedCell.row--
|
|
|
|
selectedCell.col = unref(columnLength) - 1
|
|
|
|
editEnabled.value = false
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (selectedCell.col < unref(columnLength) - 1) {
|
|
|
|
selectedCell.col++
|
|
|
|
editEnabled.value = false
|
|
|
|
} else if (selectedCell.row < unref(data).length - 1) {
|
|
|
|
selectedCell.row++
|
|
|
|
selectedCell.col = 0
|
|
|
|
editEnabled.value = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
scrollToActiveCell?.()
|
|
|
|
break
|
|
|
|
/** on enter key press make cell editable */
|
|
|
|
case 'Enter':
|
|
|
|
e.preventDefault()
|
|
|
|
selectedRange.clear()
|
|
|
|
makeEditable(unref(data)[selectedCell.row], unref(fields)[selectedCell.col])
|
|
|
|
break
|
|
|
|
/** on delete key press clear cell */
|
|
|
|
case 'Delete':
|
|
|
|
e.preventDefault()
|
|
|
|
selectedRange.clear()
|
|
|
|
await clearCell(selectedCell as { row: number; col: number })
|
|
|
|
break
|
|
|
|
/** on arrow key press navigate through cells */
|
|
|
|
case 'ArrowRight':
|
|
|
|
e.preventDefault()
|
|
|
|
selectedRange.clear()
|
|
|
|
if (selectedCell.col < unref(columnLength) - 1) {
|
|
|
|
selectedCell.col++
|
|
|
|
scrollToActiveCell?.()
|
|
|
|
editEnabled.value = false
|
|
|
|
}
|
|
|
|
break
|
|
|
|
case 'ArrowLeft':
|
|
|
|
selectedRange.clear()
|
|
|
|
e.preventDefault()
|
|
|
|
if (selectedCell.col > 0) {
|
|
|
|
selectedCell.col--
|
|
|
|
scrollToActiveCell?.()
|
|
|
|
editEnabled.value = false
|
|
|
|
}
|
|
|
|
break
|
|
|
|
case 'ArrowUp':
|
|
|
|
selectedRange.clear()
|
|
|
|
e.preventDefault()
|
|
|
|
if (selectedCell.row > 0) {
|
|
|
|
selectedCell.row--
|
|
|
|
scrollToActiveCell?.()
|
|
|
|
editEnabled.value = false
|
|
|
|
}
|
|
|
|
break
|
|
|
|
case 'ArrowDown':
|
|
|
|
selectedRange.clear()
|
|
|
|
e.preventDefault()
|
|
|
|
if (selectedCell.row < unref(data).length - 1) {
|
|
|
|
selectedCell.row++
|
|
|
|
scrollToActiveCell?.()
|
|
|
|
editEnabled.value = false
|
|
|
|
}
|
|
|
|
break
|
|
|
|
default: {
|
|
|
|
const rowObj = unref(data)[selectedCell.row]
|
|
|
|
|
|
|
|
const columnObj = unref(fields)[selectedCell.col]
|
|
|
|
|
|
|
|
if ((!unref(editEnabled) && e.metaKey) || e.ctrlKey) {
|
|
|
|
switch (e.keyCode) {
|
|
|
|
// copy - ctrl/cmd +c
|
|
|
|
case 67:
|
|
|
|
// set clipboard context only if single cell selected
|
|
|
|
if (rowObj.row[columnObj.title!]) {
|
|
|
|
clipboardContext = {
|
|
|
|
value: rowObj.row[columnObj.title!],
|
|
|
|
uidt: columnObj.uidt as UITypes,
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
clipboardContext = null
|
|
|
|
}
|
|
|
|
await copyValue()
|
|
|
|
break
|
|
|
|
case 86:
|
|
|
|
try {
|
|
|
|
// handle belongs to column
|
|
|
|
if (
|
|
|
|
columnObj.uidt === UITypes.LinkToAnotherRecord &&
|
|
|
|
(columnObj.colOptions as LinkToAnotherRecordType)?.type === RelationTypes.BELONGS_TO
|
|
|
|
) {
|
|
|
|
if (!clipboardContext.value || typeof clipboardContext.value !== 'object') {
|
|
|
|
return message.info('Invalid data')
|
|
|
|
}
|
|
|
|
rowObj.row[columnObj.title!] = convertCellData(
|
|
|
|
{
|
|
|
|
value: clipboardContext.value,
|
|
|
|
from: clipboardContext.uidt,
|
|
|
|
to: columnObj.uidt as UITypes,
|
|
|
|
},
|
|
|
|
isMysql.value,
|
|
|
|
)
|
|
|
|
e.preventDefault()
|
|
|
|
|
|
|
|
const foreignKeyColumn: ColumnType = meta.value?.columns.find(
|
|
|
|
(column: ColumnType) => column.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 as any)!.columns!,
|
|
|
|
)
|
|
|
|
|
|
|
|
return await 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.notSupported'))
|
|
|
|
}
|
|
|
|
|
|
|
|
if (clipboardContext) {
|
|
|
|
rowObj.row[columnObj.title!] = convertCellData(
|
|
|
|
{
|
|
|
|
value: clipboardContext.value,
|
|
|
|
from: clipboardContext.uidt,
|
|
|
|
to: columnObj.uidt as UITypes,
|
|
|
|
},
|
|
|
|
isMysql.value,
|
|
|
|
)
|
|
|
|
e.preventDefault()
|
|
|
|
syncCellData?.(selectedCell)
|
|
|
|
} else {
|
|
|
|
clearCell(selectedCell as { row: number; col: number }, true)
|
|
|
|
makeEditable(rowObj, columnObj)
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
message.error(await extractSdkResponseErrorMsg(error))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (unref(editEnabled) || e.ctrlKey || e.altKey || e.metaKey) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
/** on letter key press make cell editable and empty */
|
|
|
|
if (e.key.length === 1) {
|
|
|
|
if (!unref(isPkAvail) && !rowObj.rowMeta.new) {
|
|
|
|
// Update not allowed for table which doesn't have primary Key
|
|
|
|
return message.info(t('msg.info.updateNotAllowedWithoutPK'))
|
|
|
|
}
|
|
|
|
if (makeEditable(rowObj, columnObj) && columnObj.title) {
|
|
|
|
rowObj.row[columnObj.title] = ''
|
|
|
|
}
|
|
|
|
// editEnabled = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
useEventListener(document, 'keydown', onKeyDown)
|
|
|
|
|
|
|
|
return {
|
|
|
|
selectCell,
|
|
|
|
startSelectRange,
|
|
|
|
endSelectRange,
|
|
|
|
clearSelectedRange: selectedRange.clear.bind(selectedRange),
|
|
|
|
copyValue,
|
|
|
|
isCellSelected,
|
|
|
|
selectedCell,
|
|
|
|
}
|
|
|
|
}
|