|
|
|
@ -2,8 +2,8 @@ import type { Ref } from 'vue'
|
|
|
|
|
import { computed } from 'vue' |
|
|
|
|
import dayjs from 'dayjs' |
|
|
|
|
import type { MaybeRef } from '@vueuse/core' |
|
|
|
|
import type { ColumnType, LinkToAnotherRecordType, TableType, UserFieldRecordType } from 'nocodb-sdk' |
|
|
|
|
import { RelationTypes, UITypes, dateFormats, isDateMonthFormat, isSystemColumn, isVirtualCol, timeFormats } from 'nocodb-sdk' |
|
|
|
|
import type { ColumnType, LinkToAnotherRecordType, PaginatedType, TableType, UserFieldRecordType, ViewType } from 'nocodb-sdk' |
|
|
|
|
import { UITypes, dateFormats, isDateMonthFormat, isSystemColumn, isVirtualCol, timeFormats } from 'nocodb-sdk' |
|
|
|
|
import { parse } from 'papaparse' |
|
|
|
|
import type { Cell } from './cellRange' |
|
|
|
|
import { CellRange } from './cellRange' |
|
|
|
@ -12,11 +12,14 @@ import type { Nullable, Row } from '#imports'
|
|
|
|
|
import { |
|
|
|
|
extractPkFromRow, |
|
|
|
|
extractSdkResponseErrorMsg, |
|
|
|
|
isBt, |
|
|
|
|
isDrawerOrModalExist, |
|
|
|
|
isExpandedCellInputExist, |
|
|
|
|
isMac, |
|
|
|
|
isMm, |
|
|
|
|
isTypableInputColumn, |
|
|
|
|
message, |
|
|
|
|
parseProp, |
|
|
|
|
reactive, |
|
|
|
|
ref, |
|
|
|
|
unref, |
|
|
|
@ -27,6 +30,7 @@ import {
|
|
|
|
|
useGlobal, |
|
|
|
|
useI18n, |
|
|
|
|
useMetas, |
|
|
|
|
useUndoRedo, |
|
|
|
|
} from '#imports' |
|
|
|
|
|
|
|
|
|
const MAIN_MOUSE_PRESSED = 0 |
|
|
|
@ -49,6 +53,9 @@ export function useMultiSelect(
|
|
|
|
|
syncCellData?: Function, |
|
|
|
|
bulkUpdateRows?: Function, |
|
|
|
|
fillHandle?: MaybeRef<HTMLElement | undefined>, |
|
|
|
|
view?: MaybeRef<ViewType | undefined>, |
|
|
|
|
paginationData?: MaybeRef<PaginatedType | undefined>, |
|
|
|
|
changePage?: (page: number) => void, |
|
|
|
|
) { |
|
|
|
|
const meta = ref(_meta) |
|
|
|
|
|
|
|
|
@ -66,12 +73,18 @@ export function useMultiSelect(
|
|
|
|
|
|
|
|
|
|
const { api } = useApi() |
|
|
|
|
|
|
|
|
|
const { addUndo, clone, defineViewScope } = useUndoRedo() |
|
|
|
|
|
|
|
|
|
const editEnabled = ref(_editEnabled) |
|
|
|
|
|
|
|
|
|
const isMouseDown = ref(false) |
|
|
|
|
|
|
|
|
|
const isFillMode = ref(false) |
|
|
|
|
|
|
|
|
|
const activeView = ref(view) |
|
|
|
|
|
|
|
|
|
const paginationDataRef = ref(paginationData) |
|
|
|
|
|
|
|
|
|
const selectedRange = reactive(new CellRange()) |
|
|
|
|
|
|
|
|
|
const fillRange = reactive(new CellRange()) |
|
|
|
@ -129,6 +142,23 @@ export function useMultiSelect(
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (isBt(columnObj)) { |
|
|
|
|
// fk_related_model_id is used to prevent paste operation in different fk_related_model_id cell
|
|
|
|
|
textToCopy = { |
|
|
|
|
fk_related_model_id: (columnObj.colOptions as LinkToAnotherRecordType).fk_related_model_id, |
|
|
|
|
value: textToCopy || null, |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (isMm(columnObj)) { |
|
|
|
|
textToCopy = { |
|
|
|
|
rowId: extractPkFromRow(rowObj.row, meta.value?.columns as ColumnType[]), |
|
|
|
|
columnId: columnObj.id, |
|
|
|
|
fk_related_model_id: (columnObj.colOptions as LinkToAnotherRecordType).fk_related_model_id, |
|
|
|
|
value: !isNaN(+textToCopy) ? +textToCopy : 0, |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (typeof textToCopy === 'object') { |
|
|
|
|
textToCopy = JSON.stringify(textToCopy) |
|
|
|
|
} else { |
|
|
|
@ -143,7 +173,7 @@ export function useMultiSelect(
|
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if ([UITypes.DateTime, UITypes.CreatedTime, UITypes.LastModifiedTime].includes(columnObj.uidt)) { |
|
|
|
|
if ([UITypes.DateTime, UITypes.CreatedTime, UITypes.LastModifiedTime].includes(columnObj.uidt as UITypes)) { |
|
|
|
|
// remove `"`
|
|
|
|
|
// e.g. "2023-05-12T08:03:53.000Z" -> 2023-05-12T08:03:53.000Z
|
|
|
|
|
textToCopy = textToCopy.replace(/["']/g, '') |
|
|
|
@ -164,14 +194,15 @@ export function useMultiSelect(
|
|
|
|
|
// therefore, here we reformat to the correct datetime format based on the meta
|
|
|
|
|
textToCopy = d.format(constructDateTimeFormat(columnObj)) |
|
|
|
|
|
|
|
|
|
if (!dayjs(textToCopy).isValid()) { |
|
|
|
|
if (!d.isValid()) { |
|
|
|
|
// return empty string for invalid datetime
|
|
|
|
|
return '' |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (columnObj.uidt === UITypes.Date) { |
|
|
|
|
const dateFormat = columnObj.meta?.date_format |
|
|
|
|
const dateFormat = parseProp(columnObj.meta)?.date_format |
|
|
|
|
|
|
|
|
|
if (dateFormat && isDateMonthFormat(dateFormat)) { |
|
|
|
|
// any date month format (e.g. YYYY-MM) couldn't be stored in database
|
|
|
|
|
// with date type since it is not a valid date
|
|
|
|
@ -798,6 +829,7 @@ export function useMultiSelect(
|
|
|
|
|
const propsToPaste: string[] = [] |
|
|
|
|
|
|
|
|
|
let pastedRows = 0 |
|
|
|
|
let isInfoShown = false |
|
|
|
|
|
|
|
|
|
for (let i = 0; i < pasteMatrixRows; i++) { |
|
|
|
|
const pasteRow = rowsToPaste[i] |
|
|
|
@ -811,6 +843,10 @@ export function useMultiSelect(
|
|
|
|
|
const pasteCol = colsToPaste[j] |
|
|
|
|
|
|
|
|
|
if (!isPasteable(pasteRow, pasteCol)) { |
|
|
|
|
if ((isBt(pasteCol) || isMm(pasteCol)) && !isInfoShown) { |
|
|
|
|
message.info(t('msg.info.groupPasteIsNotSupportedOnLinksColumn')) |
|
|
|
|
isInfoShown = true |
|
|
|
|
} |
|
|
|
|
continue |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -847,15 +883,10 @@ export function useMultiSelect(
|
|
|
|
|
const columnObj = unref(fields)[activeCell.col] |
|
|
|
|
|
|
|
|
|
// handle belongs to column
|
|
|
|
|
if ( |
|
|
|
|
columnObj.uidt === UITypes.LinkToAnotherRecord && |
|
|
|
|
(columnObj.colOptions as LinkToAnotherRecordType)?.type === RelationTypes.BELONGS_TO |
|
|
|
|
) { |
|
|
|
|
const clipboardContext = JSON.parse(clipboardData!) |
|
|
|
|
|
|
|
|
|
rowObj.row[columnObj.title!] = convertCellData( |
|
|
|
|
if (isBt(columnObj)) { |
|
|
|
|
const pasteVal = convertCellData( |
|
|
|
|
{ |
|
|
|
|
value: clipboardContext, |
|
|
|
|
value: clipboardData, |
|
|
|
|
to: columnObj.uidt as UITypes, |
|
|
|
|
column: columnObj, |
|
|
|
|
appInfo: unref(appInfo), |
|
|
|
@ -863,19 +894,210 @@ export function useMultiSelect(
|
|
|
|
|
isMysql(meta.value?.source_id), |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
if (pasteVal === undefined) return |
|
|
|
|
|
|
|
|
|
const foreignKeyColumn = meta.value?.columns?.find( |
|
|
|
|
(column: ColumnType) => column.id === (columnObj.colOptions as LinkToAnotherRecordType)?.fk_child_column_id, |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
if (!foreignKeyColumn) return |
|
|
|
|
|
|
|
|
|
const relatedTableMeta = await getMeta((columnObj.colOptions as LinkToAnotherRecordType).fk_related_model_id!) |
|
|
|
|
|
|
|
|
|
if (!foreignKeyColumn) return |
|
|
|
|
// 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[foreignKeyColumn.title!] = extractPkFromRow(clipboardContext, (relatedTableMeta as any)!.columns!) |
|
|
|
|
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 (isMm(columnObj)) { |
|
|
|
|
const pasteVal = convertCellData( |
|
|
|
|
{ |
|
|
|
|
value: clipboardData, |
|
|
|
|
to: columnObj.uidt as UITypes, |
|
|
|
|
column: columnObj, |
|
|
|
|
appInfo: unref(appInfo), |
|
|
|
|
}, |
|
|
|
|
isMysql(meta.value?.source_id), |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
if (pasteVal === undefined) 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.dbDataTableRow.nestedListCopyPasteOrDeleteAll( |
|
|
|
|
meta.value?.id as string, |
|
|
|
|
columnObj.id as string, |
|
|
|
|
[ |
|
|
|
|
{ |
|
|
|
|
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, |
|
|
|
|
}, |
|
|
|
|
], |
|
|
|
|
{ viewId: activeView?.value?.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 |
|
|
|
|
} |
|
|
|
|
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)[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.dbDataTableRow.nestedLink( |
|
|
|
|
meta.value?.id as string, |
|
|
|
|
columnObj.id as string, |
|
|
|
|
encodeURIComponent(pasteRowPk), |
|
|
|
|
result.link, |
|
|
|
|
{ |
|
|
|
|
viewId: activeView?.value?.id, |
|
|
|
|
}, |
|
|
|
|
), |
|
|
|
|
result.unlink.length && |
|
|
|
|
api.dbDataTableRow.nestedUnlink( |
|
|
|
|
meta.value?.id as string, |
|
|
|
|
columnObj.id as string, |
|
|
|
|
encodeURIComponent(pasteRowPk), |
|
|
|
|
result.unlink, |
|
|
|
|
{ viewId: activeView?.value?.id }, |
|
|
|
|
), |
|
|
|
|
]) |
|
|
|
|
|
|
|
|
|
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)[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.dbDataTableRow.nestedLink( |
|
|
|
|
meta.value?.id as string, |
|
|
|
|
columnObj.id as string, |
|
|
|
|
encodeURIComponent(pasteRowPk), |
|
|
|
|
result.unlink, |
|
|
|
|
), |
|
|
|
|
result.link.length && |
|
|
|
|
api.dbDataTableRow.nestedUnlink( |
|
|
|
|
meta.value?.id as string, |
|
|
|
|
columnObj.id as string, |
|
|
|
|
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 }), |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return await syncCellData?.(activeCell) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (!isPasteable(rowObj, columnObj, true)) { |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
@ -918,13 +1140,20 @@ export function useMultiSelect(
|
|
|
|
|
const props = [] |
|
|
|
|
|
|
|
|
|
let pasteValue |
|
|
|
|
let isInfoShown = false |
|
|
|
|
|
|
|
|
|
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) || isMm(col)) && !isInfoShown) { |
|
|
|
|
message.info(t('msg.info.groupPasteIsNotSupportedOnLinksColumn')) |
|
|
|
|
isInfoShown = true |
|
|
|
|
} |
|
|
|
|
continue |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|