From cf64048304ad50412bb26373a032596a8aec3462 Mon Sep 17 00:00:00 2001 From: mertmit Date: Sat, 3 Jun 2023 12:48:28 +0300 Subject: [PATCH 01/42] fix: trim unnecessary spaces from copy output Signed-off-by: mertmit --- .../nc-gui/composables/useMultiSelect/copyValue.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/nc-gui/composables/useMultiSelect/copyValue.ts b/packages/nc-gui/composables/useMultiSelect/copyValue.ts index 3a2fca1ac7..14ca8d3e21 100644 --- a/packages/nc-gui/composables/useMultiSelect/copyValue.ts +++ b/packages/nc-gui/composables/useMultiSelect/copyValue.ts @@ -5,21 +5,22 @@ export const copyTable = async (rows: Row[], cols: ColumnType[]) => { let copyHTML = '' let copyPlainText = '' - rows.forEach((row) => { + rows.forEach((row, i) => { let copyRow = '' - cols.forEach((col) => { + cols.forEach((col, i) => { let value = (col.title && row.row[col.title]) ?? '' if (typeof value === 'object') { value = JSON.stringify(value) } copyRow += `` - copyPlainText = `${copyPlainText} ${value} \t` + copyPlainText = `${copyPlainText}${value}${cols.length - 1 !== i ? '\t' : ''}` }) copyHTML += `${copyRow}` - copyPlainText = `${copyPlainText.trim()}\n` + if (rows.length - 1 !== i) { + copyPlainText = `${copyPlainText}\n` + } }) copyHTML += '
${value}
' - copyPlainText.trim() const blobHTML = new Blob([copyHTML], { type: 'text/html' }) const blobPlainText = new Blob([copyPlainText], { type: 'text/plain' }) From 3e72b1670214ff06ca88504cff219ee4fa86a32a Mon Sep 17 00:00:00 2001 From: mertmit Date: Sat, 3 Jun 2023 12:48:48 +0300 Subject: [PATCH 02/42] feat: multiple paste Signed-off-by: mertmit --- .../nc-gui/components/smartsheet/Grid.vue | 2 + .../useMultiSelect/convertCellData.ts | 6 + .../composables/useMultiSelect/index.ts | 191 ++++++++++++------ packages/nc-gui/composables/useViewData.ts | 112 ++++++++++ 4 files changed, 244 insertions(+), 67 deletions(-) diff --git a/packages/nc-gui/components/smartsheet/Grid.vue b/packages/nc-gui/components/smartsheet/Grid.vue index c9b81b0e1e..c0f7d6c2c9 100644 --- a/packages/nc-gui/components/smartsheet/Grid.vue +++ b/packages/nc-gui/components/smartsheet/Grid.vue @@ -125,6 +125,7 @@ const { navigateToSiblingRow, getExpandedRowIndex, deleteRangeOfRows, + updateMultipleRows, } = useViewData(meta, view, xWhere) const { getMeta } = useMetas() @@ -337,6 +338,7 @@ const { // update/save cell value await updateOrSaveRow(rowObj, ctx.updatedColumnTitle || columnObj.title) }, + updateMultipleRows, ) function scrollToCell(row?: number | null, col?: number | null) { diff --git a/packages/nc-gui/composables/useMultiSelect/convertCellData.ts b/packages/nc-gui/composables/useMultiSelect/convertCellData.ts index a6bb90685a..baf26529b0 100644 --- a/packages/nc-gui/composables/useMultiSelect/convertCellData.ts +++ b/packages/nc-gui/composables/useMultiSelect/convertCellData.ts @@ -31,6 +31,12 @@ export default function convertCellData( return parsedNumber } case UITypes.Checkbox: + if (typeof value === 'boolean') return value + if (typeof value === 'string') { + const strval = value.trim().toLowerCase() + if (strval === 'true' || strval === '1') return true + if (strval === 'false' || strval === '0' || strval === '') return false + } return Boolean(value) case UITypes.Date: { const parsedDate = dayjs(value) diff --git a/packages/nc-gui/composables/useMultiSelect/index.ts b/packages/nc-gui/composables/useMultiSelect/index.ts index c9f06926dd..b02acbd0ba 100644 --- a/packages/nc-gui/composables/useMultiSelect/index.ts +++ b/packages/nc-gui/composables/useMultiSelect/index.ts @@ -43,6 +43,7 @@ export function useMultiSelect( scrollToActiveCell?: (row?: number | null, col?: number | null) => void, keyEventHandler?: Function, syncCellData?: Function, + updateMultipleRows?: Function, ) { const meta = ref(_meta) @@ -344,73 +345,6 @@ export function useMultiSelect( } await copyValue() break - // paste - ctrl/cmd + v - case 86: - try { - // if edit permission is not there, return - if (!hasEditPermission) return - - // handle belongs to column - if ( - columnObj.uidt === UITypes.LinkToAnotherRecord && - (columnObj.colOptions as LinkToAnotherRecordType)?.type === RelationTypes.BELONGS_TO - ) { - if (!clipboardContext || typeof clipboardContext.value !== 'object') { - return message.info('Invalid data') - } - rowObj.row[columnObj.title!] = convertCellData( - { - value: clipboardContext.value, - from: clipboardContext.uidt, - to: columnObj.uidt as UITypes, - column: columnObj, - appInfo: unref(appInfo), - }, - isMysql(meta.value?.base_id), - ) - e.preventDefault() - - const foreignKeyColumn = meta.value?.columns?.find( - (column: ColumnType) => column.id === (columnObj.colOptions as LinkToAnotherRecordType)?.fk_child_column_id, - ) - - const relatedTableMeta = await getMeta((columnObj.colOptions as LinkToAnotherRecordType).fk_related_model_id!) - - if (!foreignKeyColumn) return - - rowObj.row[foreignKeyColumn.title!] = extractPkFromRow( - clipboardContext.value, - (relatedTableMeta as any)!.columns!, - ) - - return await syncCellData?.({ ...activeCell, updatedColumnTitle: foreignKeyColumn.title }) - } - - // if it's a virtual column excluding belongs to cell type skip paste - if (isVirtualCol(columnObj)) { - return message.info(t('msg.info.pasteNotSupported')) - } - - if (clipboardContext) { - rowObj.row[columnObj.title!] = convertCellData( - { - value: clipboardContext.value, - from: clipboardContext.uidt, - to: columnObj.uidt as UITypes, - column: columnObj, - appInfo: unref(appInfo), - }, - isMysql(meta.value?.base_id), - ) - e.preventDefault() - syncCellData?.(activeCell) - } else { - clearCell(activeCell as { row: number; col: number }, true) - makeEditable(rowObj, columnObj) - } - } catch (error: any) { - message.error(await extractSdkResponseErrorMsg(error)) - } } } @@ -438,8 +372,131 @@ export function useMultiSelect( const clearSelectedRange = selectedRange.clear.bind(selectedRange) + const handlePaste = async (e: ClipboardEvent) => { + if (!isCellActive.value) { + return + } + + if (unref(editEnabled)) { + return + } + + const clipboardData = e.clipboardData?.getData('text/plain') + + const rowObj = unref(data)[activeCell.row] + const columnObj = unref(fields)[activeCell.col] + + try { + // handle belongs to column + if ( + columnObj.uidt === UITypes.LinkToAnotherRecord && + (columnObj.colOptions as LinkToAnotherRecordType)?.type === RelationTypes.BELONGS_TO + ) { + if (!clipboardContext || typeof clipboardContext.value !== 'object') { + return message.info('Invalid data') + } + rowObj.row[columnObj.title!] = convertCellData( + { + value: clipboardContext.value, + from: clipboardContext.uidt, + to: columnObj.uidt as UITypes, + column: columnObj, + appInfo: unref(appInfo), + }, + isMysql(meta.value?.base_id), + ) + e.preventDefault() + + const foreignKeyColumn = meta.value?.columns?.find( + (column: ColumnType) => column.id === (columnObj.colOptions as LinkToAnotherRecordType)?.fk_child_column_id, + ) + + const relatedTableMeta = await getMeta((columnObj.colOptions as LinkToAnotherRecordType).fk_related_model_id!) + + if (!foreignKeyColumn) return + + rowObj.row[foreignKeyColumn.title!] = extractPkFromRow(clipboardContext.value, (relatedTableMeta as any)!.columns!) + + return await syncCellData?.({ ...activeCell, updatedColumnTitle: foreignKeyColumn.title }) + } + + // if it's a virtual column excluding belongs to cell type skip paste + if (isVirtualCol(columnObj)) { + return message.info(t('msg.info.pasteNotSupported')) + } + + if (clipboardContext) { + rowObj.row[columnObj.title!] = convertCellData( + { + value: clipboardContext.value, + from: clipboardContext.uidt, + to: columnObj.uidt as UITypes, + column: columnObj, + appInfo: unref(appInfo), + }, + isMysql(meta.value?.base_id), + ) + e.preventDefault() + syncCellData?.(activeCell) + } else { + e.preventDefault() + if (clipboardData?.includes('\n') || clipboardData?.includes('\t')) { + const pasteMatrix = clipboardData.split('\n').map((row) => row.split('\t')) + + const pasteMatrixRows = pasteMatrix.length + const pasteMatrixCols = pasteMatrix[0].length + + const colsToPaste = unref(fields).slice(activeCell.col, activeCell.col + pasteMatrixCols) + const rowsToPaste = unref(data).slice(activeCell.row, activeCell.row + pasteMatrixRows) + + for (let i = 0; i < pasteMatrixRows; i++) { + for (let j = 0; j < pasteMatrixCols; j++) { + const pasteRow = rowsToPaste[i] + const pasteCol = colsToPaste[j] + + if (!pasteRow || !pasteCol) { + continue + } + + pasteRow.row[pasteCol.title!] = convertCellData( + { + value: pasteMatrix[i][j], + from: UITypes.SingleLineText, + to: pasteCol.uidt as UITypes, + column: pasteCol, + appInfo: unref(appInfo), + }, + isMysql(meta.value?.base_id), + ) + } + } + console.log(pasteMatrix) + + await updateMultipleRows?.(rowsToPaste) + } else { + rowObj.row[columnObj.title!] = convertCellData( + { + value: clipboardData, + from: UITypes.SingleLineText, + to: columnObj.uidt as UITypes, + column: columnObj, + appInfo: unref(appInfo), + }, + isMysql(meta.value?.base_id), + ) + syncCellData?.(activeCell) + } + // clearCell(activeCell as { row: number; col: number }, true) + // makeEditable(rowObj, columnObj) + } + } catch (error: any) { + message.error(await extractSdkResponseErrorMsg(error)) + } + } + useEventListener(document, 'keydown', handleKeyDown) useEventListener(tbodyEl, 'mouseup', handleMouseUp) + useEventListener(document, 'paste', handlePaste) return { isCellActive, diff --git a/packages/nc-gui/composables/useViewData.ts b/packages/nc-gui/composables/useViewData.ts index 22e829f0b9..d1ffcc2c5d 100644 --- a/packages/nc-gui/composables/useViewData.ts +++ b/packages/nc-gui/composables/useViewData.ts @@ -448,6 +448,117 @@ export function useViewData( } } + async function updateMultipleRows( + rows: Row[], + { metaValue = meta.value, viewMetaValue = viewMeta.value }: { metaValue?: TableType; viewMetaValue?: ViewType } = {}, + undo = false, + ) { + const promises = [] + + for (const row of rows) { + // update changed status + if (row.rowMeta) row.rowMeta.changed = false + + // if new row and save is in progress then wait until the save is complete + promises.push(until(() => !(row.rowMeta?.new && row.rowMeta?.saving)).toMatch((v) => v)) + } + + await Promise.all(promises) + + const updateArray = [] + + for (const row of rows) { + if (row.rowMeta) row.rowMeta.saving = true + + const pk = rowPkData(row.row, metaValue?.columns as ColumnType[]) + + updateArray.push({ ...row.row, ...pk }) + } + + if (!undo) { + addUndo({ + redo: { + fn: async function redo(redoRows: Row[], pg: { page: number; pageSize: number }) { + await updateMultipleRows(redoRows, { metaValue, viewMetaValue }, true) + if (pg.page === paginationData.value.page && pg.pageSize === paginationData.value.pageSize) { + for (const toUpdate of redoRows) { + const rowIndex = findIndexByPk(rowPkData(toUpdate.row, meta?.value?.columns as ColumnType[])) + if (rowIndex !== -1) { + const row = formattedData.value[rowIndex] + Object.assign(row.row, toUpdate.row) + Object.assign(row.oldRow, toUpdate.row) + } else { + await loadData() + break + } + } + } else { + await changePage(pg.page) + } + }, + args: [clone(rows), { page: paginationData.value.page, pageSize: paginationData.value.pageSize }], + }, + undo: { + fn: async function undo(undoRows: Row[], pg: { page: number; pageSize: number }) { + await updateMultipleRows(undoRows, { metaValue, viewMetaValue }, true) + if (pg.page === paginationData.value.page && pg.pageSize === paginationData.value.pageSize) { + for (const toUpdate of undoRows) { + const rowIndex = findIndexByPk(rowPkData(toUpdate.row, meta?.value?.columns as ColumnType[])) + if (rowIndex !== -1) { + const row = formattedData.value[rowIndex] + Object.assign(row.row, toUpdate.row) + Object.assign(row.oldRow, toUpdate.row) + } else { + await loadData() + break + } + } + } else { + await changePage(pg.page) + } + }, + args: [ + clone( + rows.map((row) => { + return { row: row.oldRow, oldRow: row.row, rowMeta: row.rowMeta } + }), + ), + { page: paginationData.value.page, pageSize: paginationData.value.pageSize }, + ], + }, + scope: defineViewScope({ view: viewMetaValue }), + }) + } + + await $api.dbTableRow.bulkUpdate(NOCO, metaValue?.project_id as string, metaValue?.id as string, updateArray) + + for (const row of rows) { + if (!undo) { + /** update row data(to sync formula and other related columns) + * update only formula, rollup and auto updated datetime columns data to avoid overwriting any changes made by user + */ + Object.assign( + row.row, + metaValue!.columns!.reduce>((acc: Record, col: ColumnType) => { + if ( + col.uidt === UITypes.Formula || + col.uidt === UITypes.QrCode || + col.uidt === UITypes.Barcode || + col.uidt === UITypes.Rollup || + col.au || + col.cdf?.includes(' on update ') + ) + acc[col.title!] = row.row[col.title!] + return acc + }, {} as Record), + ) + Object.assign(row.oldRow, row.row) + } + + if (row.rowMeta) row.rowMeta.saving = false + } + } + async function changePage(page: number) { paginationData.value.page = page await loadData({ @@ -807,6 +918,7 @@ export function useViewData( deleteSelectedRows, deleteRangeOfRows, updateOrSaveRow, + updateMultipleRows, selectedAllRecords, syncCount, syncPagination, From 3181721e3d8af46a01def3579de33846ff1a91ce Mon Sep 17 00:00:00 2001 From: mertmit Date: Wed, 7 Jun 2023 22:52:20 +0300 Subject: [PATCH 03/42] feat: revised convertCellData Signed-off-by: mertmit --- .../useMultiSelect/convertCellData.ts | 9 +- .../composables/useMultiSelect/index.ts | 170 ++++++++---------- 2 files changed, 76 insertions(+), 103 deletions(-) diff --git a/packages/nc-gui/composables/useMultiSelect/convertCellData.ts b/packages/nc-gui/composables/useMultiSelect/convertCellData.ts index baf26529b0..44fdf3e367 100644 --- a/packages/nc-gui/composables/useMultiSelect/convertCellData.ts +++ b/packages/nc-gui/composables/useMultiSelect/convertCellData.ts @@ -5,13 +5,10 @@ import type { AppInfo } from '~/composables/useGlobal' import { parseProp } from '#imports' export default function convertCellData( - args: { from: UITypes; to: UITypes; value: any; column: ColumnType; appInfo: AppInfo }, + args: { to: UITypes; value: any; column: ColumnType; appInfo: AppInfo }, isMysql = false, ) { - const { from, to, value } = args - if (from === to && ![UITypes.Attachment, UITypes.Date, UITypes.DateTime, UITypes.Time, UITypes.Year].includes(to)) { - return value - } + const { to, value } = args const dateFormat = isMysql ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ' @@ -145,7 +142,7 @@ export default function convertCellData( case UITypes.Rollup: case UITypes.Formula: case UITypes.QrCode: - throw new Error(`Unsupported conversion from ${from} to ${to}`) + throw new Error(`Unsupported conversion for ${to}`) default: return value } diff --git a/packages/nc-gui/composables/useMultiSelect/index.ts b/packages/nc-gui/composables/useMultiSelect/index.ts index b02acbd0ba..439a61985e 100644 --- a/packages/nc-gui/composables/useMultiSelect/index.ts +++ b/packages/nc-gui/composables/useMultiSelect/index.ts @@ -59,8 +59,6 @@ export function useMultiSelect( const { isMysql } = useProject() - let clipboardContext = $ref<{ value: any; uidt: UITypes } | null>(null) - const editEnabled = ref(_editEnabled) let isMouseDown = $ref(false) @@ -333,16 +331,6 @@ export function useMultiSelect( switch (e.keyCode) { // copy - ctrl/cmd +c case 67: - // set clipboard context only if single cell selected - // or if selected range is empty - if (selectedRange.isSingleCell() || (selectedRange.isEmpty() && rowObj && columnObj)) { - clipboardContext = { - value: rowObj.row[columnObj.title!], - uidt: columnObj.uidt as UITypes, - } - } else { - clipboardContext = null - } await copyValue() break } @@ -381,113 +369,101 @@ export function useMultiSelect( return } - const clipboardData = e.clipboardData?.getData('text/plain') + if (activeCell.row === null || activeCell.row === undefined || activeCell.col === null || activeCell.col === undefined) { + return + } + + e.preventDefault() + + const clipboardData = e.clipboardData?.getData('text/plain') || '' const rowObj = unref(data)[activeCell.row] const columnObj = unref(fields)[activeCell.col] try { - // handle belongs to column - if ( - columnObj.uidt === UITypes.LinkToAnotherRecord && - (columnObj.colOptions as LinkToAnotherRecordType)?.type === RelationTypes.BELONGS_TO - ) { - if (!clipboardContext || typeof clipboardContext.value !== 'object') { - return message.info('Invalid data') - } - rowObj.row[columnObj.title!] = convertCellData( - { - value: clipboardContext.value, - from: clipboardContext.uidt, - to: columnObj.uidt as UITypes, - column: columnObj, - appInfo: unref(appInfo), - }, - isMysql(meta.value?.base_id), - ) - e.preventDefault() - - const foreignKeyColumn = meta.value?.columns?.find( - (column: ColumnType) => column.id === (columnObj.colOptions as LinkToAnotherRecordType)?.fk_child_column_id, - ) + if (clipboardData?.includes('\n') || clipboardData?.includes('\t')) { + // if the clipboard data contains new line or tab, then it is a matrix + const pasteMatrix = clipboardData.split('\n').map((row) => row.split('\t')) - const relatedTableMeta = await getMeta((columnObj.colOptions as LinkToAnotherRecordType).fk_related_model_id!) + const pasteMatrixRows = pasteMatrix.length + const pasteMatrixCols = pasteMatrix[0].length - if (!foreignKeyColumn) return + const colsToPaste = unref(fields).slice(activeCell.col, activeCell.col + pasteMatrixCols) + const rowsToPaste = unref(data).slice(activeCell.row, activeCell.row + pasteMatrixRows) - rowObj.row[foreignKeyColumn.title!] = extractPkFromRow(clipboardContext.value, (relatedTableMeta as any)!.columns!) + for (let i = 0; i < pasteMatrixRows; i++) { + for (let j = 0; j < pasteMatrixCols; j++) { + const pasteRow = rowsToPaste[i] + const pasteCol = colsToPaste[j] - return await syncCellData?.({ ...activeCell, updatedColumnTitle: foreignKeyColumn.title }) - } - - // if it's a virtual column excluding belongs to cell type skip paste - if (isVirtualCol(columnObj)) { - return message.info(t('msg.info.pasteNotSupported')) - } + if (!pasteRow || !pasteCol) { + continue + } - if (clipboardContext) { - rowObj.row[columnObj.title!] = convertCellData( - { - value: clipboardContext.value, - from: clipboardContext.uidt, - to: columnObj.uidt as UITypes, - column: columnObj, - appInfo: unref(appInfo), - }, - isMysql(meta.value?.base_id), - ) - e.preventDefault() - syncCellData?.(activeCell) - } else { - e.preventDefault() - if (clipboardData?.includes('\n') || clipboardData?.includes('\t')) { - const pasteMatrix = clipboardData.split('\n').map((row) => row.split('\t')) - - const pasteMatrixRows = pasteMatrix.length - const pasteMatrixCols = pasteMatrix[0].length - - const colsToPaste = unref(fields).slice(activeCell.col, activeCell.col + pasteMatrixCols) - const rowsToPaste = unref(data).slice(activeCell.row, activeCell.row + pasteMatrixRows) - - for (let i = 0; i < pasteMatrixRows; i++) { - for (let j = 0; j < pasteMatrixCols; j++) { - const pasteRow = rowsToPaste[i] - const pasteCol = colsToPaste[j] - - if (!pasteRow || !pasteCol) { - continue - } - - pasteRow.row[pasteCol.title!] = convertCellData( - { - value: pasteMatrix[i][j], - from: UITypes.SingleLineText, - to: pasteCol.uidt as UITypes, - column: pasteCol, - appInfo: unref(appInfo), - }, - isMysql(meta.value?.base_id), - ) + // skip pasting virtual columns for now + if (isVirtualCol(columnObj)) { + continue } + + pasteRow.row[pasteCol.title!] = convertCellData( + { + value: pasteMatrix[i][j], + to: pasteCol.uidt as UITypes, + column: pasteCol, + appInfo: unref(appInfo), + }, + isMysql(meta.value?.base_id), + ) } - console.log(pasteMatrix) + } + await updateMultipleRows?.(rowsToPaste) + } else { + // handle belongs to column + if ( + columnObj.uidt === UITypes.LinkToAnotherRecord && + (columnObj.colOptions as LinkToAnotherRecordType)?.type === RelationTypes.BELONGS_TO + ) { + const clipboardContext = JSON.parse(clipboardData!) - await updateMultipleRows?.(rowsToPaste) - } else { rowObj.row[columnObj.title!] = convertCellData( { - value: clipboardData, - from: UITypes.SingleLineText, + value: clipboardContext, to: columnObj.uidt as UITypes, column: columnObj, appInfo: unref(appInfo), }, isMysql(meta.value?.base_id), ) - syncCellData?.(activeCell) + + const foreignKeyColumn = meta.value?.columns?.find( + (column: ColumnType) => column.id === (columnObj.colOptions as LinkToAnotherRecordType)?.fk_child_column_id, + ) + + const relatedTableMeta = await getMeta((columnObj.colOptions as LinkToAnotherRecordType).fk_related_model_id!) + + if (!foreignKeyColumn) return + + rowObj.row[foreignKeyColumn.title!] = extractPkFromRow(clipboardContext, (relatedTableMeta as any)!.columns!) + + return await syncCellData?.({ ...activeCell, updatedColumnTitle: foreignKeyColumn.title }) + } + + // if it's a virtual column excluding belongs to cell type skip paste + if (isVirtualCol(columnObj)) { + return message.info(t('msg.info.pasteNotSupported')) } - // clearCell(activeCell as { row: number; col: number }, true) - // makeEditable(rowObj, columnObj) + + rowObj.row[columnObj.title!] = convertCellData( + { + value: clipboardData, + to: columnObj.uidt as UITypes, + column: columnObj, + appInfo: unref(appInfo), + }, + isMysql(meta.value?.base_id), + ) + + await syncCellData?.(activeCell) } } catch (error: any) { message.error(await extractSdkResponseErrorMsg(error)) From 788317c71410536daff27e5e98ba80c5eab4413e Mon Sep 17 00:00:00 2001 From: mertmit Date: Wed, 7 Jun 2023 22:53:39 +0300 Subject: [PATCH 04/42] feat: converCellData for select columns Signed-off-by: mertmit --- .../useMultiSelect/convertCellData.ts | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/nc-gui/composables/useMultiSelect/convertCellData.ts b/packages/nc-gui/composables/useMultiSelect/convertCellData.ts index 44fdf3e367..ea1dbb494a 100644 --- a/packages/nc-gui/composables/useMultiSelect/convertCellData.ts +++ b/packages/nc-gui/composables/useMultiSelect/convertCellData.ts @@ -1,14 +1,14 @@ import dayjs from 'dayjs' -import type { ColumnType } from 'nocodb-sdk' +import type { ColumnType, SelectOptionsType } from 'nocodb-sdk' import { UITypes } from 'nocodb-sdk' import type { AppInfo } from '~/composables/useGlobal' import { parseProp } from '#imports' export default function convertCellData( - args: { to: UITypes; value: any; column: ColumnType; appInfo: AppInfo }, + args: { to: UITypes; value: string; column: ColumnType; appInfo: AppInfo }, isMysql = false, ) { - const { to, value } = args + const { to, value, column } = args const dateFormat = isMysql ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ' @@ -98,7 +98,7 @@ export default function convertCellData( const attachmentMeta = { ...defaultAttachmentMeta, - ...parseProp(args.column?.meta), + ...parseProp(column?.meta), } const attachments = [] @@ -137,6 +137,20 @@ export default function convertCellData( return JSON.stringify(attachments) } + case UITypes.SingleSelect: + case UITypes.MultiSelect: { + // return null if value is empty + if (value === '') return null + + const availableOptions = ((column.colOptions as SelectOptionsType)?.options || []).map((o) => o.title) + const vals = value.split(',') + const validVals = vals.filter((v) => availableOptions.includes(v)) + + // return null if no valid values + if (validVals.length === 0) return null + + return validVals.join(',') + } case UITypes.LinkToAnotherRecord: case UITypes.Lookup: case UITypes.Rollup: From 7a5a34c0b8229fef3edc8ab42c528fd757e20a8f Mon Sep 17 00:00:00 2001 From: mertmit Date: Thu, 8 Jun 2023 18:56:56 +0300 Subject: [PATCH 05/42] refactor: function name for bulk update Signed-off-by: mertmit --- packages/nc-gui/components/smartsheet/Grid.vue | 4 ++-- packages/nc-gui/composables/useMultiSelect/index.ts | 6 +++--- packages/nc-gui/composables/useViewData.ts | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/nc-gui/components/smartsheet/Grid.vue b/packages/nc-gui/components/smartsheet/Grid.vue index c0f7d6c2c9..15d6d3b4dc 100644 --- a/packages/nc-gui/components/smartsheet/Grid.vue +++ b/packages/nc-gui/components/smartsheet/Grid.vue @@ -125,7 +125,7 @@ const { navigateToSiblingRow, getExpandedRowIndex, deleteRangeOfRows, - updateMultipleRows, + bulkUpdateRows, } = useViewData(meta, view, xWhere) const { getMeta } = useMetas() @@ -338,7 +338,7 @@ const { // update/save cell value await updateOrSaveRow(rowObj, ctx.updatedColumnTitle || columnObj.title) }, - updateMultipleRows, + bulkUpdateRows, ) function scrollToCell(row?: number | null, col?: number | null) { diff --git a/packages/nc-gui/composables/useMultiSelect/index.ts b/packages/nc-gui/composables/useMultiSelect/index.ts index 439a61985e..a936b49c76 100644 --- a/packages/nc-gui/composables/useMultiSelect/index.ts +++ b/packages/nc-gui/composables/useMultiSelect/index.ts @@ -43,7 +43,7 @@ export function useMultiSelect( scrollToActiveCell?: (row?: number | null, col?: number | null) => void, keyEventHandler?: Function, syncCellData?: Function, - updateMultipleRows?: Function, + bulkUpdateRows?: Function, ) { const meta = ref(_meta) @@ -392,8 +392,8 @@ export function useMultiSelect( const rowsToPaste = unref(data).slice(activeCell.row, activeCell.row + pasteMatrixRows) for (let i = 0; i < pasteMatrixRows; i++) { + const pasteRow = rowsToPaste[i] for (let j = 0; j < pasteMatrixCols; j++) { - const pasteRow = rowsToPaste[i] const pasteCol = colsToPaste[j] if (!pasteRow || !pasteCol) { @@ -416,7 +416,7 @@ export function useMultiSelect( ) } } - await updateMultipleRows?.(rowsToPaste) + await bulkUpdateRows?.(rowsToPaste) } else { // handle belongs to column if ( diff --git a/packages/nc-gui/composables/useViewData.ts b/packages/nc-gui/composables/useViewData.ts index d1ffcc2c5d..134e7aeea3 100644 --- a/packages/nc-gui/composables/useViewData.ts +++ b/packages/nc-gui/composables/useViewData.ts @@ -448,7 +448,7 @@ export function useViewData( } } - async function updateMultipleRows( + async function bulkUpdateRows( rows: Row[], { metaValue = meta.value, viewMetaValue = viewMeta.value }: { metaValue?: TableType; viewMetaValue?: ViewType } = {}, undo = false, @@ -479,7 +479,7 @@ export function useViewData( addUndo({ redo: { fn: async function redo(redoRows: Row[], pg: { page: number; pageSize: number }) { - await updateMultipleRows(redoRows, { metaValue, viewMetaValue }, true) + await bulkUpdateRows(redoRows, { metaValue, viewMetaValue }, true) if (pg.page === paginationData.value.page && pg.pageSize === paginationData.value.pageSize) { for (const toUpdate of redoRows) { const rowIndex = findIndexByPk(rowPkData(toUpdate.row, meta?.value?.columns as ColumnType[])) @@ -500,7 +500,7 @@ export function useViewData( }, undo: { fn: async function undo(undoRows: Row[], pg: { page: number; pageSize: number }) { - await updateMultipleRows(undoRows, { metaValue, viewMetaValue }, true) + await bulkUpdateRows(undoRows, { metaValue, viewMetaValue }, true) if (pg.page === paginationData.value.page && pg.pageSize === paginationData.value.pageSize) { for (const toUpdate of undoRows) { const rowIndex = findIndexByPk(rowPkData(toUpdate.row, meta?.value?.columns as ColumnType[])) @@ -918,7 +918,7 @@ export function useViewData( deleteSelectedRows, deleteRangeOfRows, updateOrSaveRow, - updateMultipleRows, + bulkUpdateRows, selectedAllRecords, syncCount, syncPagination, From 33bbb383eed0f668bdd036c7e2392a5c3ad885a6 Mon Sep 17 00:00:00 2001 From: mertmit Date: Thu, 8 Jun 2023 19:48:45 +0300 Subject: [PATCH 06/42] fix: bulk update only pasted columns Signed-off-by: mertmit --- .../composables/useMultiSelect/index.ts | 7 +++++-- packages/nc-gui/composables/useViewData.ts | 19 +++++++++++++------ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/nc-gui/composables/useMultiSelect/index.ts b/packages/nc-gui/composables/useMultiSelect/index.ts index a936b49c76..3fb6804433 100644 --- a/packages/nc-gui/composables/useMultiSelect/index.ts +++ b/packages/nc-gui/composables/useMultiSelect/index.ts @@ -390,6 +390,7 @@ export function useMultiSelect( const colsToPaste = unref(fields).slice(activeCell.col, activeCell.col + pasteMatrixCols) const rowsToPaste = unref(data).slice(activeCell.row, activeCell.row + pasteMatrixRows) + const propsToPaste: string[] = [] for (let i = 0; i < pasteMatrixRows; i++) { const pasteRow = rowsToPaste[i] @@ -401,10 +402,12 @@ export function useMultiSelect( } // skip pasting virtual columns for now - if (isVirtualCol(columnObj)) { + if (isVirtualCol(pasteCol)) { continue } + propsToPaste.push(pasteCol.title!) + pasteRow.row[pasteCol.title!] = convertCellData( { value: pasteMatrix[i][j], @@ -416,7 +419,7 @@ export function useMultiSelect( ) } } - await bulkUpdateRows?.(rowsToPaste) + await bulkUpdateRows?.(rowsToPaste, propsToPaste) } else { // handle belongs to column if ( diff --git a/packages/nc-gui/composables/useViewData.ts b/packages/nc-gui/composables/useViewData.ts index 134e7aeea3..0667742671 100644 --- a/packages/nc-gui/composables/useViewData.ts +++ b/packages/nc-gui/composables/useViewData.ts @@ -450,6 +450,7 @@ export function useViewData( async function bulkUpdateRows( rows: Row[], + props: string[], { metaValue = meta.value, viewMetaValue = viewMeta.value }: { metaValue?: TableType; viewMetaValue?: ViewType } = {}, undo = false, ) { @@ -472,14 +473,19 @@ export function useViewData( const pk = rowPkData(row.row, metaValue?.columns as ColumnType[]) - updateArray.push({ ...row.row, ...pk }) + const updateData = props.reduce((acc: Record, prop) => { + acc[prop] = row.row[prop] + return acc + }, {} as Record) + + updateArray.push({ ...updateData, ...pk }) } if (!undo) { addUndo({ redo: { - fn: async function redo(redoRows: Row[], pg: { page: number; pageSize: number }) { - await bulkUpdateRows(redoRows, { metaValue, viewMetaValue }, true) + fn: async function redo(redoRows: Row[], props: string[], pg: { page: number; pageSize: number }) { + await bulkUpdateRows(redoRows, props, { metaValue, viewMetaValue }, true) if (pg.page === paginationData.value.page && pg.pageSize === paginationData.value.pageSize) { for (const toUpdate of redoRows) { const rowIndex = findIndexByPk(rowPkData(toUpdate.row, meta?.value?.columns as ColumnType[])) @@ -496,11 +502,11 @@ export function useViewData( await changePage(pg.page) } }, - args: [clone(rows), { page: paginationData.value.page, pageSize: paginationData.value.pageSize }], + args: [clone(rows), clone(props), { page: paginationData.value.page, pageSize: paginationData.value.pageSize }], }, undo: { - fn: async function undo(undoRows: Row[], pg: { page: number; pageSize: number }) { - await bulkUpdateRows(undoRows, { metaValue, viewMetaValue }, true) + fn: async function undo(undoRows: Row[], props: string[], pg: { page: number; pageSize: number }) { + await bulkUpdateRows(undoRows, props, { metaValue, viewMetaValue }, true) if (pg.page === paginationData.value.page && pg.pageSize === paginationData.value.pageSize) { for (const toUpdate of undoRows) { const rowIndex = findIndexByPk(rowPkData(toUpdate.row, meta?.value?.columns as ColumnType[])) @@ -523,6 +529,7 @@ export function useViewData( return { row: row.oldRow, oldRow: row.row, rowMeta: row.rowMeta } }), ), + props, { page: paginationData.value.page, pageSize: paginationData.value.pageSize }, ], }, From aeb8a7a4f118c7a0162fbe152ed1c5daadf2eb71 Mon Sep 17 00:00:00 2001 From: mertmit Date: Fri, 9 Jun 2023 02:04:37 +0300 Subject: [PATCH 07/42] fix: handle LongText line breaks on paste Signed-off-by: mertmit --- .../composables/useMultiSelect/copyValue.ts | 29 ---- .../composables/useMultiSelect/index.ts | 142 +++++++++++------- 2 files changed, 91 insertions(+), 80 deletions(-) delete mode 100644 packages/nc-gui/composables/useMultiSelect/copyValue.ts diff --git a/packages/nc-gui/composables/useMultiSelect/copyValue.ts b/packages/nc-gui/composables/useMultiSelect/copyValue.ts deleted file mode 100644 index 14ca8d3e21..0000000000 --- a/packages/nc-gui/composables/useMultiSelect/copyValue.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { ColumnType } from 'nocodb-sdk' -import type { Row } from '~/lib' - -export const copyTable = async (rows: Row[], cols: ColumnType[]) => { - let copyHTML = '' - let copyPlainText = '' - - rows.forEach((row, i) => { - let copyRow = '' - cols.forEach((col, i) => { - let value = (col.title && row.row[col.title]) ?? '' - if (typeof value === 'object') { - value = JSON.stringify(value) - } - copyRow += `` - copyPlainText = `${copyPlainText}${value}${cols.length - 1 !== i ? '\t' : ''}` - }) - copyHTML += `${copyRow}` - if (rows.length - 1 !== i) { - copyPlainText = `${copyPlainText}\n` - } - }) - copyHTML += '
${value}
' - - const blobHTML = new Blob([copyHTML], { type: 'text/html' }) - const blobPlainText = new Blob([copyPlainText], { type: 'text/plain' }) - - return navigator.clipboard.write([new ClipboardItem({ [blobHTML.type]: blobHTML, [blobPlainText.type]: blobPlainText })]) -} diff --git a/packages/nc-gui/composables/useMultiSelect/index.ts b/packages/nc-gui/composables/useMultiSelect/index.ts index 3fb6804433..abc3b6f51f 100644 --- a/packages/nc-gui/composables/useMultiSelect/index.ts +++ b/packages/nc-gui/composables/useMultiSelect/index.ts @@ -2,12 +2,12 @@ import dayjs from 'dayjs' import type { MaybeRef } from '@vueuse/core' import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk' import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk' +import { parse } from 'papaparse' import type { Cell } from './cellRange' import { CellRange } from './cellRange' import convertCellData from './convertCellData' import type { Nullable, Row } from '~/lib' import { - copyTable, dateFormats, extractPkFromRow, extractSdkResponseErrorMsg, @@ -99,6 +99,84 @@ export function useMultiSelect( return parseProp(column?.meta)?.time_format ?? timeFormats[0] } + const valueToCopy = (rowObj: Row, columnObj: ColumnType) => { + let textToCopy = (columnObj.title && rowObj.row[columnObj.title]) || '' + + if (columnObj.uidt === UITypes.Checkbox) { + textToCopy = !!textToCopy + } + + if (typeof textToCopy === 'object') { + textToCopy = JSON.stringify(textToCopy) + } + + if (columnObj.uidt === UITypes.Formula) { + textToCopy = textToCopy.replace(/\b(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2})\b/g, (d: string) => { + // TODO(timezone): retrieve the format from the corresponding column meta + // assume hh:mm at this moment + return dayjs(d).utc().local().format('YYYY-MM-DD HH:mm') + }) + } + + if (columnObj.uidt === UITypes.DateTime || columnObj.uidt === UITypes.Time) { + // remove `"` + // e.g. "2023-05-12T08:03:53.000Z" -> 2023-05-12T08:03:53.000Z + textToCopy = textToCopy.replace(/["']/g, '') + + const isMySQL = isMysql(columnObj.base_id) + + let d = dayjs(textToCopy) + + if (!d.isValid()) { + // insert a datetime value, copy the value without refreshing + // e.g. textToCopy = 2023-05-12T03:49:25.000Z + // feed custom parse format + d = dayjs(textToCopy, isMySQL ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ') + } + + // users can change the datetime format in UI + // `textToCopy` would be always in YYYY-MM-DD HH:mm:ss(Z / +xx:yy) format + // therefore, here we reformat to the correct datetime format based on the meta + textToCopy = d.format( + columnObj.uidt === UITypes.DateTime ? constructDateTimeFormat(columnObj) : constructTimeFormat(columnObj), + ) + + if (columnObj.uidt === UITypes.DateTime && !dayjs(textToCopy).isValid()) { + throw new Error('Invalid DateTime') + } + } + + if (columnObj.uidt === UITypes.LongText) { + textToCopy = `"${textToCopy.replace(/\"/g, '""')}"` + } + + return textToCopy + } + + const copyTable = async (rows: Row[], cols: ColumnType[]) => { + let copyHTML = '' + let copyPlainText = '' + + rows.forEach((row, i) => { + let copyRow = '' + cols.forEach((col, i) => { + const value = valueToCopy(row, col) + copyRow += `` + copyPlainText = `${copyPlainText}${value}${cols.length - 1 !== i ? '\t' : ''}` + }) + copyHTML += `${copyRow}` + if (rows.length - 1 !== i) { + copyPlainText = `${copyPlainText}\n` + } + }) + copyHTML += '
${value}
' + + const blobHTML = new Blob([copyHTML], { type: 'text/html' }) + const blobPlainText = new Blob([copyPlainText], { type: 'text/plain' }) + + return navigator.clipboard.write([new ClipboardItem({ [blobHTML.type]: blobHTML, [blobPlainText.type]: blobPlainText })]) + } + async function copyValue(ctx?: Cell) { try { if (selectedRange.start !== null && selectedRange.end !== null && !selectedRange.isSingleCell()) { @@ -117,51 +195,7 @@ export function useMultiSelect( const rowObj = unref(data)[cpRow] const columnObj = unref(fields)[cpCol] - let textToCopy = (columnObj.title && rowObj.row[columnObj.title]) || '' - - if (columnObj.uidt === UITypes.Checkbox) { - textToCopy = !!textToCopy - } - - if (typeof textToCopy === 'object') { - textToCopy = JSON.stringify(textToCopy) - } - - if (columnObj.uidt === UITypes.Formula) { - textToCopy = textToCopy.replace(/\b(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2})\b/g, (d: string) => { - // TODO(timezone): retrieve the format from the corresponding column meta - // assume hh:mm at this moment - return dayjs(d).utc().local().format('YYYY-MM-DD HH:mm') - }) - } - - if (columnObj.uidt === UITypes.DateTime || columnObj.uidt === UITypes.Time) { - // remove `"` - // e.g. "2023-05-12T08:03:53.000Z" -> 2023-05-12T08:03:53.000Z - textToCopy = textToCopy.replace(/["']/g, '') - - const isMySQL = isMysql(columnObj.base_id) - - let d = dayjs(textToCopy) - - if (!d.isValid()) { - // insert a datetime value, copy the value without refreshing - // e.g. textToCopy = 2023-05-12T03:49:25.000Z - // feed custom parse format - d = dayjs(textToCopy, isMySQL ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ') - } - - // users can change the datetime format in UI - // `textToCopy` would be always in YYYY-MM-DD HH:mm:ss(Z / +xx:yy) format - // therefore, here we reformat to the correct datetime format based on the meta - textToCopy = d.format( - columnObj.uidt === UITypes.DateTime ? constructDateTimeFormat(columnObj) : constructTimeFormat(columnObj), - ) - - if (columnObj.uidt === UITypes.DateTime && !dayjs(textToCopy).isValid()) { - throw new Error('Invalid DateTime') - } - } + const textToCopy = valueToCopy(rowObj, columnObj) await copy(textToCopy) message.success(t('msg.info.copiedToClipboard')) @@ -382,11 +416,17 @@ export function useMultiSelect( try { if (clipboardData?.includes('\n') || clipboardData?.includes('\t')) { - // if the clipboard data contains new line or tab, then it is a matrix - const pasteMatrix = clipboardData.split('\n').map((row) => row.split('\t')) + // if the clipboard data contains new line or tab, then it is a matrix or LongText + const parsedClipboard = parse(clipboardData, { delimiter: '\t' }) + + if (parsedClipboard.errors.length > 0) { + throw new Error(parsedClipboard.errors[0].message) + } + + const clipboardMatrix = parsedClipboard.data as string[][] - const pasteMatrixRows = pasteMatrix.length - const pasteMatrixCols = pasteMatrix[0].length + const pasteMatrixRows = clipboardMatrix.length + const pasteMatrixCols = clipboardMatrix[0].length const colsToPaste = unref(fields).slice(activeCell.col, activeCell.col + pasteMatrixCols) const rowsToPaste = unref(data).slice(activeCell.row, activeCell.row + pasteMatrixRows) @@ -410,7 +450,7 @@ export function useMultiSelect( pasteRow.row[pasteCol.title!] = convertCellData( { - value: pasteMatrix[i][j], + value: clipboardMatrix[i][j], to: pasteCol.uidt as UITypes, column: pasteCol, appInfo: unref(appInfo), From 51b6d1073a9c13017e1dd7213828304fc2eb17db Mon Sep 17 00:00:00 2001 From: mertmit Date: Fri, 9 Jun 2023 02:40:23 +0300 Subject: [PATCH 08/42] fix: disable grid copy/paste if modal or drawer present Signed-off-by: mertmit --- packages/nc-gui/composables/useMultiSelect/index.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/nc-gui/composables/useMultiSelect/index.ts b/packages/nc-gui/composables/useMultiSelect/index.ts index abc3b6f51f..b146f3b019 100644 --- a/packages/nc-gui/composables/useMultiSelect/index.ts +++ b/packages/nc-gui/composables/useMultiSelect/index.ts @@ -11,6 +11,7 @@ import { dateFormats, extractPkFromRow, extractSdkResponseErrorMsg, + isDrawerOrModalExist, isMac, isTypableInputColumn, message, @@ -361,7 +362,11 @@ export function useMultiSelect( const rowObj = unref(data)[activeCell.row] const columnObj = unref(fields)[activeCell.col] - if ((!unref(editEnabled) || !isTypableInputColumn(columnObj)) && (isMac() ? e.metaKey : e.ctrlKey)) { + if ( + (!unref(editEnabled) || !isTypableInputColumn(columnObj)) && + !isDrawerOrModalExist() && + (isMac() ? e.metaKey : e.ctrlKey) + ) { switch (e.keyCode) { // copy - ctrl/cmd +c case 67: @@ -395,6 +400,10 @@ export function useMultiSelect( const clearSelectedRange = selectedRange.clear.bind(selectedRange) const handlePaste = async (e: ClipboardEvent) => { + if (isDrawerOrModalExist()) { + return + } + if (!isCellActive.value) { return } From 4d79a04923cdb8a7bf86899448ec50c424604d9d Mon Sep 17 00:00:00 2001 From: mertmit Date: Fri, 9 Jun 2023 02:43:12 +0300 Subject: [PATCH 09/42] fix: avoid edit mode for select columns Signed-off-by: mertmit --- packages/nc-gui/components/cell/MultiSelect.vue | 6 ++---- packages/nc-gui/components/cell/SingleSelect.vue | 14 ++++++-------- packages/nc-gui/components/smartsheet/Grid.vue | 4 ++++ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/nc-gui/components/cell/MultiSelect.vue b/packages/nc-gui/components/cell/MultiSelect.vue index 044f406aad..ab227f90f0 100644 --- a/packages/nc-gui/components/cell/MultiSelect.vue +++ b/packages/nc-gui/components/cell/MultiSelect.vue @@ -47,8 +47,6 @@ const readOnly = inject(ReadonlyInj)! const active = inject(ActiveCellInj, ref(false)) -const editable = inject(EditModeInj, ref(false)) - const isPublic = inject(IsPublicInj, ref(false)) const isForm = inject(IsFormInj, ref(false)) @@ -96,7 +94,7 @@ const isOptionMissing = computed(() => { const hasEditRoles = computed(() => hasRole('owner', true) || hasRole('creator', true) || hasRole('editor', true)) -const editAllowed = computed(() => (hasEditRoles.value || isForm.value) && (active.value || editable.value)) +const editAllowed = computed(() => (hasEditRoles.value || isForm.value) && active.value) const vModel = computed({ get: () => { @@ -331,7 +329,7 @@ const selectedOpts = computed(() => { diff --git a/packages/nc-gui/components/smartsheet/Grid.vue b/packages/nc-gui/components/smartsheet/Grid.vue index 100719df37..fac496b06a 100644 --- a/packages/nc-gui/components/smartsheet/Grid.vue +++ b/packages/nc-gui/components/smartsheet/Grid.vue @@ -572,7 +572,7 @@ async function clearCell(ctx: { row: number; col: number } | null, skipUpdate = } function makeEditable(row: Row, col: ColumnType) { - if (!hasEditPermission || editEnabled || isView) { + if (!hasEditPermission || editEnabled || isView || isLocked.value || readOnly.value) { return } From 3f4350131d5109938820b1a373ad226425237a65 Mon Sep 17 00:00:00 2001 From: mertmit Date: Sat, 10 Jun 2023 19:52:44 +0300 Subject: [PATCH 28/42] fix: clear selected range on addEmptyRow Signed-off-by: mertmit --- packages/nc-gui/components/smartsheet/Grid.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/nc-gui/components/smartsheet/Grid.vue b/packages/nc-gui/components/smartsheet/Grid.vue index fac496b06a..5c7507d7ab 100644 --- a/packages/nc-gui/components/smartsheet/Grid.vue +++ b/packages/nc-gui/components/smartsheet/Grid.vue @@ -831,6 +831,7 @@ const deleteSelectedRangeOfRows = () => { function addEmptyRow(row?: number) { const rowObj = _addEmptyRow(row) nextTick().then(() => { + clearSelectedRange() makeActive(row ?? data.value.length - 1, 0) scrollToCell?.() }) From dbc9272a45e5bae1e94b825f0de364546cbd41af Mon Sep 17 00:00:00 2001 From: mertmit Date: Sun, 11 Jun 2023 01:02:56 +0300 Subject: [PATCH 29/42] fix: skip front-end columns on paste for now Signed-off-by: mertmit --- .../nc-gui/composables/useMultiSelect/index.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/nc-gui/composables/useMultiSelect/index.ts b/packages/nc-gui/composables/useMultiSelect/index.ts index e87fca319b..14c7f10f78 100644 --- a/packages/nc-gui/composables/useMultiSelect/index.ts +++ b/packages/nc-gui/composables/useMultiSelect/index.ts @@ -521,8 +521,15 @@ export function useMultiSelect( const rowsToPaste = unref(data).slice(activeCell.row, activeCell.row + pasteMatrixRows) const propsToPaste: string[] = [] + let pastedRows = 0 + for (let i = 0; i < pasteMatrixRows; i++) { const pasteRow = rowsToPaste[i] + // TODO handle insert new row + if (pasteRow.rowMeta.new) break + + pastedRows++ + for (let j = 0; j < pasteMatrixCols; j++) { const pasteCol = colsToPaste[j] @@ -565,9 +572,11 @@ export function useMultiSelect( } await bulkUpdateRows?.(rowsToPaste, propsToPaste) - // highlight the pasted range - selectedRange.startRange({ row: activeCell.row, col: activeCell.col }) - selectedRange.endRange({ row: activeCell.row + pasteMatrixRows - 1, col: activeCell.col + pasteMatrixCols - 1 }) + if (pastedRows > 0) { + // highlight the pasted range + selectedRange.startRange({ row: activeCell.row, col: activeCell.col }) + selectedRange.endRange({ row: activeCell.row + pastedRows - 1, col: activeCell.col + pasteMatrixCols - 1 }) + } } else { // handle belongs to column if ( From aa0c9322dd975f7d786cd147266eb73b7e2820c9 Mon Sep 17 00:00:00 2001 From: mertmit Date: Sun, 11 Jun 2023 01:27:30 +0300 Subject: [PATCH 30/42] feat: clear multiple cells Signed-off-by: mertmit --- .../nc-gui/components/smartsheet/Grid.vue | 36 +++++++++++++++++++ .../composables/useMultiSelect/index.ts | 10 ++++-- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/packages/nc-gui/components/smartsheet/Grid.vue b/packages/nc-gui/components/smartsheet/Grid.vue index 5c7507d7ab..13ae19a5f8 100644 --- a/packages/nc-gui/components/smartsheet/Grid.vue +++ b/packages/nc-gui/components/smartsheet/Grid.vue @@ -210,6 +210,7 @@ const { $$(editEnabled), isPkAvail, clearCell, + clearSelectedRangeOfCells, makeEditable, scrollToCell, (e: KeyboardEvent) => { @@ -571,6 +572,36 @@ async function clearCell(ctx: { row: number; col: number } | null, skipUpdate = } } +async function clearSelectedRangeOfCells() { + if (!hasEditPermission) return + + 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 = fields.value.slice(startCol, endCol + 1) + const rows = data.value.slice(startRow, endRow + 1) + const props = [] + + for (const row of rows) { + for (const col of cols) { + if (!row || !col || !col.title) continue + + // TODO handle LinkToAnotherRecord + if (isVirtualCol(col)) continue + + row.row[col.title] = null + props.push(col.title) + } + } + + await bulkUpdateRows(rows, props) +} + function makeEditable(row: Row, col: ColumnType) { if (!hasEditPermission || editEnabled || isView || isLocked.value || readOnly.value) { return @@ -1095,6 +1126,11 @@ function addEmptyRow(row?: number) {
{{ $t('activity.clearCell') }}
+ + +
Clear Cells
+
+
diff --git a/packages/nc-gui/composables/useMultiSelect/index.ts b/packages/nc-gui/composables/useMultiSelect/index.ts index 14c7f10f78..195569d811 100644 --- a/packages/nc-gui/composables/useMultiSelect/index.ts +++ b/packages/nc-gui/composables/useMultiSelect/index.ts @@ -40,6 +40,7 @@ export function useMultiSelect( _editEnabled: MaybeRef, isPkAvail: MaybeRef, clearCell: Function, + clearSelectedRangeOfCells: Function, makeEditable: Function, scrollToCell?: (row?: number | null, col?: number | null) => void, keyEventHandler?: Function, @@ -335,9 +336,14 @@ export function useMultiSelect( /** on delete key press clear cell */ case 'Delete': e.preventDefault() - selectedRange.clear() - await clearCell(activeCell as { row: number; col: number }) + if (selectedRange.isSingleCell()) { + selectedRange.clear() + + await clearCell(activeCell as { row: number; col: number }) + } else { + await clearSelectedRangeOfCells() + } break /** on arrow key press navigate through cells */ case 'ArrowRight': From dab3471bed45a573fd43ca2bc808d19233b7e0a3 Mon Sep 17 00:00:00 2001 From: mertmit Date: Sun, 11 Jun 2023 04:53:46 +0300 Subject: [PATCH 31/42] feat: fill using multiple select and paste Signed-off-by: mertmit --- .../composables/useMultiSelect/index.ts | 164 ++++++++++++------ 1 file changed, 111 insertions(+), 53 deletions(-) diff --git a/packages/nc-gui/composables/useMultiSelect/index.ts b/packages/nc-gui/composables/useMultiSelect/index.ts index 195569d811..d92c112329 100644 --- a/packages/nc-gui/composables/useMultiSelect/index.ts +++ b/packages/nc-gui/composables/useMultiSelect/index.ts @@ -506,9 +506,6 @@ export function useMultiSelect( const clipboardData = e.clipboardData?.getData('text/plain') || '' - const rowObj = unref(data)[activeCell.row] - const columnObj = unref(fields)[activeCell.col] - try { if (clipboardData?.includes('\n') || clipboardData?.includes('\t')) { // if the clipboard data contains new line or tab, then it is a matrix or LongText @@ -531,6 +528,7 @@ export function useMultiSelect( for (let i = 0; i < pasteMatrixRows; i++) { const pasteRow = rowsToPaste[i] + // TODO handle insert new row if (pasteRow.rowMeta.new) break @@ -543,7 +541,7 @@ export function useMultiSelect( continue } - // skip pasting virtual columns for now + // skip pasting virtual columns (including LTAR columns for now) if (isVirtualCol(pasteCol)) { continue } @@ -584,16 +582,58 @@ export function useMultiSelect( selectedRange.endRange({ row: activeCell.row + pastedRows - 1, col: activeCell.col + pasteMatrixCols - 1 }) } } else { - // 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 (selectedRange.isSingleCell()) { + const rowObj = unref(data)[activeCell.row] + 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( + { + value: clipboardContext, + to: columnObj.uidt as UITypes, + column: columnObj, + appInfo: unref(appInfo), + }, + isMysql(meta.value?.base_id), + ) + + const foreignKeyColumn = meta.value?.columns?.find( + (column: ColumnType) => column.id === (columnObj.colOptions as LinkToAnotherRecordType)?.fk_child_column_id, + ) + + const relatedTableMeta = await getMeta((columnObj.colOptions as LinkToAnotherRecordType).fk_related_model_id!) + + if (!foreignKeyColumn) return + + rowObj.row[foreignKeyColumn.title!] = extractPkFromRow(clipboardContext, (relatedTableMeta as any)!.columns!) + + return await syncCellData?.({ ...activeCell, updatedColumnTitle: foreignKeyColumn.title }) + } + + // if it's a virtual column excluding belongs to cell type skip paste + if (isVirtualCol(columnObj)) { + return message.info(t('msg.info.pasteNotSupported')) + } + + // skip pasting auto increment columns + if (columnObj.ai) { + return message.info(t('msg.info.autoIncFieldNotEditable')) + } + + // skip pasting primary key columns + if (columnObj.pk && !rowObj.rowMeta.new) { + return message.info(t('msg.info.editingPKnotSupported')) + } + + const pasteValue = convertCellData( { - value: clipboardContext, + value: clipboardData, to: columnObj.uidt as UITypes, column: columnObj, appInfo: unref(appInfo), @@ -601,49 +641,67 @@ export function useMultiSelect( isMysql(meta.value?.base_id), ) - const foreignKeyColumn = meta.value?.columns?.find( - (column: ColumnType) => column.id === (columnObj.colOptions as LinkToAnotherRecordType)?.fk_child_column_id, - ) - - const relatedTableMeta = await getMeta((columnObj.colOptions as LinkToAnotherRecordType).fk_related_model_id!) - - if (!foreignKeyColumn) return - - rowObj.row[foreignKeyColumn.title!] = extractPkFromRow(clipboardContext, (relatedTableMeta as any)!.columns!) - - return await syncCellData?.({ ...activeCell, updatedColumnTitle: foreignKeyColumn.title }) - } - - // if it's a virtual column excluding belongs to cell type skip paste - if (isVirtualCol(columnObj)) { - return message.info(t('msg.info.pasteNotSupported')) - } - - // skip pasting auto increment columns - if (columnObj.ai) { - return message.info(t('msg.info.autoIncFieldNotEditable')) - } + if (pasteValue !== undefined) { + rowObj.row[columnObj.title!] = pasteValue + } - // skip pasting primary key columns - if (columnObj.pk && !rowObj.rowMeta.new) { - return message.info(t('msg.info.editingPKnotSupported')) - } + await syncCellData?.(activeCell) + } else { + 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) + const rows = unref(data).slice(startRow, endRow + 1) + const props = [] + + for (const row of rows) { + // TODO handle insert new row + if (row.rowMeta.new) continue + + for (const col of cols) { + if (!row || !col || !col.title) continue + + // skip pasting virtual columns (including LTAR columns for now) + if (isVirtualCol(col)) { + continue + } + + // skip pasting auto increment columns + if (col.ai) { + continue + } + + // skip pasting primary key columns + if (col.pk && !row.rowMeta.new) { + continue + } + + props.push(col.title) + + const pasteValue = convertCellData( + { + value: clipboardData, + to: col.uidt as UITypes, + column: col, + appInfo: unref(appInfo), + }, + isMysql(meta.value?.base_id), + true, + ) + + if (pasteValue !== undefined) { + row.row[col.title] = pasteValue + } + } + } - const pasteValue = convertCellData( - { - value: clipboardData, - to: columnObj.uidt as UITypes, - column: columnObj, - appInfo: unref(appInfo), - }, - isMysql(meta.value?.base_id), - ) - - if (pasteValue !== undefined) { - rowObj.row[columnObj.title!] = pasteValue + await bulkUpdateRows?.(rows, props) } - - await syncCellData?.(activeCell) } } catch (error: any) { message.error(await extractSdkResponseErrorMsg(error)) From ffff6f73dfdd76655f5114790b900fd35dc31859 Mon Sep 17 00:00:00 2001 From: mertmit Date: Sun, 11 Jun 2023 17:40:53 +0300 Subject: [PATCH 32/42] fix: selected range anchor Signed-off-by: mertmit --- packages/nc-gui/composables/useMultiSelect/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/nc-gui/composables/useMultiSelect/index.ts b/packages/nc-gui/composables/useMultiSelect/index.ts index d92c112329..93f6f42e25 100644 --- a/packages/nc-gui/composables/useMultiSelect/index.ts +++ b/packages/nc-gui/composables/useMultiSelect/index.ts @@ -278,7 +278,9 @@ export function useMultiSelect( setTimeout(() => { // if shift key is pressed, don't change the active cell if (event.shiftKey) return - makeActive(selectedRange.start.row, selectedRange.start.col) + if (selectedRange._start) { + makeActive(selectedRange._start.row, selectedRange._start.col) + } }, 0) // if the editEnabled is false, prevent selecting text on mouseUp From 7fb6f4efc2bc4f3bb8ff4659c23a271c26c0aca8 Mon Sep 17 00:00:00 2001 From: mertmit Date: Mon, 12 Jun 2023 11:42:17 +0300 Subject: [PATCH 33/42] fix: move handleMouseUp to document to avoid stucked select mode Signed-off-by: mertmit --- .../nc-gui/components/smartsheet/Grid.vue | 18 +++++----- .../composables/useMultiSelect/index.ts | 35 +++++++++---------- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/packages/nc-gui/components/smartsheet/Grid.vue b/packages/nc-gui/components/smartsheet/Grid.vue index 13ae19a5f8..cc6c69e7ca 100644 --- a/packages/nc-gui/components/smartsheet/Grid.vue +++ b/packages/nc-gui/components/smartsheet/Grid.vue @@ -104,7 +104,8 @@ const expandedFormDlg = ref(false) const expandedFormRow = ref() const expandedFormRowState = ref>() const gridWrapper = ref() -const tableHead = ref() +const tableHeadEl = ref() +const tableBodyEl = ref() const isAddingColumnAllowed = $computed(() => !readOnly.value && !isLocked.value && isUIAllowed('add-column') && !isSqlView.value) @@ -199,7 +200,6 @@ const { clearSelectedRange, copyValue, isCellActive, - tbodyEl, resetSelectedRange, makeActive, selectedRange, @@ -348,13 +348,13 @@ function scrollToCell(row?: number | null, col?: number | null) { if (row !== null && col !== null) { // get active cell - const rows = tbodyEl.value?.querySelectorAll('tr') + const rows = tableBodyEl.value?.querySelectorAll('tr') const cols = rows?.[row].querySelectorAll('td') const td = cols?.[col === 0 ? 0 : col + 1] if (!td || !gridWrapper.value) return - const { height: headerHeight } = tableHead.value!.getBoundingClientRect() + const { height: headerHeight } = tableHeadEl.value!.getBoundingClientRect() const tdScroll = getContainerScrollForElement(td, gridWrapper.value, { top: headerHeight, bottom: 9, right: 9 }) // if first column set left to 0 since it's sticky it will be visible and calculated value will be wrong @@ -465,7 +465,7 @@ const onXcResizing = (cn: string, event: any) => { defineExpose({ loadData, openColumnCreate: (data) => { - tableHead.value?.querySelector('th:last-child')?.scrollIntoView({ behavior: 'smooth' }) + tableHeadEl.value?.querySelector('th:last-child')?.scrollIntoView({ behavior: 'smooth' }) setTimeout(() => { addColumnDropdown.value = true preloadColumn.value = data @@ -642,7 +642,7 @@ useEventListener(document, 'keyup', async (e: KeyboardEvent) => { /** On clicking outside of table reset active cell */ const smartTable = ref(null) -onClickOutside(tbodyEl, (e) => { +onClickOutside(tableBodyEl, (e) => { // do nothing if context menu was open if (contextMenu.value) return @@ -826,7 +826,7 @@ const closeAddColumnDropdown = (scrollToLastCol = false) => { addColumnDropdown.value = false if (scrollToLastCol) { setTimeout(() => { - const lastAddNewRowHeader = tableHead.value?.querySelector('th:last-child') + const lastAddNewRowHeader = tableHeadEl.value?.querySelector('th:last-child') if (lastAddNewRowHeader) { lastAddNewRowHeader.scrollIntoView({ behavior: 'smooth' }) } @@ -889,7 +889,7 @@ function addEmptyRow(row?: number) { class="xc-row-table nc-grid backgroundColorDefault !h-auto bg-white" @contextmenu="showContextMenu" > - +
@@ -955,7 +955,7 @@ function addEmptyRow(row?: number) { - +