Browse Source

fix: undo redo mm cell copy paste

pull/7558/head
Ramesh Mane 10 months ago
parent
commit
4434da23e9
  1. 3
      packages/nc-gui/components/smartsheet/Row.vue
  2. 42
      packages/nc-gui/components/smartsheet/grid/Table.vue
  3. 2
      packages/nc-gui/composables/useData.ts
  4. 11
      packages/nc-gui/composables/useLTARStore.ts
  5. 8
      packages/nc-gui/composables/useMultiSelect/convertCellData.ts
  6. 74
      packages/nc-gui/composables/useMultiSelect/index.ts
  7. 40
      packages/nc-gui/composables/useSmartsheetRowStore.ts
  8. 6
      packages/nocodb/src/controllers/data-table.controller.ts
  9. 24
      packages/nocodb/src/schema/swagger.json
  10. 178
      packages/nocodb/src/services/data-table.service.ts
  11. 4
      packages/nocodb/src/utils/acl.ts

3
packages/nc-gui/components/smartsheet/Row.vue

@ -21,7 +21,7 @@ const currentRow = toRef(props, 'row')
const { meta } = useSmartsheetStoreOrThrow() const { meta } = useSmartsheetStoreOrThrow()
const { isNew, state, syncLTARRefs, clearLTARCell, addLTARRef } = useProvideSmartsheetRowStore(meta as Ref<TableType>, currentRow) const { isNew, state, syncLTARRefs, clearLTARCell, addLTARRef, cleaMMCell } = useProvideSmartsheetRowStore(meta as Ref<TableType>, currentRow)
const reloadViewDataTrigger = inject(ReloadViewDataHookInj)! const reloadViewDataTrigger = inject(ReloadViewDataHookInj)!
@ -41,6 +41,7 @@ defineExpose({
syncLTARRefs, syncLTARRefs,
clearLTARCell, clearLTARCell,
addLTARRef, addLTARRef,
cleaMMCell
}) })
</script> </script>

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

@ -50,6 +50,7 @@ import {
useViewColumnsOrThrow, useViewColumnsOrThrow,
useViewsStore, useViewsStore,
watch, watch,
useApi
} from '#imports' } from '#imports'
import type { CellRange, Row } from '#imports' import type { CellRange, Row } from '#imports'
@ -98,6 +99,8 @@ const dataRef = toRef(props, 'data')
const paginationStyleRef = toRef(props, 'pagination') const paginationStyleRef = toRef(props, 'pagination')
const { api } = useApi()
const { const {
loadData, loadData,
changePage, changePage,
@ -263,17 +266,19 @@ provide(JsonExpandInj, isJsonExpand)
async function clearCell(ctx: { row: number; col: number } | null, skipUpdate = false) { async function clearCell(ctx: { row: number; col: number } | null, skipUpdate = false) {
if (!ctx || !hasEditPermission.value || (!isLinksOrLTAR(fields.value[ctx.col]) && isVirtualCol(fields.value[ctx.col]))) return if (!ctx || !hasEditPermission.value || (!isLinksOrLTAR(fields.value[ctx.col]) && isVirtualCol(fields.value[ctx.col]))) return
if (fields.value[ctx.col]?.uidt === UITypes.Links) {
return message.info(t('msg.linkColumnClearNotSupportedYet'))
}
const rowObj = dataRef.value[ctx.row] const rowObj = dataRef.value[ctx.row]
const columnObj = fields.value[ctx.col] const columnObj = fields.value[ctx.col]
if (isVirtualCol(columnObj)) { if (isVirtualCol(columnObj)) {
let mmClearResult
if (isMm(columnObj) && rowRefs.value) {
mmClearResult = await rowRefs.value[ctx.row]!.cleaMMCell(columnObj)
}
addUndo({ addUndo({
undo: { undo: {
fn: async (ctx: { row: number; col: number }, col: ColumnType, row: Row, pg: PaginatedType) => { fn: async (ctx: { row: number; col: number }, col: ColumnType, row: Row, pg: PaginatedType, mmClearResult: any[]) => {
if (paginationDataRef.value?.pageSize === pg.pageSize) { if (paginationDataRef.value?.pageSize === pg.pageSize) {
if (paginationDataRef.value?.page !== pg.page) { if (paginationDataRef.value?.page !== pg.page) {
await changePage?.(pg.page!) await changePage?.(pg.page!)
@ -286,11 +291,17 @@ async function clearCell(ctx: { row: number; col: number } | null, skipUpdate =
rowId === extractPkFromRow(rowObj.row, meta.value?.columns as ColumnType[]) && rowId === extractPkFromRow(rowObj.row, meta.value?.columns as ColumnType[]) &&
columnObj.id === col.id columnObj.id === col.id
) { ) {
rowObj.row[columnObj.title] = row.row[columnObj.title]
if (rowRefs.value) { if (rowRefs.value) {
await rowRefs.value[ctx.row]!.addLTARRef(rowObj.row[columnObj.title], columnObj) if (isBt(columnObj)) {
await rowRefs.value[ctx.row]!.syncLTARRefs(rowObj.row) rowObj.row[columnObj.title] = row.row[columnObj.title]
await rowRefs.value[ctx.row]!.addLTARRef(rowObj.row[columnObj.title], columnObj)
await rowRefs.value[ctx.row]!.syncLTARRefs(rowObj.row)
} else if (isMm(columnObj)) {
await api.dbDataTableRow.nestedLink(meta.value?.id as string, columnObj.id as string, encodeURIComponent(rowId as string), mmClearResult)
rowObj.row[columnObj.title] = mmClearResult?.length ? mmClearResult?.length : null
}
} }
// eslint-disable-next-line @typescript-eslint/no-use-before-define // eslint-disable-next-line @typescript-eslint/no-use-before-define
@ -305,10 +316,10 @@ async function clearCell(ctx: { row: number; col: number } | null, skipUpdate =
throw new Error(t('msg.pageSizeChanged')) throw new Error(t('msg.pageSizeChanged'))
} }
}, },
args: [clone(ctx), clone(columnObj), clone(rowObj), clone(paginationDataRef.value)], args: [clone(ctx), clone(columnObj), clone(rowObj), clone(paginationDataRef.value), mmClearResult],
}, },
redo: { redo: {
fn: async (ctx: { row: number; col: number }, col: ColumnType, row: Row, pg: PaginatedType) => { fn: async (ctx: { row: number; col: number }, col: ColumnType, row: Row, pg: PaginatedType, mmClearResult:any[]) => {
if (paginationDataRef.value?.pageSize === pg.pageSize) { if (paginationDataRef.value?.pageSize === pg.pageSize) {
if (paginationDataRef.value?.page !== pg.page) { if (paginationDataRef.value?.page !== pg.page) {
await changePage?.(pg.page!) await changePage?.(pg.page!)
@ -318,7 +329,11 @@ async function clearCell(ctx: { row: number; col: number } | null, skipUpdate =
const columnObj = fields.value[ctx.col] const columnObj = fields.value[ctx.col]
if (rowId === extractPkFromRow(rowObj.row, meta.value?.columns as ColumnType[]) && columnObj.id === col.id) { if (rowId === extractPkFromRow(rowObj.row, meta.value?.columns as ColumnType[]) && columnObj.id === col.id) {
if (rowRefs.value) { if (rowRefs.value) {
await rowRefs.value[ctx.row]!.clearLTARCell(columnObj) if (isBt(columnObj)) {
await rowRefs.value[ctx.row]!.clearLTARCell(columnObj)
} else if (isMm(columnObj)){
await rowRefs.value[ctx.row]!.cleaMMCell(columnObj)
}
} }
// eslint-disable-next-line @typescript-eslint/no-use-before-define // eslint-disable-next-line @typescript-eslint/no-use-before-define
activeCell.col = ctx.col activeCell.col = ctx.col
@ -332,11 +347,12 @@ async function clearCell(ctx: { row: number; col: number } | null, skipUpdate =
throw new Error(t('msg.pageSizeChanged')) throw new Error(t('msg.pageSizeChanged'))
} }
}, },
args: [clone(ctx), clone(columnObj), clone(rowObj), clone(paginationDataRef.value)], args: [clone(ctx), clone(columnObj), clone(rowObj), clone(paginationDataRef.value), mmClearResult],
}, },
scope: defineViewScope({ view: view.value }), scope: defineViewScope({ view: view.value }),
}) })
if (rowRefs.value) await rowRefs.value[ctx.row]!.clearLTARCell(columnObj) if (isBt(columnObj) && rowRefs.value) await rowRefs.value[ctx.row]!.clearLTARCell(columnObj)
return return
} }

2
packages/nc-gui/composables/useData.ts

@ -480,7 +480,7 @@ export function useData(args: {
base.value.title as string, base.value.title as string,
metaValue?.title as string, metaValue?.title as string,
encodeURIComponent(rowId), encodeURIComponent(rowId),
type as 'mm' | 'hm', type as RelationTypes,
column.title as string, column.title as string,
encodeURIComponent(relatedRowId), encodeURIComponent(relatedRowId),
) )

11
packages/nc-gui/composables/useLTARStore.ts

@ -8,6 +8,7 @@ import {
dateFormats, dateFormats,
parseStringDateTime, parseStringDateTime,
timeFormats, timeFormats,
RelationTypes,
} from 'nocodb-sdk' } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue' import type { ComputedRef, Ref } from 'vue'
import { import {
@ -230,7 +231,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
baseId, baseId,
meta.value.id, meta.value.id,
encodeURIComponent(rowId.value), encodeURIComponent(rowId.value),
colOptions.value.type as 'mm' | 'hm', colOptions.value.type as RelationTypes,
column?.value?.id, column?.value?.id,
{ {
limit: String(childrenExcludedListPagination.size), limit: String(childrenExcludedListPagination.size),
@ -284,7 +285,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
childrenList.value = await $api.public.dataNestedList( childrenList.value = await $api.public.dataNestedList(
sharedView.value?.uuid as string, sharedView.value?.uuid as string,
encodeURIComponent(rowId.value), encodeURIComponent(rowId.value),
colOptions.value.type as 'mm' | 'hm', colOptions.value.type as RelationTypes,
column.value.id, column.value.id,
{ {
limit: String(childrenListPagination.size), limit: String(childrenListPagination.size),
@ -304,7 +305,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
(base?.value?.id || (sharedView.value?.view as any)?.base_id) as string, (base?.value?.id || (sharedView.value?.view as any)?.base_id) as string,
meta.value.id, meta.value.id,
encodeURIComponent(rowId.value), encodeURIComponent(rowId.value),
colOptions.value.type as 'mm' | 'hm', colOptions.value.type as RelationTypes,
column?.value?.id, column?.value?.id,
{ {
limit: String(childrenListPagination.size), limit: String(childrenListPagination.size),
@ -395,7 +396,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
base.value.id as string, base.value.id as string,
metaValue.id!, metaValue.id!,
encodeURIComponent(rowId.value), encodeURIComponent(rowId.value),
colOptions.value.type as 'mm' | 'hm', colOptions.value.type as RelationTypes,
column?.value?.id, column?.value?.id,
encodeURIComponent(getRelatedTableRowId(row) as string), encodeURIComponent(getRelatedTableRowId(row) as string),
) )
@ -461,7 +462,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
base.value.id as string, base.value.id as string,
metaValue.id as string, metaValue.id as string,
encodeURIComponent(rowId.value), encodeURIComponent(rowId.value),
colOptions.value.type as 'mm' | 'hm', colOptions.value.type as RelationTypes,
column?.value?.id, column?.value?.id,
encodeURIComponent(getRelatedTableRowId(row) as string) as string, encodeURIComponent(getRelatedTableRowId(row) as string) as string,
) )

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

@ -1,8 +1,8 @@
import dayjs from 'dayjs' import dayjs from 'dayjs'
import type { ColumnType, LinkToAnotherRecordType, SelectOptionsType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType, SelectOptionsType } from 'nocodb-sdk'
import { RelationTypes, UITypes } from 'nocodb-sdk' import { UITypes } from 'nocodb-sdk'
import type { AppInfo } from '~/composables/useGlobal' import type { AppInfo } from '~/composables/useGlobal'
import { parseProp } from '#imports' import { isBt, isMm, parseProp } from '#imports'
export default function convertCellData( export default function convertCellData(
args: { to: UITypes; value: string; column: ColumnType; appInfo: AppInfo; files?: FileList | File[]; oldValue?: unknown }, args: { to: UITypes; value: string; column: ColumnType; appInfo: AppInfo; files?: FileList | File[]; oldValue?: unknown },
@ -254,7 +254,7 @@ export default function convertCellData(
return undefined return undefined
} }
if ((column.colOptions as LinkToAnotherRecordType)?.type === RelationTypes.BELONGS_TO) { if (isBt(column)) {
const parsedVal = typeof value === 'string' ? JSON.parse(value) : value const parsedVal = typeof value === 'string' ? JSON.parse(value) : value
if ( if (
@ -274,7 +274,7 @@ export default function convertCellData(
return undefined return undefined
} }
if ((column.colOptions as LinkToAnotherRecordType)?.type === RelationTypes.MANY_TO_MANY) { if (isMm(column)) {
const parsedVal = typeof value === 'string' ? JSON.parse(value) : value const parsedVal = typeof value === 'string' ? JSON.parse(value) : value
if ( if (

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

@ -3,7 +3,7 @@ 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, ViewType } 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 { 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'
import { CellRange } from './cellRange' import { CellRange } from './cellRange'
@ -12,9 +12,11 @@ import type { Nullable, Row } from '#imports'
import { import {
extractPkFromRow, extractPkFromRow,
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
isBt,
isDrawerOrModalExist, isDrawerOrModalExist,
isExpandedCellInputExist, isExpandedCellInputExist,
isMac, isMac,
isMm,
isTypableInputColumn, isTypableInputColumn,
message, message,
parseProp, parseProp,
@ -136,10 +138,7 @@ export function useMultiSelect(
} }
} }
if ( if (isBt(columnObj)) {
columnObj.uidt === UITypes.LinkToAnotherRecord &&
(columnObj.colOptions as LinkToAnotherRecordType).type === RelationTypes.BELONGS_TO
) {
// fk_related_model_id is used to prevent paste operation in different fk_related_model_id cell // fk_related_model_id is used to prevent paste operation in different fk_related_model_id cell
textToCopy = { textToCopy = {
fk_related_model_id: (columnObj.colOptions as LinkToAnotherRecordType).fk_related_model_id, fk_related_model_id: (columnObj.colOptions as LinkToAnotherRecordType).fk_related_model_id,
@ -147,10 +146,7 @@ export function useMultiSelect(
} }
} }
if ( if (isMm(columnObj)) {
columnObj.uidt === UITypes.Links &&
(columnObj.colOptions as LinkToAnotherRecordType).type === RelationTypes.MANY_TO_MANY
) {
textToCopy = { textToCopy = {
rowId: extractPkFromRow(rowObj.row, meta.value?.columns as ColumnType[]), rowId: extractPkFromRow(rowObj.row, meta.value?.columns as ColumnType[]),
columnId: columnObj.id, columnId: columnObj.id,
@ -878,10 +874,7 @@ export function useMultiSelect(
const columnObj = unref(fields)[activeCell.col] const columnObj = unref(fields)[activeCell.col]
// handle belongs to column // handle belongs to column
if ( if (isBt(columnObj)) {
columnObj.uidt === UITypes.LinkToAnotherRecord &&
(columnObj.colOptions as LinkToAnotherRecordType)?.type === RelationTypes.BELONGS_TO
) {
const pasteVal = convertCellData( const pasteVal = convertCellData(
{ {
value: clipboardData, value: clipboardData,
@ -911,10 +904,7 @@ export function useMultiSelect(
return await syncCellData?.({ ...activeCell, updatedColumnTitle: foreignKeyColumn.title }) return await syncCellData?.({ ...activeCell, updatedColumnTitle: foreignKeyColumn.title })
} }
if ( if (isMm(columnObj)) {
columnObj.uidt === UITypes.Links &&
(columnObj.colOptions as LinkToAnotherRecordType)?.type === RelationTypes.MANY_TO_MANY
) {
const pasteVal = convertCellData( const pasteVal = convertCellData(
{ {
value: clipboardData, value: clipboardData,
@ -937,21 +927,26 @@ export function useMultiSelect(
let result let result
try { try {
result = await api.dbDataTableRow.nestedListCopyPaste(meta.value?.id as string, columnObj.id as string, [ result = await api.dbDataTableRow.nestedListCopyPasteOrDeleteAll(
{ meta.value?.id as string,
operation: 'copy', columnObj.id as string,
rowId: pasteVal.rowId, [
columnId: pasteVal.columnId, {
fk_related_model_id: pasteVal.fk_related_model_id, operation: 'copy',
}, rowId: pasteVal.rowId,
{ columnId: pasteVal.columnId,
operation: 'paste', fk_related_model_id: pasteVal.fk_related_model_id,
rowId: pasteRowPk, },
columnId: pasteVal.columnId, {
fk_related_model_id: operation: 'paste',
(columnObj.colOptions as LinkToAnotherRecordType).fk_related_model_id || pasteVal.fk_related_model_id, 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 { } catch {
rowObj.row[columnObj.title!] = oldCellValue rowObj.row[columnObj.title!] = oldCellValue
return return
@ -970,17 +965,23 @@ export function useMultiSelect(
pasteRowPk: string, pasteRowPk: string,
result: { link: any[]; unlink: any[] }, result: { link: any[]; unlink: any[] },
value: number, value: number,
activeCell: Nullable<Cell>, activeCell: Cell,
) => { ) => {
const rowObj = unref(data)[activeCell.row]
const columnObj = unref(fields)[activeCell.col]
await Promise.all([ await Promise.all([
result.link.length && result.link.length &&
api.dbDataTableRow.nestedLink(tableId, columnId, encodeURIComponent(pasteRowPk), result.link), api.dbDataTableRow.nestedLink(tableId, columnId, encodeURIComponent(pasteRowPk), result.link, {
viewId: activeView?.value?.id,
}),
result.unlink.length && result.unlink.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(pasteRowPk), encodeURIComponent(pasteRowPk),
result.unlink, result.unlink,
{ viewId: activeView?.value?.id },
), ),
]) ])
@ -997,8 +998,11 @@ export function useMultiSelect(
pasteRowPk: string, pasteRowPk: string,
result: { link: any[]; unlink: any[] }, result: { link: any[]; unlink: any[] },
value: number, value: number,
activeCell: Nullable<Cell>, activeCell: Cell,
) => { ) => {
const rowObj = unref(data)[activeCell.row]
const columnObj = unref(fields)[activeCell.col]
await Promise.all([ await Promise.all([
result.unlink.length && result.unlink.length &&
api.dbDataTableRow.nestedLink(tableId, columnId, encodeURIComponent(pasteRowPk), result.unlink), api.dbDataTableRow.nestedLink(tableId, columnId, encodeURIComponent(pasteRowPk), result.unlink),

40
packages/nc-gui/composables/useSmartsheetRowStore.ts

@ -83,7 +83,7 @@ const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState(
base.value.id as string, base.value.id as string,
metaValue?.id as string, metaValue?.id as string,
encodeURIComponent(rowId), encodeURIComponent(rowId),
type as 'mm' | 'hm', type,
column.id as string, column.id as string,
encodeURIComponent(relatedRowId), encodeURIComponent(relatedRowId),
) )
@ -185,6 +185,43 @@ const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState(
}) })
} }
// clear MM cell
const cleaMMCell = async (column: ColumnType) => {
try {
if (!column || !isLinksOrLTAR(column)) return
const relatedTableMeta = metas.value?.[(<LinkToAnotherRecordType>column?.colOptions)?.fk_related_model_id as string]
if (isNew.value) {
state.value[column.title!] = null
} else if (currentRow.value) {
if ((<LinkToAnotherRecordType>column.colOptions)?.type === RelationTypes.MANY_TO_MANY) {
if (!currentRow.value.row[column.title!]) return
console.log('currentRow.value.row, meta.value?.columns', currentRow.value.row, meta.value?.columns)
const result = await $api.dbDataTableRow.nestedListCopyPasteOrDeleteAll(
meta.value?.id as string,
column.id as string,
[
{
operation: 'deleteAll',
rowId: extractPkFromRow(currentRow.value.row, meta.value?.columns as ColumnType[]) as string,
columnId: column.id as string,
fk_related_model_id: (column.colOptions as LinkToAnotherRecordType)?.fk_related_model_id as string,
},
],
)
currentRow.value.row[column.title!] = null
return Array.isArray(result.unlink) ? result.unlink : []
}
}
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
return { return {
row, row,
state, state,
@ -196,6 +233,7 @@ const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState(
loadRow, loadRow,
currentRow, currentRow,
clearLTARCell, clearLTARCell,
cleaMMCell,
} }
}, },
'smartsheet-row-store', 'smartsheet-row-store',

6
packages/nocodb/src/controllers/data-table.controller.ts

@ -196,8 +196,8 @@ export class DataTableController {
// todo: naming // todo: naming
@Post(['/api/v2/tables/:modelId/links/:columnId/records']) @Post(['/api/v2/tables/:modelId/links/:columnId/records'])
@Acl('nestedDataListCopyPaste') @Acl('nestedDataListCopyPasteOrDeleteAll')
async nestedListCopyPaste( async nestedListCopyPasteOrDeleteAll(
@Req() req: Request, @Req() req: Request,
@Param('modelId') modelId: string, @Param('modelId') modelId: string,
@Query('viewId') viewId: string, @Query('viewId') viewId: string,
@ -210,7 +210,7 @@ export class DataTableController {
fk_related_model_id: string; fk_related_model_id: string;
}[], }[],
) { ) {
return await this.dataTableService.nestedListCopyPaste({ return await this.dataTableService.nestedListCopyPasteOrDeleteAll({
modelId, modelId,
query: req.query, query: req.query,
viewId, viewId,

24
packages/nocodb/src/schema/swagger.json

@ -16351,8 +16351,8 @@
} }
], ],
"post": { "post": {
"summary": "Copy paste nested link", "summary": "Copy paste or deleteAll nested link",
"operationId": "db-data-table-row-nested-list-copy-paste", "operationId": "db-data-table-row-nested-list-copy-paste-or-deleteAll",
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
@ -16388,7 +16388,7 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/nestedListCopyPasteReq" "$ref": "#/components/schemas/nestedListCopyPasteOrDeleteAllReq"
}, },
"examples": { "examples": {
"Example 1": { "Example 1": {
@ -16406,12 +16406,22 @@
"fk_related_model_id": "m797s6vbrqvo2pv" "fk_related_model_id": "m797s6vbrqvo2pv"
} }
] ]
},
"Example 2": {
"value": [
{
"operation": "deleteAll",
"rowId": "1",
"columnId": "ca2ppiy8raidc09",
"fk_related_model_id": "m797s6vbrqvo2pv"
}
]
} }
} }
} }
} }
}, },
"description": "Copy links from the one cell and paste them into another cell", "description": "Copy links from the one cell and paste them into another cell or delete all records from cell",
"parameters": [ "parameters": [
{ {
"$ref": "#/components/parameters/xc-auth" "$ref": "#/components/parameters/xc-auth"
@ -24035,16 +24045,16 @@
} }
} }
}, },
"nestedListCopyPasteReq": { "nestedListCopyPasteOrDeleteAllReq": {
"type": "array", "type": "array",
"minItems": 2, "minItems": 1,
"maxItems": 2, "maxItems": 2,
"items": { "items": {
"type": "object", "type": "object",
"properties": { "properties": {
"operation": { "operation": {
"type": "string", "type": "string",
"enum": ["copy", "paste"] "enum": ["copy", "paste", "deleteAll"]
}, },
"rowId": { "rowId": {
"type": "string" "type": "string"

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

@ -445,41 +445,33 @@ export class DataTableService {
} }
// todo: naming & optimizing // todo: naming & optimizing
async nestedListCopyPaste(param: { async nestedListCopyPasteOrDeleteAll(param: {
cookie: any; cookie: any;
viewId: string; viewId: string;
modelId: string; modelId: string;
columnId: string; columnId: string;
query: any; query: any;
data: { data: {
operation: 'copy' | 'paste'; operation: 'copy' | 'paste' | 'deleteAll';
rowId: string; rowId: string;
columnId: string; columnId: string;
fk_related_model_id: string; fk_related_model_id: string;
}[]; }[];
}) { }) {
validatePayload( validatePayload(
'swagger.json#/components/schemas/nestedListCopyPasteReq', 'swagger.json#/components/schemas/nestedListCopyPasteOrDeleteAllReq',
param.data, param.data,
); );
if (
param.data[0]?.fk_related_model_id !== param.data[1]?.fk_related_model_id
) {
throw new Error(
'The operation is not supported on different fk_related_model_id',
);
}
const operationMap = param.data.reduce( const operationMap = param.data.reduce(
(map, p) => { (map, p) => {
map[p.operation] = p; map[p.operation] = p;
return map; return map;
}, },
{} as Record< {} as Record<
'copy' | 'paste', 'copy' | 'paste' | 'deleteAll',
{ {
operation: 'copy' | 'paste'; operation: 'copy' | 'paste' | 'deleteAll';
rowId: string; rowId: string;
columnId: string; columnId: string;
fk_related_model_id: string; fk_related_model_id: string;
@ -487,6 +479,16 @@ export class DataTableService {
>, >,
); );
if (
!operationMap.deleteAll &&
operationMap.copy.fk_related_model_id !==
operationMap.paste.fk_related_model_id
) {
throw new Error(
'The operation is not supported on different fk_related_model_id',
);
}
const { model, view } = await this.getModelAndView(param); const { model, view } = await this.getModelAndView(param);
const source = await Source.get(model.source_id); const source = await Source.get(model.source_id);
@ -497,21 +499,32 @@ export class DataTableService {
dbDriver: await NcConnectionMgrv2.get(source), dbDriver: await NcConnectionMgrv2.get(source),
}); });
const [existsCopyRow, existsPasteRow] = await Promise.all([ if (
baseModel.exist(operationMap.copy.rowId), operationMap.deleteAll &&
baseModel.exist(operationMap.paste.rowId), !(await baseModel.exist(operationMap.deleteAll.rowId))
]); ) {
if (!existsCopyRow && !existsPasteRow) {
NcError.notFound(
`Record with id '${operationMap.copy.rowId}' and '${operationMap.paste.rowId}' not found`,
);
} else if (!existsCopyRow) {
NcError.notFound(`Record with id '${operationMap.copy.rowId}' not found`);
} else if (!existsPasteRow) {
NcError.notFound( NcError.notFound(
`Record with id '${operationMap.paste.rowId}' not found`, `Record with id '${operationMap.deleteAll.rowId}' not found`,
); );
} else if (operationMap.copy && operationMap.paste) {
const [existsCopyRow, existsPasteRow] = await Promise.all([
baseModel.exist(operationMap.copy.rowId),
baseModel.exist(operationMap.paste.rowId),
]);
if (!existsCopyRow && !existsPasteRow) {
NcError.notFound(
`Record with id '${operationMap.copy.rowId}' and '${operationMap.paste.rowId}' not found`,
);
} else if (!existsCopyRow) {
NcError.notFound(
`Record with id '${operationMap.copy.rowId}' not found`,
);
} else if (!existsPasteRow) {
NcError.notFound(
`Record with id '${operationMap.paste.rowId}' not found`,
);
}
} }
const column = await this.getColumn(param); const column = await this.getColumn(param);
@ -537,55 +550,88 @@ export class DataTableService {
listArgs.sortArr = JSON.parse(listArgs.sortArrJson); listArgs.sortArr = JSON.parse(listArgs.sortArrJson);
} catch (e) {} } catch (e) {}
const [copiedCellNestedList, pasteCellNestedList] = await Promise.all([ if (operationMap.deleteAll) {
baseModel.mmList( let deleteCellNestedList = await baseModel.mmList(
{
colId: operationMap.copy.columnId,
parentId: operationMap.copy.rowId,
},
listArgs as any,
true,
),
baseModel.mmList(
{ {
colId: column.id, colId: column.id,
parentId: operationMap.paste.rowId, parentId: operationMap.deleteAll.rowId,
}, },
listArgs as any, listArgs as any,
true, true,
), );
]);
const filteredRowsToLink = this.filterAndMapRows(
copiedCellNestedList,
pasteCellNestedList,
relatedModel.primaryKeys,
);
const filteredRowsToUnlink = this.filterAndMapRows(
pasteCellNestedList,
copiedCellNestedList,
relatedModel.primaryKeys,
);
await Promise.all([ if (deleteCellNestedList && Array.isArray(deleteCellNestedList)) {
filteredRowsToLink.length && await baseModel.removeLinks({
baseModel.addLinks({
colId: column.id, colId: column.id,
childIds: filteredRowsToLink, childIds: deleteCellNestedList,
rowId: operationMap.paste.rowId, rowId: operationMap.deleteAll.rowId,
cookie: param.cookie, cookie: param.cookie,
}), });
filteredRowsToUnlink.length &&
baseModel.removeLinks({ // extract only pk row data
colId: column.id, deleteCellNestedList = deleteCellNestedList.map((nestedList) => {
childIds: filteredRowsToUnlink, return relatedModel.primaryKeys.reduce((acc, col) => {
rowId: operationMap.paste.rowId, acc[col.title || col.column_name] =
cookie: param.cookie, nestedList[col.title || col.column_name];
}), return acc;
]); }, {});
});
} else {
deleteCellNestedList = [];
}
return { link: filteredRowsToLink, unlink: filteredRowsToUnlink }; return { link: [], unlink: deleteCellNestedList };
} else if (operationMap.copy && operationMap.paste) {
const [copiedCellNestedList, pasteCellNestedList] = await Promise.all([
baseModel.mmList(
{
colId: operationMap.copy.columnId,
parentId: operationMap.copy.rowId,
},
listArgs as any,
true,
),
baseModel.mmList(
{
colId: column.id,
parentId: operationMap.paste.rowId,
},
listArgs as any,
true,
),
]);
const filteredRowsToLink = this.filterAndMapRows(
copiedCellNestedList,
pasteCellNestedList,
relatedModel.primaryKeys,
);
const filteredRowsToUnlink = this.filterAndMapRows(
pasteCellNestedList,
copiedCellNestedList,
relatedModel.primaryKeys,
);
await Promise.all([
filteredRowsToLink.length &&
baseModel.addLinks({
colId: column.id,
childIds: filteredRowsToLink,
rowId: operationMap.paste.rowId,
cookie: param.cookie,
}),
filteredRowsToUnlink.length &&
baseModel.removeLinks({
colId: column.id,
childIds: filteredRowsToUnlink,
rowId: operationMap.paste.rowId,
cookie: param.cookie,
}),
]);
return { link: filteredRowsToLink, unlink: filteredRowsToUnlink };
}
} }
private validateIds(rowIds: any[] | any) { private validateIds(rowIds: any[] | any) {

4
packages/nocodb/src/utils/acl.ts

@ -120,7 +120,7 @@ const permissionScopes = {
'nestedDataList', 'nestedDataList',
'nestedDataLink', 'nestedDataLink',
'nestedDataUnlink', 'nestedDataUnlink',
'nestedListCopyPaste', 'nestedListCopyPasteOrDeleteAll',
'baseUserList', 'baseUserList',
// Base API Tokens // Base API Tokens
@ -228,7 +228,7 @@ const rolePermissions:
nestedDataLink: true, nestedDataLink: true,
nestedDataUnlink: true, nestedDataUnlink: true,
nestedListCopyPaste: true, nestedListCopyPasteOrDeleteAll: true,
// TODO add ACL with base scope // TODO add ACL with base scope
// upload: true, // upload: true,
// uploadViaURL: true, // uploadViaURL: true,

Loading…
Cancel
Save