mirror of
https://github.com/nocodb/nocodb.git
synced 2026-05-01 09:46:42 +00:00
1961 lines
71 KiB
TypeScript
1961 lines
71 KiB
TypeScript
import type { MaybeRef } from '@vueuse/core'
|
|
import type { AttachmentType, ColumnType, LinkToAnotherRecordType, PaginatedType, TableType, ViewType } from 'nocodb-sdk'
|
|
import { ColumnHelper, UITypes, isSystemColumn, isVirtualCol, populateUniqueFileName } from 'nocodb-sdk'
|
|
import { parse } from 'papaparse'
|
|
import type { Ref } from 'vue'
|
|
import { computed } from 'vue'
|
|
import { ComputedTypePasteError } from '../../error/computed-type-paste.error'
|
|
import { SelectTypeConversionError } from '../../error/select-type-conversion.error'
|
|
import type { SuppressedError } from '../../error/suppressed.error'
|
|
import { TypeConversionError } from '../../error/type-conversion.error'
|
|
import { generateUniqueColumnName } from '../../helpers/parsers/parserHelpers'
|
|
import type { Row } from '../../lib/types'
|
|
import type { Cell } from './cellRange'
|
|
import { CellRange } from './cellRange'
|
|
import convertCellData from './convertCellData'
|
|
|
|
const MAIN_MOUSE_PRESSED = 0
|
|
|
|
/**
|
|
* Utility to help with multi-selecting rows/cells in the smartsheet
|
|
*/
|
|
|
|
export function useMultiSelect(
|
|
_meta: MaybeRef<TableType | undefined>,
|
|
fields: MaybeRef<ColumnType[]>,
|
|
data: MaybeRef<Row[]> | MaybeRef<Map<number, Row>>,
|
|
_totalRows?: MaybeRef<number>,
|
|
_editEnabled: MaybeRef<boolean>,
|
|
isPkAvail: MaybeRef<boolean | undefined>,
|
|
contextMenu: Ref<boolean>,
|
|
clearCell: Function,
|
|
clearSelectedRangeOfCells: Function,
|
|
makeEditable: Function,
|
|
scrollToCell?: (row?: number | null, col?: number | null, scrollBehaviour?: ScrollBehavior) => void,
|
|
expandRows?: ({
|
|
newRows,
|
|
newColumns,
|
|
cellsOverwritten,
|
|
rowsUpdated,
|
|
}: {
|
|
newRows: number
|
|
newColumns: number
|
|
cellsOverwritten: number
|
|
rowsUpdated: number
|
|
}) => Promise<{
|
|
continue: boolean
|
|
expand: boolean
|
|
}>,
|
|
keyEventHandler?: Function,
|
|
syncCellData?: Function,
|
|
bulkUpdateRows?: (
|
|
rows: Row[],
|
|
props: string[],
|
|
metas?: { metaValue?: TableType; viewMetaValue?: ViewType },
|
|
undo?: boolean,
|
|
) => Promise<void>,
|
|
bulkUpsertRows?: (
|
|
insertRows: Row[],
|
|
updateRows: Row[],
|
|
props: string[],
|
|
metas?: { metaValue?: TableType; viewMetaValue?: ViewType },
|
|
newColumns?: Partial<ColumnType>[],
|
|
) => Promise<void>,
|
|
fillHandle?: MaybeRef<HTMLElement | undefined>,
|
|
view?: MaybeRef<ViewType | undefined>,
|
|
paginationData?: MaybeRef<PaginatedType | undefined>,
|
|
changePage?: (page: number) => void,
|
|
fetchChunk?: (chunkId: number) => Promise<void>,
|
|
onActiveCellChanged?: () => void,
|
|
getRows?: Promise<Row[]>,
|
|
isInfiniteScroll: boolean = true,
|
|
) {
|
|
const meta = ref(_meta)
|
|
|
|
const MAX_ROW_SELECTION = 100
|
|
|
|
const CHUNK_SIZE = 50
|
|
|
|
const { t } = useI18n()
|
|
|
|
const { copy } = useCopy()
|
|
|
|
const { getMeta, metas } = useMetas()
|
|
|
|
const { appInfo } = useGlobal()
|
|
|
|
const { isMysql, isPg } = useBase()
|
|
|
|
const { base } = storeToRefs(useBase())
|
|
|
|
const { api } = useApi()
|
|
|
|
const { $api } = useNuxtApp()
|
|
|
|
const { fillRows } = useNocoAi()
|
|
|
|
const { addUndo, clone, defineViewScope } = useUndoRedo()
|
|
|
|
const { isDataReadOnly } = useRoles()
|
|
|
|
const { meta: metaKey } = useMagicKeys()
|
|
|
|
const { isFeatureEnabled } = useBetaFeatureToggle()
|
|
|
|
const { isSqlView, isExternalSource } = useSmartsheetStoreOrThrow()
|
|
|
|
const { blockExternalSourceRecordVisibility, showUpgradeToAddMoreAttachmentsInCell, maxAttachmentsAllowedInCell } =
|
|
useEeConfig()
|
|
|
|
const { batchUploadFiles } = useAttachment()
|
|
|
|
const {
|
|
setCellClipboardDataItem,
|
|
getClipboardItemId,
|
|
getCurrentCopiedCellClipboardData,
|
|
extractCellClipboardData,
|
|
waitingCellClipboardDataIds,
|
|
} = useNcClipboardData()
|
|
const aiMode = ref(false)
|
|
|
|
const isArrayStructure = typeof unref(data) === 'object' && Array.isArray(unref(data))
|
|
|
|
const paginationDataRef = ref(paginationData)
|
|
|
|
const editEnabled = ref(_editEnabled)
|
|
|
|
const isMouseDown = ref(false)
|
|
|
|
const isFillMode = ref(false)
|
|
|
|
const activeView = ref(view)
|
|
|
|
const selectedRange = reactive(new CellRange())
|
|
|
|
const fillRange = reactive(new CellRange())
|
|
|
|
const activeCell = reactive<Nullable<Cell>>({ row: null, col: null })
|
|
|
|
const columnLength = computed(() => unref(fields)?.length)
|
|
|
|
const isCellActive = computed(
|
|
() => !(activeCell.row === null || activeCell.col === null || isNaN(activeCell.row) || isNaN(activeCell.col)),
|
|
)
|
|
|
|
const removeInlineAddRecord = computed(
|
|
() => blockExternalSourceRecordVisibility(isExternalSource.value) && unref(_totalRows!) >= EXTERNAL_SOURCE_VISIBLE_ROWS,
|
|
)
|
|
|
|
function limitSelection(anchor: Cell, end: Cell): Cell {
|
|
const limitedEnd = { ...end }
|
|
const totalRows = Math.abs(end.row - anchor.row) + 1
|
|
if (totalRows > MAX_ROW_SELECTION) {
|
|
const direction = end.row > anchor.row ? 1 : -1
|
|
limitedEnd.row = anchor.row + (MAX_ROW_SELECTION - 1) * direction
|
|
}
|
|
return limitedEnd
|
|
}
|
|
|
|
function makeActive(row: number, col: number) {
|
|
if (activeCell.row === row && activeCell.col === col) {
|
|
return
|
|
}
|
|
|
|
// disable edit mode if active cell is changed
|
|
editEnabled.value = false
|
|
|
|
activeCell.row = row
|
|
activeCell.col = col
|
|
}
|
|
|
|
const copyTable = async (rows: Row[], cols: ColumnType[]) => {
|
|
const {
|
|
html: copyHTML,
|
|
text: copyPlainText,
|
|
clipboardItemConfig,
|
|
} = serializeRange(rows, cols, {
|
|
isPg,
|
|
isMysql,
|
|
meta: meta.value,
|
|
metas: metas.value,
|
|
})
|
|
|
|
const blobHTML = new Blob([copyHTML], { type: 'text/html' })
|
|
const blobPlainText = new Blob([copyPlainText], { type: 'text/plain' })
|
|
|
|
const clipboardItem: NcClipboardDataItemType = {
|
|
...clipboardItemConfig,
|
|
tableId: meta.value?.id,
|
|
id: getClipboardItemId(),
|
|
}
|
|
|
|
const res = await (navigator.clipboard?.write([
|
|
new ClipboardItem({ [blobHTML.type]: blobHTML, [blobPlainText.type]: blobPlainText }),
|
|
]) ?? copy(copyPlainText))
|
|
|
|
setCellClipboardDataItem(clipboardItem)
|
|
|
|
return res
|
|
}
|
|
|
|
async function copyValue(ctx?: Cell) {
|
|
try {
|
|
if (selectedRange.start !== null && selectedRange.end !== null && !selectedRange.isSingleCell()) {
|
|
let cprows
|
|
if (isArrayStructure) {
|
|
cprows = unref(data as Row[]).slice(selectedRange.start.row, selectedRange.end.row + 1) // slice the selected rows for copy
|
|
} else {
|
|
const startChunkId = Math.floor(selectedRange.start.row / CHUNK_SIZE)
|
|
const endChunkId = Math.floor(selectedRange.end.row / CHUNK_SIZE)
|
|
|
|
const chunksToFetch = new Set()
|
|
for (let chunkId = startChunkId; chunkId <= endChunkId; chunkId++) {
|
|
chunksToFetch.add(chunkId)
|
|
}
|
|
|
|
// Fetch all required chunks
|
|
await Promise.all([...chunksToFetch].map(fetchChunk))
|
|
cprows = await getRows(selectedRange.start.row, selectedRange.end.row)
|
|
}
|
|
const cpcols = unref(fields).slice(selectedRange.start.col, selectedRange.end.col + 1) // slice the selected cols for copy
|
|
|
|
await copyTable(cprows, cpcols)
|
|
message.toast(
|
|
t(`msg.toast.nCell${cprows.length * cpcols.length === 1 ? '' : 's'}Copied`, {
|
|
n: cprows.length * cpcols.length,
|
|
}),
|
|
)
|
|
} 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 ?? activeCell.row
|
|
const cpCol = ctx?.col ?? activeCell.col
|
|
|
|
if (cpRow != null && cpCol != null) {
|
|
const rowObj = isArrayStructure ? unref(data as Row[])[cpRow] : unref(data as Map<number, Row>).get(cpRow)
|
|
if (!rowObj) return
|
|
const columnObj = unref(fields)[cpCol]
|
|
|
|
const { textToCopy, cellValue, clipboardColumn, rowId } = valueToCopy(rowObj, columnObj, {
|
|
meta: meta.value,
|
|
metas: metas.value,
|
|
isPg,
|
|
isMysql,
|
|
})
|
|
|
|
const plainTextValue = isValidValue(textToCopy) ? textToCopy : ''
|
|
|
|
await copy(plainTextValue)
|
|
|
|
const clipboardItem: NcClipboardDataItemType = {
|
|
dbCellValueArr: [[cellValue]],
|
|
columns: [clipboardColumn],
|
|
copiedPlainText: plainTextValue,
|
|
rowIds: [rowId],
|
|
tableId: meta.value?.id,
|
|
id: getClipboardItemId(),
|
|
}
|
|
|
|
setCellClipboardDataItem(clipboardItem)
|
|
message.toast(
|
|
t(`msg.toast.nCellCopied`, {
|
|
n: 1,
|
|
}),
|
|
)
|
|
}
|
|
}
|
|
} catch {
|
|
message.error(t('msg.error.copyToClipboardError'))
|
|
}
|
|
}
|
|
|
|
const fillRangeMap = computed(() => {
|
|
/*
|
|
`${rowIndex}-${colIndex}`: true | false
|
|
*/
|
|
const map: Record<string, boolean> = {}
|
|
|
|
if (fillRange._start === null || fillRange._end === null) {
|
|
return map
|
|
}
|
|
|
|
for (let row = fillRange.start.row; row <= fillRange.end.row; row++) {
|
|
for (let col = fillRange.start.col; col <= fillRange.end.col; col++) {
|
|
map[`${row}-${col}`] = true
|
|
}
|
|
}
|
|
|
|
return map
|
|
})
|
|
|
|
const selectRangeMap = computed(() => {
|
|
/*
|
|
`${rowIndex}-${colIndex}`: true | false
|
|
*/
|
|
const map: Record<string, boolean> = {}
|
|
|
|
if (activeCell.row !== null && activeCell.col !== null) {
|
|
map[`${activeCell.row}-${activeCell.col}`] = true
|
|
}
|
|
|
|
if (selectedRange._start === null || selectedRange._end === null) {
|
|
return map
|
|
}
|
|
|
|
for (let row = selectedRange.start.row; row <= selectedRange.end.row; row++) {
|
|
for (let col = selectedRange.start.col; col <= selectedRange.end.col; col++) {
|
|
map[`${row}-${col}`] = true
|
|
}
|
|
}
|
|
|
|
return map
|
|
})
|
|
|
|
function handleMouseOver(event: MouseEvent, row: number, col: number) {
|
|
if (isFillMode.value) {
|
|
const rw = isArrayStructure ? (unref(data) as Row[])[row] : (unref(data) as Map<number, Row>).get(row)
|
|
|
|
if (!rw) return
|
|
|
|
if (!selectedRange._start || !selectedRange._end) return
|
|
|
|
// fill is not supported for new rows yet
|
|
if (rw.rowMeta.new) return
|
|
|
|
const endRow = Math.min(selectedRange._start.row + 100, row)
|
|
const endCol = aiMode.value ? Math.min(selectedRange._start.col + 100, col) : selectedRange._end.col
|
|
|
|
fillRange.endRange({
|
|
row: endRow,
|
|
col: endCol,
|
|
})
|
|
|
|
scrollToCell?.(endRow, col)
|
|
return
|
|
}
|
|
|
|
if (!isMouseDown.value) {
|
|
return
|
|
}
|
|
|
|
const limitedEnd = limitSelection(selectedRange.start, { row, col })
|
|
selectedRange.endRange(limitedEnd)
|
|
scrollToCell?.(limitedEnd.row, limitedEnd.col)
|
|
|
|
// avoid selecting text
|
|
event.preventDefault()
|
|
}
|
|
|
|
function handleMouseDown(event: MouseEvent, row: number, col: number) {
|
|
// if there was a right click on selected range, don't restart the selection
|
|
if (
|
|
(event?.button !== MAIN_MOUSE_PRESSED || (event?.button === MAIN_MOUSE_PRESSED && event.ctrlKey)) &&
|
|
selectRangeMap.value[`${row}-${col}`]
|
|
) {
|
|
return
|
|
}
|
|
|
|
// if edit is enabled, don't start the selection (some cells shrink after edit mode, which causes the selection to expand if flag is set)
|
|
if (!editEnabled.value) isMouseDown.value = true
|
|
|
|
contextMenu.value = false
|
|
|
|
// avoid text selection
|
|
event.preventDefault()
|
|
|
|
// if shift key is pressed, extend the selection
|
|
if (event.shiftKey) {
|
|
// if shift key is pressed, don't restart the selection (unless there is no active cell)
|
|
if (activeCell.col === null || activeCell.row === null) {
|
|
selectedRange.startRange({ row, col })
|
|
}
|
|
|
|
selectedRange.endRange({ row, col })
|
|
return
|
|
}
|
|
|
|
// start a new selection
|
|
selectedRange.startRange({ row, col })
|
|
|
|
if (activeCell.row !== row || activeCell.col !== col) {
|
|
// clear active cell on selection start
|
|
// activeCell.row = null
|
|
activeCell.col = null
|
|
}
|
|
}
|
|
|
|
const handleCellClick = (event: MouseEvent, row: number, col: number) => {
|
|
// if shift key is pressed, don't change the active cell (unless there is no active cell)
|
|
if (!event.shiftKey || activeCell.col === null || activeCell.row === null) {
|
|
makeActive(row, col)
|
|
}
|
|
|
|
scrollToCell?.(row, col)
|
|
}
|
|
|
|
function isPasteable(row?: Row, col?: ColumnType, showInfo = false) {
|
|
if (!row || !col) {
|
|
if (showInfo) {
|
|
message.toast('Please select a cell to paste')
|
|
}
|
|
return false
|
|
}
|
|
|
|
// skip pasting virtual columns (including LTAR columns for now) and system columns
|
|
if (isVirtualCol(col) || isSystemColumn(col) || col?.readonly) {
|
|
if (showInfo) {
|
|
message.toast(t('msg.info.pasteNotSupported'))
|
|
}
|
|
return false
|
|
}
|
|
|
|
// skip pasting auto increment columns
|
|
if (col.ai) {
|
|
if (showInfo) {
|
|
message.toast(t('msg.info.autoIncFieldNotEditable'))
|
|
}
|
|
return false
|
|
}
|
|
|
|
// skip pasting primary key columns
|
|
if (col.pk && !row.rowMeta.new) {
|
|
if (showInfo) {
|
|
message.toast(t('msg.info.editingPKnotSupported'))
|
|
}
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
const v2HandleFillValue = async ({
|
|
rawMatrix,
|
|
cpCols,
|
|
rowToPaste,
|
|
data,
|
|
}: {
|
|
rawMatrix: string[][]
|
|
cpCols: (ColumnType & {
|
|
extra?: any | never
|
|
})[]
|
|
rowToPaste: { start: number; end: number }
|
|
data: Row[]
|
|
}) => {
|
|
if (rawMatrix.length === 0) return
|
|
const direction = rowToPaste.end - rowToPaste.start > 0 ? 1 : -1
|
|
// reverse if going up
|
|
const rawMatrixFromDirection = direction === -1 ? rawMatrix.reverse() : rawMatrix
|
|
// we transform from rows to cols based
|
|
const rawMatrixTransposed = rawMatrixFromDirection[0]!.map((_, colIndex) =>
|
|
rawMatrixFromDirection.map((row) => row[colIndex]),
|
|
)
|
|
const fillValuesByCols: any[][] = []
|
|
const numberOfRows = Math.abs(rowToPaste.end - rowToPaste.start) + 1
|
|
|
|
for (const [i, rowsOfCol] of rawMatrixTransposed.entries()) {
|
|
const cpCol = cpCols[i]
|
|
const populatedFillHandle = ColumnHelper.populateFillHandle({
|
|
column: cpCol!,
|
|
highlightedData: rowsOfCol,
|
|
numberOfRows,
|
|
})
|
|
if (populatedFillHandle) {
|
|
fillValuesByCols.push(populatedFillHandle)
|
|
}
|
|
// TODO: else, but should not happen
|
|
}
|
|
|
|
// apply the populated fill handle to rows
|
|
// maybe TODO: extract to other function
|
|
const rowsToPaste: Row[] = []
|
|
let incrementIndex = 0
|
|
// We can use this if we want to avoid same info multiple times per column
|
|
const isColInfoShown = {} as Record<string, boolean>
|
|
for (
|
|
let rowIndex = rowToPaste.start + direction * rawMatrix.length;
|
|
direction === 1 ? rowIndex <= rowToPaste.end : rowIndex >= rowToPaste.end;
|
|
// Increment/decrement row counter based on fill direction
|
|
rowIndex += direction
|
|
) {
|
|
const rowObj = data[rowIndex]
|
|
if (rowObj) {
|
|
for (const [colIndex, cpCol] of cpCols.entries()) {
|
|
const pasteValue = convertCellData(
|
|
{
|
|
value: fillValuesByCols[colIndex!]![incrementIndex],
|
|
to: cpCol.uidt as UITypes,
|
|
column: cpCol,
|
|
appInfo: unref(appInfo),
|
|
maxAttachmentsAllowedInCell: maxAttachmentsAllowedInCell.value,
|
|
showUpgradeToAddMoreAttachmentsInCell,
|
|
isInfoShown: isColInfoShown[cpCol.title!],
|
|
markInfoShown: () => {
|
|
isColInfoShown[cpCol.title!] = true
|
|
},
|
|
},
|
|
isMysql(meta.value?.source_id),
|
|
true,
|
|
)
|
|
rowObj.row[cpCol.title] = pasteValue
|
|
}
|
|
rowsToPaste.push(rowObj)
|
|
}
|
|
incrementIndex++
|
|
}
|
|
|
|
// If not in AI fill mode, perform a regular bulk update
|
|
await bulkUpdateRows?.(
|
|
rowsToPaste,
|
|
cpCols.map((k) => k.title!),
|
|
)
|
|
}
|
|
|
|
const handleMouseUp = (_event: MouseEvent) => {
|
|
if (isFillMode.value) {
|
|
try {
|
|
const localAiMode = Boolean(aiMode.value)
|
|
|
|
isFillMode.value = false
|
|
aiMode.value = false
|
|
// We can use this if we want to avoid same info multiple times per column
|
|
const isColInfoShown = {} as Record<string, boolean>
|
|
|
|
if (fillRange._start === null || fillRange._end === null) return
|
|
|
|
if (selectedRange._start !== null && selectedRange._end !== null) {
|
|
const tempActiveCell = { row: selectedRange._start.row, col: selectedRange._start.col }
|
|
|
|
let cprows
|
|
|
|
if (isArrayStructure) {
|
|
cprows = (unref(data) as Row[]).slice(selectedRange.start.row, selectedRange.end.row + 1)
|
|
} else {
|
|
cprows = Array.from(unref(data) as Map<number, Row>)
|
|
.filter(([index]) => index >= selectedRange.start.row && index <= selectedRange.end.row)
|
|
.map(([, row]) => row)
|
|
}
|
|
|
|
const cpcols = unref(fields).slice(selectedRange.start.col, selectedRange.end.col + 1) // slice the selected cols for copy
|
|
|
|
const rawMatrix = serializeRange(cprows, cpcols, {
|
|
isPg,
|
|
isMysql,
|
|
meta: meta.value,
|
|
metas: metas.value,
|
|
}).json
|
|
|
|
const fillDirection = fillRange._start.row <= fillRange._end.row ? 1 : -1
|
|
const startRangeBottomMost = Math.max(
|
|
selectedRange.start.row,
|
|
selectedRange.end.row,
|
|
fillRange._start.row,
|
|
fillRange._end.row,
|
|
)
|
|
const startRangeTopMost = Math.min(
|
|
selectedRange.start.row,
|
|
selectedRange.end.row,
|
|
fillRange._start.row,
|
|
fillRange._end.row,
|
|
)
|
|
// if not localAiMode, use the new v2 handle fill logic
|
|
if (!localAiMode) {
|
|
const dataArray: Row[] = isArrayStructure
|
|
? (unref(data) as Row[])
|
|
: unref(data) instanceof Map
|
|
? Array.from(unref(data) as Map<number, Row>, ([_name, value]) => value)
|
|
: (unref(data) as Row[])
|
|
return v2HandleFillValue({
|
|
data: dataArray,
|
|
rawMatrix,
|
|
cpCols: cpcols,
|
|
rowToPaste: {
|
|
start: fillDirection === 1 ? startRangeTopMost : startRangeBottomMost,
|
|
end: fillDirection === 1 ? startRangeBottomMost : startRangeTopMost,
|
|
},
|
|
}).then(() => {
|
|
if (fillRange._start === null || fillRange._end === null) return
|
|
selectedRange.startRange(tempActiveCell)
|
|
selectedRange.endRange(fillRange._end)
|
|
makeActive(tempActiveCell.row, tempActiveCell.col)
|
|
fillRange.clear()
|
|
})
|
|
}
|
|
|
|
let fillIndex = fillDirection === 1 ? 0 : rawMatrix.length - 1
|
|
|
|
const rowsToPaste: Row[] = []
|
|
const rowsToFill: Row[] = []
|
|
const propsToPaste: string[] = []
|
|
const propsToFill: string[] = []
|
|
|
|
for (
|
|
let row = fillRange._start.row;
|
|
fillDirection === 1 ? row <= fillRange._end.row : row >= fillRange._end.row;
|
|
row += fillDirection
|
|
) {
|
|
const rowObj = isArrayStructure ? (unref(data) as Row[])[row] : (unref(data) as Map<number, Row>).get(row)
|
|
|
|
if (!rowObj) {
|
|
continue
|
|
}
|
|
|
|
let pasteIndex = 0
|
|
|
|
if (!selectRangeMap.value[`${row}-${selectedRange.start.col}`]) {
|
|
rowsToPaste.push(rowObj)
|
|
}
|
|
|
|
for (let col = fillRange.start.col; col <= fillRange.end.col; col++) {
|
|
const colObj = unref(fields)[col]
|
|
|
|
if (!isPasteable(rowObj, colObj)) {
|
|
pasteIndex++
|
|
continue
|
|
}
|
|
|
|
// if the column is added only for the fill operation, don't paste the value
|
|
if (
|
|
selectedRange._start &&
|
|
selectedRange._end &&
|
|
selectedRange._start.col <= col &&
|
|
col <= selectedRange._end.col
|
|
) {
|
|
if (cpcols.findIndex((c) => c.id === colObj.id) === -1) {
|
|
if (!propsToFill.includes(colObj.title!)) propsToPaste.push(colObj.title!)
|
|
}
|
|
|
|
if (!propsToPaste.includes(colObj.title!) && !propsToFill.includes(colObj.title!))
|
|
propsToPaste.push(colObj.title!)
|
|
|
|
let pasteValue
|
|
|
|
try {
|
|
pasteValue = convertCellData(
|
|
{
|
|
value: rawMatrix[fillIndex][pasteIndex],
|
|
to: colObj.uidt as UITypes,
|
|
column: colObj,
|
|
appInfo: unref(appInfo),
|
|
maxAttachmentsAllowedInCell: maxAttachmentsAllowedInCell.value,
|
|
showUpgradeToAddMoreAttachmentsInCell,
|
|
isInfoShown: isColInfoShown[colObj.title!],
|
|
markInfoShown: () => {
|
|
isColInfoShown[colObj.title!] = true
|
|
},
|
|
},
|
|
isMysql(meta.value?.source_id),
|
|
true,
|
|
)
|
|
} catch (ex) {
|
|
if (ex instanceof ComputedTypePasteError) {
|
|
throw ex
|
|
}
|
|
|
|
pasteValue = null
|
|
}
|
|
|
|
if (pasteValue !== undefined) {
|
|
if (!localAiMode) rowObj.row[colObj.title!] = pasteValue
|
|
}
|
|
} else {
|
|
if (localAiMode) {
|
|
propsToFill.push(colObj.title!)
|
|
|
|
// add rows to fill if they are not already in the list
|
|
if (
|
|
!rowsToFill.find(
|
|
(r) =>
|
|
extractPkFromRow(r.row, meta.value?.columns as ColumnType[]) ===
|
|
extractPkFromRow(rowObj.row, meta.value?.columns as ColumnType[]),
|
|
)
|
|
) {
|
|
rowsToFill.push(rowObj)
|
|
}
|
|
}
|
|
}
|
|
|
|
pasteIndex++
|
|
}
|
|
|
|
if (fillDirection === 1) {
|
|
fillIndex = fillIndex < rawMatrix.length - 1 ? fillIndex + 1 : 0
|
|
} else {
|
|
fillIndex = fillIndex >= 1 ? fillIndex - 1 : rawMatrix.length - 1
|
|
}
|
|
}
|
|
|
|
if (localAiMode) {
|
|
const sampleRows = cprows.map((row) => {
|
|
const sampleRow: Record<string, any> = {
|
|
Id: extractPkFromRow(row.row, meta.value?.columns as ColumnType[]),
|
|
}
|
|
|
|
for (const prop of propsToPaste) {
|
|
sampleRow[prop] = row.row[prop]
|
|
}
|
|
|
|
for (const prop of propsToFill) {
|
|
sampleRow[prop] = 'FILL'
|
|
}
|
|
|
|
return sampleRow
|
|
})
|
|
|
|
// string[] of Ids of rows to paste
|
|
const generateIds = rowsToPaste.map((row) => extractPkFromRow(row.row, meta.value?.columns as ColumnType[]))
|
|
|
|
fillRows(
|
|
meta.value?.id as string,
|
|
{
|
|
rows: sampleRows,
|
|
generateIds,
|
|
numRows: generateIds.length,
|
|
},
|
|
{
|
|
workspaceId: (meta.value as any)?.fk_workspace_id ?? base.value!.fk_workspace_id!,
|
|
baseId: meta.value!.base_id!,
|
|
},
|
|
).then((r?: Record<string, any>[]) => {
|
|
if (!r) return
|
|
if (fillRange._start === null || fillRange._end === null) return
|
|
// update cells with the generated data
|
|
|
|
for (const row of rowsToPaste.concat(rowsToFill)) {
|
|
const generatedRow = r.find(
|
|
(genRow) =>
|
|
extractPkFromRow(row.row, meta.value?.columns as ColumnType[]) ===
|
|
extractPkFromRow(genRow, meta.value?.columns as ColumnType[]),
|
|
)
|
|
|
|
if (!generatedRow) {
|
|
continue
|
|
}
|
|
|
|
for (const prop of propsToPaste.concat(propsToFill)) {
|
|
row.row[prop] = generatedRow[prop]
|
|
}
|
|
}
|
|
|
|
bulkUpdateRows?.(rowsToPaste.concat(rowsToFill), propsToPaste.concat(propsToFill)).then(() => {
|
|
if (fillRange._start === null || fillRange._end === null) return
|
|
selectedRange.startRange(tempActiveCell)
|
|
selectedRange.endRange(fillRange._end)
|
|
makeActive(tempActiveCell.row, tempActiveCell.col)
|
|
fillRange.clear()
|
|
})
|
|
})
|
|
return
|
|
}
|
|
|
|
bulkUpdateRows?.(rowsToPaste, propsToPaste).then(() => {
|
|
if (fillRange._start === null || fillRange._end === null) return
|
|
selectedRange.startRange(tempActiveCell)
|
|
selectedRange.endRange(fillRange._end)
|
|
makeActive(tempActiveCell.row, tempActiveCell.col)
|
|
fillRange.clear()
|
|
})
|
|
} else {
|
|
fillRange.clear()
|
|
}
|
|
return
|
|
} catch (error) {
|
|
if (error instanceof TypeConversionError !== true || !(error as SuppressedError).isErrorSuppressed) {
|
|
console.error(error, (error as SuppressedError).isErrorSuppressed)
|
|
message.error(error?.message || 'Something went wrong')
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isMouseDown.value) {
|
|
isMouseDown.value = false
|
|
// timeout is needed, because we want to set cell as active AFTER all the child's click handler's called
|
|
// this is needed e.g. for date field edit, where two clicks had to be done - one to select cell, and another one to open date dropdown
|
|
setTimeout(() => {
|
|
if (selectedRange._start) {
|
|
if (activeCell.row !== selectedRange._start.row || activeCell.col !== selectedRange._start.col) {
|
|
makeActive(selectedRange._start.row, selectedRange._start.col)
|
|
}
|
|
}
|
|
}, 0)
|
|
}
|
|
}
|
|
|
|
const handleKeyDownAction = async (e: KeyboardEvent) => {
|
|
const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey
|
|
|
|
if (activeCell.row === null || activeCell.col === null) return
|
|
switch (e.key) {
|
|
case 'Tab':
|
|
selectedRange.clear()
|
|
|
|
if (e.shiftKey) {
|
|
if (activeCell.col > 0) {
|
|
activeCell.col--
|
|
editEnabled.value = false
|
|
} else if (activeCell.row > 0) {
|
|
activeCell.row--
|
|
activeCell.col = unref(columnLength.value) - 1
|
|
editEnabled.value = false
|
|
}
|
|
} else {
|
|
if (activeCell.col < unref(columnLength.value) - 1) {
|
|
activeCell.col++
|
|
editEnabled.value = false
|
|
} else if (activeCell.row < (isArrayStructure ? (unref(data) as Row[]).length : unref(_totalRows!)) - 1) {
|
|
activeCell.row++
|
|
activeCell.col = 0
|
|
editEnabled.value = false
|
|
}
|
|
}
|
|
scrollToCell?.()
|
|
break
|
|
case 'ArrowRight':
|
|
if (e.shiftKey) {
|
|
if (cmdOrCtrl) {
|
|
editEnabled.value = false
|
|
selectedRange.endRange({
|
|
row: selectedRange._end?.row ?? activeCell.row,
|
|
col: unref(columnLength.value) - 1,
|
|
})
|
|
scrollToCell?.(selectedRange._end?.row, selectedRange._end?.col)
|
|
} else if ((selectedRange._end?.col ?? activeCell.col) < unref(columnLength.value) - 1) {
|
|
editEnabled.value = false
|
|
selectedRange.endRange({
|
|
row: selectedRange._end?.row ?? activeCell.row,
|
|
col: (selectedRange._end?.col ?? activeCell.col) + 1,
|
|
})
|
|
scrollToCell?.(selectedRange._end?.row, selectedRange._end?.col)
|
|
}
|
|
} else {
|
|
selectedRange.clear()
|
|
|
|
if (activeCell.col < unref(columnLength.value) - 1) {
|
|
activeCell.col++
|
|
selectedRange.startRange({ row: activeCell.row, col: activeCell.col })
|
|
scrollToCell?.()
|
|
editEnabled.value = false
|
|
}
|
|
}
|
|
break
|
|
case 'ArrowLeft':
|
|
if (e.shiftKey) {
|
|
if (cmdOrCtrl) {
|
|
editEnabled.value = false
|
|
selectedRange.endRange({
|
|
row: selectedRange._end?.row ?? activeCell.row,
|
|
col: 0,
|
|
})
|
|
scrollToCell?.(selectedRange._end?.row, selectedRange._end?.col)
|
|
} else if ((selectedRange._end?.col ?? activeCell.col) > 0) {
|
|
editEnabled.value = false
|
|
selectedRange.endRange({
|
|
row: selectedRange._end?.row ?? activeCell.row,
|
|
col: (selectedRange._end?.col ?? activeCell.col) - 1,
|
|
})
|
|
scrollToCell?.(selectedRange._end?.row, selectedRange._end?.col)
|
|
}
|
|
} else {
|
|
selectedRange.clear()
|
|
|
|
if (activeCell.col > 0) {
|
|
activeCell.col--
|
|
selectedRange.startRange({ row: activeCell.row, col: activeCell.col })
|
|
scrollToCell?.()
|
|
editEnabled.value = false
|
|
}
|
|
}
|
|
break
|
|
case 'ArrowUp':
|
|
if (e.shiftKey) {
|
|
const anchor = selectedRange._start ?? activeCell
|
|
let newEnd: Cell
|
|
|
|
if (cmdOrCtrl) {
|
|
newEnd = { row: 0, col: selectedRange._end?.col ?? activeCell.col }
|
|
} else {
|
|
newEnd = {
|
|
row: (selectedRange._end?.row ?? activeCell.row) - 1,
|
|
col: selectedRange._end?.col ?? activeCell.col,
|
|
}
|
|
}
|
|
|
|
const limitedEnd = limitSelection(anchor, newEnd)
|
|
editEnabled.value = false
|
|
selectedRange.endRange(limitedEnd)
|
|
scrollToCell?.(limitedEnd.row, limitedEnd.col, 'instant')
|
|
} else {
|
|
selectedRange.clear()
|
|
|
|
if (activeCell.row > 0) {
|
|
activeCell.row--
|
|
selectedRange.startRange({ row: activeCell.row, col: activeCell.col })
|
|
scrollToCell?.()
|
|
editEnabled.value = false
|
|
}
|
|
}
|
|
onActiveCellChanged?.()
|
|
break
|
|
case 'ArrowDown':
|
|
if (e.shiftKey) {
|
|
const anchor = selectedRange._start ?? activeCell
|
|
let newEnd: Cell
|
|
|
|
if (cmdOrCtrl) {
|
|
newEnd = {
|
|
row: (isArrayStructure ? (unref(data) as Row[]).length : unref(_totalRows!)) - 1,
|
|
col: selectedRange._end?.col ?? activeCell.col,
|
|
}
|
|
} else {
|
|
newEnd = {
|
|
row: (selectedRange._end?.row ?? activeCell.row) + 1,
|
|
col: selectedRange._end?.col ?? activeCell.col,
|
|
}
|
|
}
|
|
|
|
const limitedEnd = limitSelection(anchor, newEnd)
|
|
editEnabled.value = false
|
|
selectedRange.endRange(limitedEnd)
|
|
scrollToCell?.(limitedEnd.row, limitedEnd.col, 'instant')
|
|
} else {
|
|
selectedRange.clear()
|
|
|
|
if (activeCell.row < (isArrayStructure ? (unref(data) as Row[]).length : unref(_totalRows!))) {
|
|
activeCell.row++
|
|
selectedRange.startRange({ row: activeCell.row, col: activeCell.col })
|
|
scrollToCell?.()
|
|
editEnabled.value = false
|
|
}
|
|
}
|
|
onActiveCellChanged?.()
|
|
break
|
|
case 'Enter': {
|
|
selectedRange.clear()
|
|
|
|
if (removeInlineAddRecord.value && activeCell.row >= EXTERNAL_SOURCE_VISIBLE_ROWS - 1) return
|
|
|
|
let row
|
|
|
|
if (isArrayStructure) {
|
|
row = (unref(data) as Row[])[activeCell.row]
|
|
} else {
|
|
row = (unref(data) as Map<number, Row>).get(activeCell.row)
|
|
}
|
|
|
|
makeEditable(row, unref(fields)[activeCell.col])
|
|
break
|
|
}
|
|
case 'Delete':
|
|
case 'Backspace':
|
|
if (isDataReadOnly.value || (removeInlineAddRecord.value && activeCell.row >= EXTERNAL_SOURCE_VISIBLE_ROWS - 1)) {
|
|
return
|
|
}
|
|
if (selectedRange.isSingleCell()) {
|
|
selectedRange.clear()
|
|
|
|
await clearCell(activeCell as { row: number; col: number })
|
|
} else {
|
|
await clearSelectedRangeOfCells()
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
const handleThrottledKeyDownAction = useThrottleFn(handleKeyDownAction, 60)
|
|
|
|
const handleKeyDown = async (e: KeyboardEvent) => {
|
|
// Skip keyboard handling during IME composition (e.g. Japanese, Chinese, Korean input)
|
|
if (e.isComposing) return
|
|
|
|
// invoke the keyEventHandler if provided and return if it returns true
|
|
|
|
if (isArrayStructure ? await keyEventHandler?.(e) : keyEventHandler?.(e)) {
|
|
return true
|
|
}
|
|
|
|
if (isExpandedCellInputExist() || isLinkDropdownExist()) {
|
|
return
|
|
}
|
|
|
|
if (!isCellActive.value || activeCell.row === null || activeCell.col === null) {
|
|
return
|
|
}
|
|
/** on tab key press navigate through cells */
|
|
switch (e.key) {
|
|
case 'Tab':
|
|
case 'Enter':
|
|
case 'Delete':
|
|
case 'Backspace':
|
|
case 'ArrowRight':
|
|
case 'ArrowLeft':
|
|
case 'ArrowUp':
|
|
case 'ArrowDown':
|
|
e.preventDefault()
|
|
handleThrottledKeyDownAction(e)
|
|
break
|
|
default:
|
|
{
|
|
const rowObj = isArrayStructure
|
|
? (unref(data) as Row[])[activeCell.row]
|
|
: (unref(data) as Map<number, Row>).get(activeCell.row)
|
|
if (!rowObj) return
|
|
const columnObj = unref(fields)[activeCell.col]
|
|
|
|
if (
|
|
(!unref(editEnabled) || !isTypableInputColumn(columnObj)) &&
|
|
!isDrawerOrModalExist() &&
|
|
(isMac() ? e.metaKey : e.ctrlKey)
|
|
) {
|
|
switch (e.keyCode) {
|
|
// copy - ctrl/cmd +c
|
|
case 67:
|
|
await copyValue()
|
|
break
|
|
}
|
|
}
|
|
|
|
if (unref(editEnabled) || e.ctrlKey || e.altKey || e.metaKey) {
|
|
return true
|
|
}
|
|
// Disable edit cell if it is external free source and row index is more than EXTERNAL_SOURCE_VISIBLE_ROWS
|
|
if (removeInlineAddRecord.value && activeCell.row >= EXTERNAL_SOURCE_VISIBLE_ROWS - 1) {
|
|
e.preventDefault()
|
|
return
|
|
}
|
|
|
|
if (isSqlView.value) return
|
|
|
|
/** 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.toast(t('msg.info.updateNotAllowedWithoutPK'))
|
|
}
|
|
if (isTypableInputColumn(columnObj) && makeEditable(rowObj, columnObj) && columnObj.title) {
|
|
if (columnObj.uidt === UITypes.LongText) {
|
|
if (rowObj.row[columnObj.title] === '<br />' || rowObj.row[columnObj.title] === '<br>') {
|
|
rowObj.row[columnObj.title] = e.key
|
|
} else if (parseProp(columnObj.meta).richMode) {
|
|
rowObj.row[columnObj.title] = rowObj.row[columnObj.title] ? rowObj.row[columnObj.title] + e.key : e.key
|
|
}
|
|
} else {
|
|
rowObj.row[columnObj.title] = ''
|
|
}
|
|
}
|
|
// editEnabled = true
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
const resetSelectedRange = () => selectedRange.clear()
|
|
|
|
const clearSelectedRange = selectedRange.clear.bind(selectedRange)
|
|
|
|
const handlePaste = async (e: ClipboardEvent) => {
|
|
if (isDataReadOnly.value || isSqlView.value) {
|
|
return
|
|
}
|
|
|
|
if (isDrawerOrModalExist() || isExpandedCellInputExist() || isLinkDropdownExist()) {
|
|
return
|
|
}
|
|
|
|
if (!isCellActive.value) {
|
|
return
|
|
}
|
|
|
|
if (unref(editEnabled)) {
|
|
return
|
|
}
|
|
|
|
if (activeCell.row === null || activeCell.row === undefined || activeCell.col === null || activeCell.col === undefined) {
|
|
return
|
|
}
|
|
|
|
e.preventDefault()
|
|
|
|
// Replace \" with " in clipboard data
|
|
let clipboardData = e.clipboardData?.getData('text/plain') || ''
|
|
|
|
const storedCopiedData = getCurrentCopiedCellClipboardData(clipboardData)
|
|
|
|
// Add the waiting clipboard data id so that if setClipboardData fn is called during this operation, it will not remove the currentCopiedCellClipboardDataItem from the clipboard data
|
|
if (storedCopiedData && !waitingCellClipboardDataIds.value.includes(storedCopiedData.id)) {
|
|
waitingCellClipboardDataIds.value.push(storedCopiedData.id)
|
|
}
|
|
|
|
if (clipboardData?.endsWith('\n')) {
|
|
// Remove '\n' from the end of the clipboardData
|
|
// When copying from XLS/XLSX files, there is an extra '\n' appended to the end
|
|
// this overwrites one additional cell information when we paste in NocoDB
|
|
clipboardData = clipboardData.replace(/\n$/, '')
|
|
}
|
|
|
|
try {
|
|
if (clipboardData?.includes('\n') || clipboardData?.includes('\t')) {
|
|
// #region handle tabbed newline data
|
|
// if the clipboard data contains new line or tab, then it is a matrix or LongText
|
|
const parsedClipboard = parse(clipboardData, { delimiter: '\t', escapeChar: '\\' })
|
|
|
|
if (parsedClipboard.errors.length > 0) {
|
|
throw new Error(parsedClipboard.errors[0].message)
|
|
}
|
|
|
|
let clipboardMatrix = parsedClipboard.data as string[][]
|
|
|
|
// Special handling for "null" values - convert literal "null" strings to empty strings
|
|
// This ensures that empty cells from numeric fields don't appear as "null" text
|
|
clipboardMatrix = clipboardMatrix.map((row) => row.map((cell) => (cell === 'null' ? '' : cell)))
|
|
|
|
const selectionRowCount = Math.max(clipboardMatrix.length, selectedRange.end.row - selectedRange.start.row + 1)
|
|
|
|
const pasteMatrixCols = clipboardMatrix[0].length
|
|
|
|
const existingFields = unref(fields)
|
|
const startColIndex = activeCell.col
|
|
const existingColCount = existingFields.length - startColIndex
|
|
const newColsNeeded = Math.max(0, pasteMatrixCols - existingColCount)
|
|
|
|
let tempTotalRows = 0
|
|
let totalRowsBeforeActiveCell
|
|
let availableRowsToUpdate
|
|
let rowsToAdd
|
|
|
|
if (isArrayStructure) {
|
|
const { totalRows: _tempTr, page = 1, pageSize = 100 } = unref(paginationData)!
|
|
if (isInfiniteScroll) {
|
|
tempTotalRows = _tempTr as number
|
|
totalRowsBeforeActiveCell = (page - 1) * pageSize + activeCell.row
|
|
} else {
|
|
tempTotalRows = Math.max(0, (_tempTr ?? 0) - (page - 1) * pageSize)
|
|
totalRowsBeforeActiveCell = activeCell.row
|
|
}
|
|
|
|
availableRowsToUpdate = Math.max(0, tempTotalRows - totalRowsBeforeActiveCell)
|
|
rowsToAdd = Math.max(0, selectionRowCount - availableRowsToUpdate)
|
|
} else {
|
|
tempTotalRows = (unref(_totalRows) as number) ?? 0
|
|
totalRowsBeforeActiveCell = activeCell.row
|
|
availableRowsToUpdate = Math.max(0, tempTotalRows - totalRowsBeforeActiveCell)
|
|
rowsToAdd = Math.max(0, selectionRowCount - availableRowsToUpdate)
|
|
}
|
|
|
|
let options = {
|
|
continue: false,
|
|
expand: (rowsToAdd > 0 || newColsNeeded > 0) && !isArrayStructure,
|
|
}
|
|
if (options.expand && !isArrayStructure) {
|
|
options = await expandRows?.({
|
|
newRows: rowsToAdd,
|
|
newColumns: newColsNeeded,
|
|
cellsOverwritten: Math.min(availableRowsToUpdate, selectionRowCount) * (pasteMatrixCols - newColsNeeded),
|
|
rowsUpdated: Math.min(availableRowsToUpdate, selectionRowCount),
|
|
})
|
|
if (!options.continue) return
|
|
}
|
|
|
|
let colsToPaste
|
|
const bulkOpsCols = []
|
|
if (options.expand) {
|
|
// #region handle expanded tab / newline paste
|
|
// when expand option is chosen,
|
|
// separate the paste column across several cells
|
|
colsToPaste = existingFields.slice(startColIndex, startColIndex + pasteMatrixCols)
|
|
|
|
if (newColsNeeded > 0) {
|
|
const columnsHash = (
|
|
await $api.internal.getOperation(
|
|
(meta.value as any)?.fk_workspace_id ?? base.value!.fk_workspace_id!,
|
|
meta.value!.base_id!,
|
|
{
|
|
operation: 'columnsHash',
|
|
tableId: meta.value?.id,
|
|
},
|
|
)
|
|
).hash
|
|
const columnsLength = meta.value?.columns?.length || 0
|
|
|
|
for (let i = 0; i < newColsNeeded; i++) {
|
|
const tempCol = {
|
|
uidt: UITypes.SingleLineText,
|
|
order: columnsLength + i,
|
|
column_order: {
|
|
order: columnsLength + i,
|
|
view_id: activeView.value?.id,
|
|
},
|
|
view_id: activeView.value?.id,
|
|
table_name: meta.value?.table_name,
|
|
}
|
|
|
|
const newColTitle = generateUniqueColumnName({
|
|
metaColumns: [...(meta.value?.columns ?? []), ...bulkOpsCols.map(({ column }) => column)],
|
|
formState: tempCol,
|
|
})
|
|
|
|
bulkOpsCols.push({
|
|
op: 'add',
|
|
column: {
|
|
...tempCol,
|
|
title: newColTitle,
|
|
},
|
|
})
|
|
}
|
|
|
|
await $api.internal.postOperation(
|
|
(meta.value as any)?.fk_workspace_id ?? base.value!.fk_workspace_id!,
|
|
meta.value!.base_id!,
|
|
{ operation: 'columnsBulk', tableId: meta.value?.id },
|
|
{
|
|
hash: columnsHash,
|
|
ops: bulkOpsCols,
|
|
},
|
|
)
|
|
|
|
await getMeta(meta.value!.base_id!, meta?.value?.id as string, true)
|
|
|
|
colsToPaste = [...colsToPaste, ...bulkOpsCols.map(({ column }) => column)]
|
|
}
|
|
// #endregion handle expanded newline paste
|
|
} else {
|
|
colsToPaste = unref(fields).slice(activeCell.col, activeCell.col + pasteMatrixCols)
|
|
}
|
|
|
|
const dataRef = unref(data)
|
|
|
|
const updatedRows: Row[] = []
|
|
const newRows: Row[] = []
|
|
const propsToPaste: string[] = []
|
|
let isInfoShown = false
|
|
// We can use this if we want to avoid same info multiple times per column
|
|
const isColInfoShown = {} as Record<string, boolean>
|
|
|
|
for (let i = 0; i < selectionRowCount; i++) {
|
|
const clipboardRowIndex = i % clipboardMatrix.length
|
|
let targetRow: any
|
|
|
|
if (i < availableRowsToUpdate) {
|
|
const absoluteRowIndex = totalRowsBeforeActiveCell + i
|
|
if (isArrayStructure) {
|
|
targetRow =
|
|
i < (dataRef as Row[]).length
|
|
? (dataRef as Row[])[absoluteRowIndex]
|
|
: {
|
|
row: {},
|
|
oldRow: {},
|
|
rowMeta: {
|
|
isExistingRow: true,
|
|
rowIndex: absoluteRowIndex,
|
|
},
|
|
}
|
|
} else {
|
|
targetRow = (dataRef as Map<number, Row>).get(absoluteRowIndex) || {
|
|
row: {},
|
|
oldRow: {},
|
|
rowMeta: {
|
|
isExistingRow: true,
|
|
rowIndex: absoluteRowIndex,
|
|
},
|
|
}
|
|
}
|
|
updatedRows.push(targetRow)
|
|
} else {
|
|
targetRow = {
|
|
row: {},
|
|
oldRow: {},
|
|
rowMeta: {
|
|
isExistingRow: false,
|
|
},
|
|
}
|
|
newRows.push(targetRow)
|
|
}
|
|
|
|
for (let j = 0; j < clipboardMatrix[clipboardRowIndex].length; j++) {
|
|
const column = colsToPaste[j]
|
|
if (!column) continue
|
|
if (column && isPasteable(targetRow, column)) {
|
|
propsToPaste.push(column.title!)
|
|
let pasteValue: any
|
|
try {
|
|
pasteValue = convertCellData(
|
|
{
|
|
value: clipboardMatrix[clipboardRowIndex][j],
|
|
to: column.uidt as UITypes,
|
|
column,
|
|
appInfo: unref(appInfo),
|
|
oldValue: column.uidt === UITypes.Attachment ? targetRow.row[column.title!] : undefined,
|
|
maxAttachmentsAllowedInCell: maxAttachmentsAllowedInCell.value,
|
|
showUpgradeToAddMoreAttachmentsInCell,
|
|
isInfoShown: isColInfoShown[column.title!],
|
|
markInfoShown: () => {
|
|
isColInfoShown[column.title!] = true
|
|
},
|
|
clipboardItem: extractCellClipboardData(storedCopiedData, clipboardRowIndex, j),
|
|
},
|
|
isMysql(meta.value?.source_id),
|
|
true,
|
|
)
|
|
validateColumnValue(column, pasteValue)
|
|
} catch (ex) {
|
|
if (ex instanceof ComputedTypePasteError) {
|
|
throw ex
|
|
} else if (ex instanceof SelectTypeConversionError) {
|
|
await appendSelectOptions({
|
|
api: $api,
|
|
col: column!,
|
|
addOptions: ex.missingOptions,
|
|
})
|
|
pasteValue = ex.value.join(',')
|
|
} else if (ex instanceof TypeConversionError) {
|
|
pasteValue = null
|
|
} else throw ex
|
|
}
|
|
|
|
if (pasteValue !== undefined) {
|
|
targetRow.row[column.title!] = pasteValue
|
|
}
|
|
} else if ((isBt(column) || isOo(column) || isMMOrMMLike(column)) && !isInfoShown) {
|
|
message.toast(t('msg.info.groupPasteIsNotSupportedOnLinksColumn'))
|
|
isInfoShown = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if (options.expand && !isArrayStructure) {
|
|
await bulkUpsertRows?.(
|
|
newRows,
|
|
updatedRows,
|
|
propsToPaste,
|
|
undefined,
|
|
bulkOpsCols.map(({ column }) => column),
|
|
)
|
|
scrollToCell?.()
|
|
} else {
|
|
await bulkUpdateRows?.(updatedRows, propsToPaste)
|
|
}
|
|
// #endregion handle tabbed newline data
|
|
} else {
|
|
if (selectedRange.isSingleCell()) {
|
|
// #region handle single cell paste
|
|
const rowObj = isArrayStructure
|
|
? (unref(data) as Row[])[activeCell.row]
|
|
: (unref(data) as Map<number, Row>).get(activeCell.row)
|
|
if (!rowObj) return
|
|
const columnObj = unref(fields)[activeCell.col]
|
|
|
|
// handle belongs to column, skip custom links
|
|
if (isBt(columnObj) && !columnObj.meta?.custom) {
|
|
const pasteVal = convertCellData(
|
|
{
|
|
value: clipboardData,
|
|
to: columnObj.uidt as UITypes,
|
|
column: columnObj,
|
|
appInfo: unref(appInfo),
|
|
clipboardItem: extractCellClipboardData(storedCopiedData, 0, 0),
|
|
},
|
|
isMysql(meta.value?.source_id),
|
|
)
|
|
|
|
if (pasteVal === undefined || !ncIsObject(pasteVal)) return
|
|
|
|
const foreignKeyColumn = meta.value?.columns?.find(
|
|
(column: ColumnType) => column.id === (columnObj.colOptions as LinkToAnotherRecordType)?.fk_child_column_id,
|
|
)
|
|
|
|
if (!foreignKeyColumn) return
|
|
|
|
const colOpts = columnObj.colOptions as LinkToAnotherRecordType
|
|
const relatedBaseId = colOpts.fk_related_base_id || (base.value?.id as string)
|
|
const relatedTableMeta = await getMeta(relatedBaseId, colOpts.fk_related_model_id!)
|
|
|
|
// update old row to allow undo redo as bt column update only through foreignKeyColumn title
|
|
rowObj.oldRow[columnObj.title!] = rowObj.row[columnObj.title!]
|
|
rowObj.oldRow[foreignKeyColumn.title!] = rowObj.row[columnObj.title!]
|
|
? extractPkFromRow(rowObj.row[columnObj.title!], (relatedTableMeta as any)!.columns!)
|
|
: null
|
|
|
|
rowObj.row[columnObj.title!] = pasteVal?.value
|
|
|
|
rowObj.row[foreignKeyColumn.title!] = pasteVal?.value
|
|
? extractPkFromRow(pasteVal.value, (relatedTableMeta as any)!.columns!)
|
|
: null
|
|
|
|
return await syncCellData?.({ ...activeCell, updatedColumnTitle: foreignKeyColumn.title })
|
|
}
|
|
|
|
if (isMMOrMMLike(columnObj)) {
|
|
const pasteVal = convertCellData(
|
|
{
|
|
value: clipboardData,
|
|
to: columnObj.uidt as UITypes,
|
|
column: columnObj,
|
|
appInfo: unref(appInfo),
|
|
clipboardItem: extractCellClipboardData(storedCopiedData, 0, 0),
|
|
},
|
|
isMysql(meta.value?.source_id),
|
|
)
|
|
|
|
if (pasteVal === undefined || !ncIsObject(pasteVal)) return
|
|
|
|
const pasteRowPk = extractPkFromRow(rowObj.row, meta.value?.columns as ColumnType[])
|
|
if (!pasteRowPk) return
|
|
|
|
const oldCellValue = rowObj.row[columnObj.title!]
|
|
|
|
rowObj.row[columnObj.title!] = pasteVal.value
|
|
|
|
let result
|
|
|
|
try {
|
|
result = await api.internal.postOperation(
|
|
meta.value?.fk_workspace_id ?? base.value.fk_workspace_id,
|
|
meta.value?.base_id ?? base.value.id,
|
|
{
|
|
operation: 'nestedDataListCopyPasteOrDeleteAll',
|
|
tableId: meta.value?.id as string,
|
|
columnId: columnObj.id as string,
|
|
viewId: activeView?.value?.id,
|
|
},
|
|
[
|
|
{
|
|
operation: 'copy',
|
|
rowId: pasteVal.rowId,
|
|
columnId: pasteVal.columnId,
|
|
fk_related_model_id: pasteVal.fk_related_model_id,
|
|
},
|
|
{
|
|
operation: 'paste',
|
|
rowId: pasteRowPk,
|
|
columnId: columnObj.id as string,
|
|
fk_related_model_id:
|
|
(columnObj.colOptions as LinkToAnotherRecordType).fk_related_model_id || pasteVal.fk_related_model_id,
|
|
},
|
|
],
|
|
)
|
|
} catch {
|
|
rowObj.row[columnObj.title!] = oldCellValue
|
|
return
|
|
}
|
|
|
|
if (result && result?.link && result?.unlink && Array.isArray(result.link) && Array.isArray(result.unlink)) {
|
|
if (!result.link.length && !result.unlink.length) {
|
|
rowObj.row[columnObj.title!] = oldCellValue
|
|
return
|
|
}
|
|
|
|
if (isArrayStructure) {
|
|
addUndo({
|
|
redo: {
|
|
fn: async (
|
|
activeCell: Cell,
|
|
col: ColumnType,
|
|
row: Row,
|
|
pg: PaginatedType,
|
|
value: number,
|
|
result: { link: any[]; unlink: any[] },
|
|
) => {
|
|
if (paginationDataRef.value?.pageSize === pg?.pageSize) {
|
|
if (paginationDataRef.value?.page !== pg?.page) {
|
|
await changePage?.(pg?.page)
|
|
}
|
|
const pasteRowPk = extractPkFromRow(row.row, meta.value?.columns as ColumnType[])
|
|
const rowObj = (unref(data) as Row[])[activeCell.row]
|
|
const columnObj = unref(fields)[activeCell.col]
|
|
if (
|
|
pasteRowPk === extractPkFromRow(rowObj.row, meta.value?.columns as ColumnType[]) &&
|
|
columnObj.id === col.id
|
|
) {
|
|
await Promise.all([
|
|
result.link.length &&
|
|
api.internal.postOperation(
|
|
meta.value?.fk_workspace_id ?? base.value.fk_workspace_id,
|
|
meta.value?.base_id ?? base.value.id,
|
|
{
|
|
operation: 'nestedDataLink',
|
|
tableId: meta.value?.id as string,
|
|
columnId: columnObj.id as string,
|
|
rowId: encodeURIComponent(pasteRowPk),
|
|
viewId: activeView?.value?.id,
|
|
},
|
|
result.link,
|
|
),
|
|
result.unlink.length &&
|
|
api.internal.postOperation(
|
|
meta.value?.fk_workspace_id ?? base.value.fk_workspace_id,
|
|
meta.value?.base_id ?? base.value.id,
|
|
{
|
|
operation: 'nestedDataUnlink',
|
|
tableId: meta.value?.id as string,
|
|
columnId: columnObj.id as string,
|
|
rowId: encodeURIComponent(pasteRowPk),
|
|
viewId: activeView?.value?.id,
|
|
},
|
|
result.unlink,
|
|
),
|
|
])
|
|
|
|
rowObj.row[columnObj.title!] = value
|
|
|
|
await syncCellData?.(activeCell)
|
|
} else {
|
|
throw new Error(t('msg.recordCouldNotBeFound'))
|
|
}
|
|
} else {
|
|
throw new Error(t('msg.pageSizeChanged'))
|
|
}
|
|
},
|
|
args: [
|
|
clone(activeCell),
|
|
clone(columnObj),
|
|
clone(rowObj),
|
|
clone(paginationDataRef.value),
|
|
clone(pasteVal.value),
|
|
result,
|
|
],
|
|
},
|
|
undo: {
|
|
fn: async (
|
|
activeCell: Cell,
|
|
col: ColumnType,
|
|
row: Row,
|
|
pg: PaginatedType,
|
|
value: number,
|
|
result: { link: any[]; unlink: any[] },
|
|
) => {
|
|
if (paginationDataRef.value?.pageSize === pg.pageSize) {
|
|
if (paginationDataRef.value?.page !== pg.page) {
|
|
await changePage?.(pg.page!)
|
|
}
|
|
|
|
const pasteRowPk = extractPkFromRow(row.row, meta.value?.columns as ColumnType[])
|
|
const rowObj = (unref(data) as Row[])[activeCell.row]
|
|
const columnObj = unref(fields)[activeCell.col]
|
|
|
|
if (
|
|
pasteRowPk === extractPkFromRow(rowObj.row, meta.value?.columns as ColumnType[]) &&
|
|
columnObj.id === col.id
|
|
) {
|
|
await Promise.all([
|
|
result.unlink.length &&
|
|
api.internal.postOperation(
|
|
meta.value?.fk_workspace_id ?? base.value.fk_workspace_id,
|
|
meta.value?.base_id ?? base.value.id,
|
|
{
|
|
operation: 'nestedDataLink',
|
|
tableId: meta.value?.id as string,
|
|
columnId: columnObj.id as string,
|
|
rowId: encodeURIComponent(pasteRowPk),
|
|
},
|
|
result.unlink,
|
|
),
|
|
result.link.length &&
|
|
api.internal.postOperation(
|
|
meta.value?.fk_workspace_id ?? base.value.fk_workspace_id,
|
|
meta.value?.base_id ?? base.value.id,
|
|
{
|
|
operation: 'nestedDataUnlink',
|
|
tableId: meta.value?.id as string,
|
|
columnId: columnObj.id as string,
|
|
rowId: encodeURIComponent(pasteRowPk),
|
|
},
|
|
result.link,
|
|
),
|
|
])
|
|
|
|
rowObj.row[columnObj.title!] = value
|
|
|
|
await syncCellData?.(activeCell)
|
|
} else {
|
|
throw new Error(t('msg.recordCouldNotBeFound'))
|
|
}
|
|
} else {
|
|
throw new Error(t('msg.pageSizeChanged'))
|
|
}
|
|
},
|
|
args: [
|
|
clone(activeCell),
|
|
clone(columnObj),
|
|
clone(rowObj),
|
|
clone(paginationDataRef.value),
|
|
clone(oldCellValue),
|
|
result,
|
|
],
|
|
},
|
|
scope: defineViewScope({ view: activeView?.value }),
|
|
})
|
|
} else {
|
|
addUndo({
|
|
redo: {
|
|
fn: async (
|
|
activeCell: Cell,
|
|
col: ColumnType,
|
|
row: Row,
|
|
value: number,
|
|
result: { link: any[]; unlink: any[] },
|
|
) => {
|
|
const pasteRowPk = extractPkFromRow(row.row, meta.value?.columns as ColumnType[])
|
|
const rowObj = (unref(data) as Map<number, Row>).get(activeCell.row)
|
|
if (!rowObj) return
|
|
const columnObj = unref(fields)[activeCell.col]
|
|
if (
|
|
pasteRowPk === extractPkFromRow(rowObj.row, meta.value?.columns as ColumnType[]) &&
|
|
columnObj.id === col.id
|
|
) {
|
|
await Promise.all([
|
|
result.link.length &&
|
|
api.internal.postOperation(
|
|
meta.value?.fk_workspace_id ?? base.value.fk_workspace_id,
|
|
meta.value?.base_id ?? base.value.id,
|
|
{
|
|
operation: 'nestedDataLink',
|
|
tableId: meta.value?.id as string,
|
|
columnId: columnObj.id as string,
|
|
rowId: encodeURIComponent(pasteRowPk),
|
|
viewId: activeView?.value?.id,
|
|
},
|
|
result.link,
|
|
),
|
|
result.unlink.length &&
|
|
api.internal.postOperation(
|
|
meta.value?.fk_workspace_id ?? base.value.fk_workspace_id,
|
|
meta.value?.base_id ?? base.value.id,
|
|
{
|
|
operation: 'nestedDataUnlink',
|
|
tableId: meta.value?.id as string,
|
|
columnId: columnObj.id as string,
|
|
rowId: encodeURIComponent(pasteRowPk),
|
|
viewId: activeView?.value?.id,
|
|
},
|
|
result.unlink,
|
|
),
|
|
])
|
|
|
|
rowObj.row[columnObj.title!] = value
|
|
|
|
await syncCellData?.(activeCell)
|
|
}
|
|
},
|
|
args: [clone(activeCell), clone(columnObj), clone(rowObj), clone(pasteVal.value), result],
|
|
},
|
|
undo: {
|
|
fn: async (
|
|
activeCell: Cell,
|
|
col: ColumnType,
|
|
row: Row,
|
|
value: number,
|
|
result: { link: any[]; unlink: any[] },
|
|
) => {
|
|
const pasteRowPk = extractPkFromRow(row.row, meta.value?.columns as ColumnType[])
|
|
const rowObj = (unref(data) as Map<number, Row>).get(activeCell.row)
|
|
if (!rowObj) return
|
|
const columnObj = unref(fields)[activeCell.col]
|
|
|
|
if (
|
|
pasteRowPk === extractPkFromRow(rowObj.row, meta.value?.columns as ColumnType[]) &&
|
|
columnObj.id === col.id
|
|
) {
|
|
await Promise.all([
|
|
result.unlink.length &&
|
|
api.internal.postOperation(
|
|
meta.value?.fk_workspace_id ?? base.value.fk_workspace_id,
|
|
meta.value?.base_id ?? base.value.id,
|
|
{
|
|
operation: 'nestedDataLink',
|
|
tableId: meta.value?.id as string,
|
|
columnId: columnObj.id as string,
|
|
rowId: encodeURIComponent(pasteRowPk),
|
|
},
|
|
result.unlink,
|
|
),
|
|
result.link.length &&
|
|
api.internal.postOperation(
|
|
meta.value?.fk_workspace_id ?? base.value.fk_workspace_id,
|
|
meta.value?.base_id ?? base.value.id,
|
|
{
|
|
operation: 'nestedDataUnlink',
|
|
tableId: meta.value?.id as string,
|
|
columnId: columnObj.id as string,
|
|
rowId: encodeURIComponent(pasteRowPk),
|
|
},
|
|
result.link,
|
|
),
|
|
])
|
|
|
|
rowObj.row[columnObj.title!] = value
|
|
|
|
await syncCellData?.(activeCell)
|
|
}
|
|
},
|
|
args: [clone(activeCell), clone(columnObj), clone(rowObj), clone(oldCellValue), result],
|
|
},
|
|
scope: defineViewScope({ view: activeView?.value }),
|
|
})
|
|
}
|
|
}
|
|
|
|
return await syncCellData?.(activeCell)
|
|
}
|
|
|
|
if (!isPasteable(rowObj, columnObj, true)) {
|
|
return
|
|
}
|
|
|
|
let pasteValue: any
|
|
try {
|
|
pasteValue = convertCellData(
|
|
{
|
|
value: clipboardData,
|
|
to: columnObj.uidt as UITypes,
|
|
column: columnObj,
|
|
appInfo: unref(appInfo),
|
|
files:
|
|
columnObj.uidt === UITypes.Attachment && e.clipboardData?.files?.length ? e.clipboardData?.files : undefined,
|
|
oldValue: rowObj.row[columnObj.title!],
|
|
maxAttachmentsAllowedInCell: maxAttachmentsAllowedInCell.value,
|
|
showUpgradeToAddMoreAttachmentsInCell,
|
|
clipboardItem: extractCellClipboardData(storedCopiedData, 0, 0),
|
|
},
|
|
isMysql(meta.value?.source_id),
|
|
)
|
|
validateColumnValue(columnObj, pasteValue)
|
|
} catch (ex) {
|
|
if (ex instanceof ComputedTypePasteError) {
|
|
throw ex
|
|
} else if (ex instanceof SelectTypeConversionError) {
|
|
await appendSelectOptions({
|
|
api: $api,
|
|
col: columnObj!,
|
|
addOptions: ex.missingOptions,
|
|
})
|
|
pasteValue = ex.value.join(',')
|
|
} else if (ex instanceof TypeConversionError) {
|
|
pasteValue = null
|
|
} else throw ex
|
|
}
|
|
if (columnObj.uidt === UITypes.Attachment && e.clipboardData?.files?.length && pasteValue?.length) {
|
|
const newAttachments =
|
|
(await handleFileUploadAndGetCellValue(pasteValue, columnObj.id!, rowObj.row[columnObj.title!])) || []
|
|
|
|
const oldAttachments = ncIsArray(rowObj.row[columnObj.title!]) ? rowObj.row[columnObj.title!] : []
|
|
|
|
rowObj.row[columnObj.title!] =
|
|
newAttachments.length || oldAttachments.length ? JSON.stringify(oldAttachments.concat(newAttachments)) : null
|
|
} else if (pasteValue !== undefined) {
|
|
rowObj.row[columnObj.title!] = pasteValue
|
|
}
|
|
|
|
await syncCellData?.(activeCell)
|
|
// #endregion handle single cell paste
|
|
} else {
|
|
// #region handle multi cell paste
|
|
const start = selectedRange.start
|
|
const end = selectedRange.end
|
|
|
|
const startRow = Math.min(start.row, end.row)
|
|
const endRow = Math.max(start.row, end.row)
|
|
const startCol = Math.min(start.col, end.col)
|
|
const endCol = Math.max(start.col, end.col)
|
|
|
|
const cols = unref(fields).slice(startCol, endCol + 1)
|
|
let rows
|
|
|
|
if (isArrayStructure) {
|
|
rows = (unref(data) as Row[]).slice(startRow, endRow + 1)
|
|
} else {
|
|
rows = Array.from(unref(data) as Map<number, Row>)
|
|
.filter(([index]) => index >= startRow && index <= endRow)
|
|
.map(([, row]) => row)
|
|
}
|
|
const props = []
|
|
|
|
let pasteValue
|
|
let isInfoShown = false
|
|
// We can use this if we want to avoid same info multiple times per column
|
|
const isColInfoShown = {} as Record<string, boolean>
|
|
|
|
const files = e.clipboardData?.files
|
|
|
|
for (const row of rows) {
|
|
// TODO handle insert new row
|
|
if (!row || row.rowMeta.new) continue
|
|
|
|
for (const col of cols) {
|
|
if (!col.title || !isPasteable(row, col)) {
|
|
if ((isBt(col) || isOo(col) || isMMOrMMLike(col)) && !isInfoShown) {
|
|
message.toast(t('msg.info.groupPasteIsNotSupportedOnLinksColumn'))
|
|
isInfoShown = true
|
|
}
|
|
continue
|
|
}
|
|
|
|
if (files?.length) {
|
|
if (col.uidt !== UITypes.Attachment) {
|
|
continue
|
|
}
|
|
|
|
if (pasteValue === undefined) {
|
|
const fileUploadPayload = convertCellData(
|
|
{
|
|
value: '',
|
|
to: col.uidt as UITypes,
|
|
column: col,
|
|
appInfo: unref(appInfo),
|
|
files,
|
|
oldValue: row.row[col.title],
|
|
maxAttachmentsAllowedInCell: maxAttachmentsAllowedInCell.value,
|
|
showUpgradeToAddMoreAttachmentsInCell,
|
|
isInfoShown: isColInfoShown[col.title!],
|
|
markInfoShown: () => {
|
|
isColInfoShown[col.title!] = true
|
|
},
|
|
clipboardItem: extractCellClipboardData(storedCopiedData, 0, 0),
|
|
},
|
|
isMysql(meta.value?.source_id),
|
|
true,
|
|
)
|
|
|
|
if (fileUploadPayload?.length) {
|
|
const newAttachments = await handleFileUploadAndGetCellValue(fileUploadPayload, col.id!, row.row[col.title!])
|
|
|
|
pasteValue = newAttachments ? JSON.stringify(newAttachments) : null
|
|
}
|
|
}
|
|
} else {
|
|
try {
|
|
pasteValue = convertCellData(
|
|
{
|
|
value: clipboardData,
|
|
to: col.uidt as UITypes,
|
|
column: col,
|
|
appInfo: unref(appInfo),
|
|
oldValue: row.row[col.title],
|
|
maxAttachmentsAllowedInCell: maxAttachmentsAllowedInCell.value,
|
|
showUpgradeToAddMoreAttachmentsInCell,
|
|
isInfoShown: isColInfoShown[col.title!],
|
|
markInfoShown: () => {
|
|
isColInfoShown[col.title!] = true
|
|
},
|
|
clipboardItem: extractCellClipboardData(storedCopiedData, 0, 0),
|
|
},
|
|
isMysql(meta.value?.source_id),
|
|
true,
|
|
)
|
|
validateColumnValue(col, pasteValue)
|
|
} catch (ex) {
|
|
if (ex instanceof ComputedTypePasteError) {
|
|
throw ex
|
|
} else if (ex instanceof SelectTypeConversionError) {
|
|
await appendSelectOptions({
|
|
api: $api,
|
|
col,
|
|
addOptions: ex.missingOptions,
|
|
})
|
|
pasteValue = ex.value.join(',')
|
|
} else if (ex instanceof TypeConversionError) {
|
|
pasteValue = null
|
|
} else throw ex
|
|
}
|
|
}
|
|
|
|
props.push(col.title)
|
|
|
|
if (pasteValue !== undefined) {
|
|
row.row[col.title] = pasteValue
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!props.length) return
|
|
await bulkUpdateRows?.(rows, props)
|
|
// #endregion handle multi cell paste
|
|
}
|
|
}
|
|
} catch (error: any) {
|
|
if (error instanceof TypeConversionError !== true || !(error as SuppressedError).isErrorSuppressed) {
|
|
console.error(error, (error as SuppressedError).isErrorSuppressed)
|
|
message.error(await extractSdkResponseErrorMsg(error))
|
|
}
|
|
} finally {
|
|
// After paste operation is completed, remove the waiting clipboard data id so that on setClipboardDateItem can remove the item from the clipboard data
|
|
if (storedCopiedData && waitingCellClipboardDataIds.value.includes(storedCopiedData.id)) {
|
|
waitingCellClipboardDataIds.value = waitingCellClipboardDataIds.value.filter((id) => id !== storedCopiedData.id)
|
|
}
|
|
}
|
|
}
|
|
|
|
function fillHandleMouseDown(event: MouseEvent) {
|
|
if (event?.button !== MAIN_MOUSE_PRESSED) {
|
|
return
|
|
}
|
|
|
|
isFillMode.value = true
|
|
if (metaKey?.value && isFeatureEnabled(FEATURE_FLAG.AI_FILL_HANDLE)) {
|
|
aiMode.value = true
|
|
}
|
|
|
|
if (selectedRange._start && selectedRange._end) {
|
|
fillRange.startRange({ row: selectedRange._start?.row, col: selectedRange._start.col })
|
|
fillRange.endRange({ row: selectedRange._end?.row, col: selectedRange._end.col })
|
|
}
|
|
|
|
event.preventDefault()
|
|
}
|
|
|
|
async function handleFileUploadAndGetCellValue(files: File[], columnId: string, oldValue: AttachmentType[]) {
|
|
const newAttachments: AttachmentType[] = []
|
|
|
|
try {
|
|
const data = await batchUploadFiles(files, [NOCO, base.value.id, meta.value?.id, columnId].join('/'))
|
|
|
|
// add suffix in duplicate file title
|
|
for (const uploadedFile of data) {
|
|
newAttachments.push({
|
|
...uploadedFile,
|
|
title: populateUniqueFileName(
|
|
uploadedFile?.title,
|
|
[...handleParseAttachmentCellData(oldValue), ...newAttachments].map((fn) => fn?.title || fn?.fileName),
|
|
uploadedFile?.mimetype,
|
|
),
|
|
})
|
|
}
|
|
return newAttachments
|
|
} catch (e: any) {
|
|
message.error((await extractSdkResponseErrorMsg(e)) || t('msg.error.internalError'))
|
|
}
|
|
}
|
|
|
|
function handleParseAttachmentCellData<T>(value: T): T {
|
|
const parsedVal = parseProp(value)
|
|
|
|
if (parsedVal && Array.isArray(parsedVal)) {
|
|
return parsedVal as T
|
|
} else {
|
|
return [] as T
|
|
}
|
|
}
|
|
|
|
useEventListener(document, 'keydown', handleKeyDown)
|
|
useEventListener(document, 'mouseup', handleMouseUp)
|
|
useEventListener(document, 'paste', handlePaste)
|
|
|
|
useEventListener(fillHandle, 'mousedown', fillHandleMouseDown)
|
|
|
|
return {
|
|
isCellActive,
|
|
handleMouseDown,
|
|
handleMouseOver,
|
|
clearSelectedRange,
|
|
copyValue,
|
|
activeCell,
|
|
handleCellClick,
|
|
resetSelectedRange,
|
|
selectedRange,
|
|
makeActive,
|
|
isMouseDown,
|
|
isFillMode,
|
|
selectRangeMap,
|
|
fillRangeMap,
|
|
metaKey,
|
|
}
|
|
}
|