Browse Source

feat: undo redo support for copy paste mm cell

pull/7558/head
Ramesh Mane 9 months ago
parent
commit
3e0a9b9e8f
  1. 1
      packages/nc-gui/components/smartsheet/grid/Table.vue
  2. 35
      packages/nc-gui/composables/useMultiSelect/convertCellData.ts
  3. 151
      packages/nc-gui/composables/useMultiSelect/index.ts
  4. 2
      packages/nocodb/src/db/BaseModelSqlv2.ts
  5. 10
      packages/nocodb/src/services/data-table.service.ts

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

@ -733,6 +733,7 @@ const {
}, },
bulkUpdateRows, bulkUpdateRows,
fillHandle, fillHandle,
view,
) )
function scrollToRow(row?: number) { function scrollToRow(row?: number) {

35
packages/nc-gui/composables/useMultiSelect/convertCellData.ts

@ -1,6 +1,6 @@
import dayjs from 'dayjs' import dayjs from 'dayjs'
import type { ColumnType, SelectOptionsType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType, SelectOptionsType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk' import { RelationTypes, UITypes } from 'nocodb-sdk'
import type { AppInfo } from '~/composables/useGlobal' import type { AppInfo } from '~/composables/useGlobal'
import { parseProp } from '#imports' import { parseProp } from '#imports'
@ -246,17 +246,42 @@ export default function convertCellData(
return parsedVal || value return parsedVal || value
} }
case UITypes.LinkToAnotherRecord: case UITypes.LinkToAnotherRecord: {
case UITypes.Links: { if (isMultiple) {
return undefined
}
const parsedVal = typeof value === 'string' ? JSON.parse(value) : value const parsedVal = typeof value === 'string' ? JSON.parse(value) : value
if (!(parsedVal && typeof parsedVal === 'object' && !Array.isArray(parsedVal) && Object.keys(parsedVal))) { if (!(parsedVal && typeof parsedVal === 'object' && !Array.isArray(parsedVal) && Object.keys(parsedVal))) {
throw new Error('Invalid LTAR data') throw new Error(`Unsupported conversion for ${to}`)
} }
return parsedVal
}
case UITypes.Links: {
if (isMultiple) { if (isMultiple) {
return undefined return undefined
} }
if ((column.colOptions as LinkToAnotherRecordType)?.type === RelationTypes.MANY_TO_MANY) {
const parsedVal = typeof value === 'string' ? JSON.parse(value) : value
if (
!(
parsedVal &&
typeof parsedVal === 'object' &&
!Array.isArray(parsedVal) &&
// eslint-disable-next-line no-prototype-builtins
['rowId', 'fk_related_model_id', 'value'].every((key) => (parsedVal as Object).hasOwnProperty(key))
) ||
parsedVal?.fk_related_model_id !== (column.colOptions as LinkToAnotherRecordType).fk_related_model_id
) {
throw new Error(`Unsupported conversion for ${to}`)
}
return parsedVal return parsedVal
} else {
throw new Error(`Unsupported conversion for ${to}`)
}
} }
case UITypes.Lookup: case UITypes.Lookup:
case UITypes.Rollup: case UITypes.Rollup:

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

@ -2,7 +2,7 @@ import type { Ref } from 'vue'
import { computed } from 'vue' import { computed } from 'vue'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import type { MaybeRef } from '@vueuse/core' import type { MaybeRef } from '@vueuse/core'
import type { ColumnType, LinkToAnotherRecordType, TableType, UserFieldRecordType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType, TableType, UserFieldRecordType, ViewType } from 'nocodb-sdk'
import { RelationTypes, UITypes, dateFormats, isDateMonthFormat, isSystemColumn, isVirtualCol, timeFormats } from 'nocodb-sdk' import { RelationTypes, UITypes, dateFormats, isDateMonthFormat, isSystemColumn, isVirtualCol, timeFormats } from 'nocodb-sdk'
import { parse } from 'papaparse' import { parse } from 'papaparse'
import type { Cell } from './cellRange' import type { Cell } from './cellRange'
@ -27,6 +27,7 @@ import {
useGlobal, useGlobal,
useI18n, useI18n,
useMetas, useMetas,
useUndoRedo,
} from '#imports' } from '#imports'
const MAIN_MOUSE_PRESSED = 0 const MAIN_MOUSE_PRESSED = 0
@ -49,6 +50,7 @@ export function useMultiSelect(
syncCellData?: Function, syncCellData?: Function,
bulkUpdateRows?: Function, bulkUpdateRows?: Function,
fillHandle?: MaybeRef<HTMLElement | undefined>, fillHandle?: MaybeRef<HTMLElement | undefined>,
view?: MaybeRef<ViewType | undefined>,
) { ) {
const meta = ref(_meta) const meta = ref(_meta)
@ -66,12 +68,16 @@ export function useMultiSelect(
const { api } = useApi() const { api } = useApi()
const { addUndo, clone, defineViewScope } = useUndoRedo()
const editEnabled = ref(_editEnabled) const editEnabled = ref(_editEnabled)
const isMouseDown = ref(false) const isMouseDown = ref(false)
const isFillMode = ref(false) const isFillMode = ref(false)
const activeView = ref(view)
const selectedRange = reactive(new CellRange()) const selectedRange = reactive(new CellRange())
const fillRange = reactive(new CellRange()) const fillRange = reactive(new CellRange())
@ -226,7 +232,6 @@ export function useMultiSelect(
) { ) {
return JSON.stringify({ return JSON.stringify({
rowId: extractPkFromRow(rowObj.row, meta.value?.columns as ColumnType[]), rowId: extractPkFromRow(rowObj.row, meta.value?.columns as ColumnType[]),
columnId: columnObj.id,
fk_related_model_id: (columnObj.colOptions as LinkToAnotherRecordType).fk_related_model_id, fk_related_model_id: (columnObj.colOptions as LinkToAnotherRecordType).fk_related_model_id,
value: !isNaN(+textToCopy) ? +textToCopy : 0, value: !isNaN(+textToCopy) ? +textToCopy : 0,
}) })
@ -866,7 +871,7 @@ export function useMultiSelect(
(columnObj.colOptions as LinkToAnotherRecordType)?.type === RelationTypes.BELONGS_TO (columnObj.colOptions as LinkToAnotherRecordType)?.type === RelationTypes.BELONGS_TO
) { ) {
const clipboardContext = JSON.parse(clipboardData!) const clipboardContext = JSON.parse(clipboardData!)
let pasteVal = convertCellData( const pasteVal = convertCellData(
{ {
value: clipboardContext, value: clipboardContext,
to: columnObj.uidt as UITypes, to: columnObj.uidt as UITypes,
@ -897,15 +902,9 @@ export function useMultiSelect(
columnObj.uidt === UITypes.Links && columnObj.uidt === UITypes.Links &&
(columnObj.colOptions as LinkToAnotherRecordType)?.type === RelationTypes.MANY_TO_MANY (columnObj.colOptions as LinkToAnotherRecordType)?.type === RelationTypes.MANY_TO_MANY
) { ) {
const clipboardContext = JSON.parse(clipboardData) const pasteVal = convertCellData(
if (clipboardContext?.fk_related_model_id !== (columnObj.colOptions as LinkToAnotherRecordType).fk_related_model_id) {
throw new Error('Invalid paste data for MM LTAR cell')
return
}
let pasteVal = convertCellData(
{ {
value: clipboardContext, value: clipboardData,
to: columnObj.uidt as UITypes, to: columnObj.uidt as UITypes,
column: columnObj, column: columnObj,
appInfo: unref(appInfo), appInfo: unref(appInfo),
@ -913,75 +912,95 @@ export function useMultiSelect(
isMysql(meta.value?.source_id), isMysql(meta.value?.source_id),
) )
console.log('paste data', clipboardContext, pasteVal) if (pasteVal === undefined) return
const relatedTableMeta = await getMeta((columnObj.colOptions as LinkToAnotherRecordType).fk_related_model_id!)
const extractedPk = extractPkFromRow(rowObj.row, meta.value?.columns as ColumnType[])
if (!extractedPk) return
const [copiedCellchildrenList, pasteCellchildrenList] = await Promise.all([
api.dbDataTableRow.nestedList(
meta.value?.id as string,
columnObj.id as string,
encodeURIComponent((clipboardContext.rowId as string) || ''),
),
api.dbDataTableRow.nestedList(
meta.value?.id as string,
columnObj.id as string,
encodeURIComponent(extractPkFromRow(rowObj.row, meta.value?.columns as ColumnType[]) || ''),
),
])
const relatedTablePrimaryKeys = (extractPk(relatedTableMeta?.columns as ColumnType[]) || '').split('__')
function filterAndMapRows(
sourceList: Record<string, any>[],
targetList: Record<string, any>[],
primaryKeys: string[] = relatedTablePrimaryKeys,
): Record<string, any>[] {
return sourceList
.filter(
(sourceRow: Record<string, any>) =>
!targetList.some((targetRow: Record<string, any>) =>
primaryKeys.every((key) => sourceRow[key] === targetRow[key]),
),
)
.map((item: Record<string, any>) =>
primaryKeys.reduce((acc, key) => {
acc[key] = item[key]
return acc
}, {} as Record<string, any>),
)
}
const filteredRowsToLink = filterAndMapRows(copiedCellchildrenList.list, pasteCellchildrenList.list) const pasteRowPk = extractPkFromRow(rowObj.row, meta.value?.columns as ColumnType[])
if (!pasteRowPk) return
const filteredRowsToUnlink = filterAndMapRows(pasteCellchildrenList.list, copiedCellchildrenList.list) const oldCellValue = rowObj.row[columnObj.title!]
console.log('copied cell child list', copiedCellchildrenList) rowObj.row[columnObj.title!] = pasteVal.value
console.log('paste cell child list', pasteCellchildrenList)
console.log('filtered', filteredRowsToLink, filteredRowsToUnlink)
rowObj.row[columnObj.title!] = clipboardContext.value let result
const result = await Promise.all([ try {
filteredRowsToLink.length && result = await api.dbDataTableRow.nestedLinkUnlink(meta.value?.id as string, columnObj.id as string, [
api.dbDataTableRow.nestedLink( {
operation: 'copy',
rowId: pasteVal.rowId,
fk_related_model_id: pasteVal.fk_related_model_id,
},
{
operation: 'paste',
rowId: pasteRowPk,
fk_related_model_id:
(columnObj.colOptions as LinkToAnotherRecordType).fk_related_model_id || pasteVal.fk_related_model_id,
},
])
} catch {
rowObj.row[columnObj.title!] = oldCellValue
}
if (result && result?.link && result?.unlink && Array.isArray(result.link) && Array.isArray(result.unlink)) {
addUndo({
redo: {
fn: async (
tableId: string,
columnId: string,
pasteRowPk: string,
result: { link: any[]; unlink: any[] },
value: number,
activeCell: Nullable<Cell>,
) => {
await Promise.all([
result.link.length &&
api.dbDataTableRow.nestedLink(tableId, columnId, encodeURIComponent(pasteRowPk), result.link),
result.unlink.length &&
api.dbDataTableRow.nestedUnlink(
meta.value?.id as string, meta.value?.id as string,
columnObj.id as string, columnObj.id as string,
encodeURIComponent(extractPkFromRow(rowObj.row, meta.value?.columns as ColumnType[]) || ''), encodeURIComponent(pasteRowPk),
filteredRowsToLink, result.unlink,
), ),
filteredRowsToUnlink.length && ])
rowObj.row[columnObj.title!] = value
await syncCellData?.(activeCell)
},
args: [meta.value?.id as string, columnObj.id as string, pasteRowPk, result, pasteVal.value, clone(activeCell)],
},
undo: {
fn: async (
tableId: string,
columnId: string,
pasteRowPk: string,
result: { link: any[]; unlink: any[] },
value: number,
activeCell: Nullable<Cell>,
) => {
await Promise.all([
result.unlink.length &&
api.dbDataTableRow.nestedLink(tableId, columnId, encodeURIComponent(pasteRowPk), result.unlink),
result.link.length &&
api.dbDataTableRow.nestedUnlink( api.dbDataTableRow.nestedUnlink(
meta.value?.id as string, meta.value?.id as string,
columnObj.id as string, columnObj.id as string,
encodeURIComponent(extractPkFromRow(rowObj.row, meta.value?.columns as ColumnType[]) || ''), encodeURIComponent(pasteRowPk),
filteredRowsToUnlink, result.link,
), ),
]) ])
console.log('result', result) rowObj.row[columnObj.title!] = value
await syncCellData?.(activeCell)
},
args: [meta.value?.id as string, columnObj.id as string, pasteRowPk, result, oldCellValue, clone(activeCell)],
},
scope: defineViewScope({ view: activeView?.value }),
})
}
return await syncCellData?.(activeCell) return await syncCellData?.(activeCell)
} }

2
packages/nocodb/src/db/BaseModelSqlv2.ts

@ -1330,7 +1330,7 @@ class BaseModelSqlv2 {
public async mmList( public async mmList(
{ colId, parentId }, { colId, parentId },
args: { limit?; offset?; fieldsSet?: Set<string> } = {}, args: { limit?; offset?; fieldsSet?: Set<string> } = {},
selectAllRecords = false selectAllRecords = false,
) { ) {
const { where, sort, ...rest } = this._getListArgs(args as any); const { where, sort, ...rest } = this._getListArgs(args as any);
const relColumn = (await this.model.getColumns()).find( const relColumn = (await this.model.getColumns()).find(

10
packages/nocodb/src/services/data-table.service.ts

@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { isLinksOrLTAR, RelationTypes } from 'nocodb-sdk'; import { isLinksOrLTAR, RelationTypes } from 'nocodb-sdk';
import { nocoExecute } from 'nc-help'; import { nocoExecute } from 'nc-help';
import { validatePayload } from 'src/helpers';
import type { LinkToAnotherRecordColumn } from '~/models'; import type { LinkToAnotherRecordColumn } from '~/models';
import { DatasService } from '~/services/datas.service'; import { DatasService } from '~/services/datas.service';
import { NcError } from '~/helpers/catchError'; import { NcError } from '~/helpers/catchError';
@ -8,7 +9,6 @@ import getAst from '~/helpers/getAst';
import { PagedResponseImpl } from '~/helpers/PagedResponse'; import { PagedResponseImpl } from '~/helpers/PagedResponse';
import { Column, Model, Source, View } from '~/models'; import { Column, Model, Source, View } from '~/models';
import NcConnectionMgrv2 from '~/utils/common/NcConnectionMgrv2'; import NcConnectionMgrv2 from '~/utils/common/NcConnectionMgrv2';
import { validatePayload } from 'src/helpers';
@Injectable() @Injectable()
export class DataTableService { export class DataTableService {
@ -495,8 +495,10 @@ export class DataTableService {
dbDriver: await NcConnectionMgrv2.get(source), dbDriver: await NcConnectionMgrv2.get(source),
}); });
const existsCopyRow = await baseModel.exist(operationMap.copy.rowId); const [existsCopyRow, existsPasteRow] = await Promise.all([
const existsPasteRow = await baseModel.exist(operationMap.paste.rowId); baseModel.exist(operationMap.copy.rowId),
baseModel.exist(operationMap.paste.rowId),
]);
if (!existsCopyRow && !existsPasteRow) { if (!existsCopyRow && !existsPasteRow) {
NcError.notFound( NcError.notFound(
@ -517,7 +519,7 @@ export class DataTableService {
if (colOptions.type !== RelationTypes.MANY_TO_MANY) return; if (colOptions.type !== RelationTypes.MANY_TO_MANY) return;
const { ast, dependencyFields } = await getAst({ const { dependencyFields } = await getAst({
model: relatedModel, model: relatedModel,
query: param.query, query: param.query,
extractOnlyPrimaries: !(param.query?.f || param.query?.fields), extractOnlyPrimaries: !(param.query?.f || param.query?.fields),

Loading…
Cancel
Save