From 4a7645fe015e1982480046db1f7b6a5c2ddb0348 Mon Sep 17 00:00:00 2001 From: mertmit Date: Tue, 28 Mar 2023 21:39:42 +0300 Subject: [PATCH 01/67] fix: tree view reorder Signed-off-by: mertmit --- .../nc-gui/components/dashboard/TreeView.vue | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/nc-gui/components/dashboard/TreeView.vue b/packages/nc-gui/components/dashboard/TreeView.vue index dd1a6f129c..b7ed8178ed 100644 --- a/packages/nc-gui/components/dashboard/TreeView.vue +++ b/packages/nc-gui/components/dashboard/TreeView.vue @@ -90,8 +90,6 @@ const initSortable = (el: Element) => { if (sortables[base_id]) sortables[base_id].destroy() Sortable.create(el as HTMLLIElement, { onEnd: async (evt) => { - const offset = tables.value.findIndex((table) => table.base_id === base_id) - const { newIndex = 0, oldIndex = 0 } = evt const itemEl = evt.item as HTMLLIElement @@ -120,8 +118,19 @@ const initSortable = (el: Element) => { item.order = ((itemBefore.order as number) + (itemAfter.order as number)) / 2 } - // update the order of the moved item - tables.value?.splice(newIndex + offset, 0, ...tables.value?.splice(oldIndex + offset, 1)) + // find the index of the moved item + const itemIndex = tables.value?.findIndex((table) => table.id === item.id) + + // move the item to the new position + if (itemBefore) { + // find the index of the item before the moved item + const itemBeforeIndex = tables.value?.findIndex((table) => table.id === itemBefore.id) + tables.value?.splice(itemBeforeIndex + (newIndex > oldIndex ? 0 : 1), 0, ...tables.value?.splice(itemIndex, 1)) + } else { + // if the item before is undefined (moving item to first slot), then find the index of the item after the moved item + const itemAfterIndex = tables.value?.findIndex((table) => table.id === itemAfter.id) + tables.value?.splice(itemAfterIndex, 0, ...tables.value?.splice(itemIndex, 1)) + } // force re-render the list if (keys[base_id]) { From c8e1fc6c04e3b2532b561ff8e460b097c5ea68fb Mon Sep 17 00:00:00 2001 From: mertmit Date: Sat, 11 Mar 2023 16:20:18 +0300 Subject: [PATCH 02/67] feat: undo/redo for row changes Signed-off-by: mertmit --- packages/nc-gui/composables/useUndoRedo.ts | 61 ++++++++ packages/nc-gui/composables/useViewData.ts | 153 +++++++++++++++++---- packages/nc-gui/lib/types.ts | 5 + packages/nc-gui/utils/dataUtils.ts | 15 +- 4 files changed, 209 insertions(+), 25 deletions(-) create mode 100644 packages/nc-gui/composables/useUndoRedo.ts diff --git a/packages/nc-gui/composables/useUndoRedo.ts b/packages/nc-gui/composables/useUndoRedo.ts new file mode 100644 index 0000000000..4866a8554f --- /dev/null +++ b/packages/nc-gui/composables/useUndoRedo.ts @@ -0,0 +1,61 @@ +import type { Ref } from 'vue' +import clone from 'just-clone' +import { createSharedComposable, ref } from '#imports' +import type { UndoRedoAction } from '~/lib' + +export const useUndoRedo = createSharedComposable(() => { + const undoQueue: Ref = ref([]) + + const redoQueue: Ref = ref([]) + + const addUndo = (action: UndoRedoAction) => { + undoQueue.value.push(action) + } + + const addRedo = (action: UndoRedoAction) => { + redoQueue.value.push(action) + } + + const undo = () => { + const action = undoQueue.value.pop() + if (action) { + action.undo.fn.apply(action, action.undo.args) + addRedo(action) + } + } + + const redo = () => { + const action = redoQueue.value.pop() + if (action) { + action.redo.fn.apply(action, action.redo.args) + addUndo(action) + } + } + + useEventListener(document, 'keydown', async (e: KeyboardEvent) => { + const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey + if (cmdOrCtrl && !e.altKey) { + switch (e.keyCode) { + case 90: { + // CMD + z and CMD + shift + z + if (!e.shiftKey) { + if (undoQueue.value.length) { + undo() + } + } else { + if (redoQueue.value.length) { + redo() + } + } + break + } + } + } + }) + + return { + addUndo, + undo, + clone, + } +}) diff --git a/packages/nc-gui/composables/useViewData.ts b/packages/nc-gui/composables/useViewData.ts index 4dc758f8ec..1f0bc270b8 100644 --- a/packages/nc-gui/composables/useViewData.ts +++ b/packages/nc-gui/composables/useViewData.ts @@ -12,6 +12,7 @@ import { message, populateInsertObject, ref, + rowPkData, storeToRefs, until, useApi, @@ -25,7 +26,7 @@ import { useSmartsheetStoreOrThrow, useUIPermission, } from '#imports' -import type { Row } from '~/lib' +import type { Row, UndoRedoAction } from '~/lib' const formatData = (list: Record[]) => list.map((row) => ({ @@ -55,6 +56,8 @@ export function useViewData( const { getMeta } = useMetas() + const { addUndo, clone } = useUndoRedo() + const appInfoDefaultLimit = appInfo.defaultLimit || 25 const _paginationData = ref({ page: 1, pageSize: appInfoDefaultLimit }) @@ -225,6 +228,7 @@ export function useViewData( currentRow: Row, ltarState: Record = {}, { metaValue = meta.value, viewMetaValue = viewMeta.value }: { metaValue?: TableType; viewMetaValue?: ViewType } = {}, + undo = false, ) { const row = currentRow.row if (currentRow.rowMeta) currentRow.rowMeta.saving = true @@ -234,6 +238,7 @@ export function useViewData( ltarState, getMeta, row, + undo, }) if (missingRequiredColumns.size) return @@ -246,6 +251,38 @@ export function useViewData( insertObj, ) + if (!undo) { + const id = extractPkFromRow(insertedData, metaValue?.columns as ColumnType[]) + + addUndo({ + redo: { + fn: async function redo( + this: UndoRedoAction, + row: Row, + ltarState: Record, + { metaValue, viewMetaValue }: { metaValue?: TableType; viewMetaValue?: ViewType }, + ) { + const pkData = rowPkData(row.row, metaValue?.columns as ColumnType[]) + row.row = { ...pkData, ...row.row } + await insertRow(row, ltarState, { metaValue, viewMetaValue }, true) + loadData() + }, + args: [clone(currentRow), clone(ltarState), clone({ metaValue, viewMetaValue })], + }, + undo: { + fn: async function undo( + this: UndoRedoAction, + id: string, + { metaValue, viewMetaValue }: { metaValue?: TableType; viewMetaValue?: ViewType } = {}, + ) { + await deleteRowById(id, { metaValue, viewMetaValue }) + loadData() + }, + args: [id, clone({ metaValue, viewMetaValue })], + }, + }) + } + Object.assign(currentRow, { row: { ...insertedData, ...row }, rowMeta: { ...(currentRow.rowMeta || {}), new: undefined }, @@ -267,6 +304,7 @@ export function useViewData( toUpdate: Row, property: string, { metaValue = meta.value, viewMetaValue = viewMeta.value }: { metaValue?: TableType; viewMetaValue?: ViewType } = {}, + undo = false, ) { if (toUpdate.rowMeta) toUpdate.rowMeta.saving = true @@ -297,25 +335,57 @@ export function useViewData( prev_value: getHTMLEncodedText(toUpdate.oldRow[property]), }) - /** 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( - toUpdate.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!] = updatedRowData[col.title!] - return acc - }, {} as Record), - ) - Object.assign(toUpdate.oldRow, updatedRowData) + if (!undo) { + addUndo({ + redo: { + fn: async function redo( + toUpdate: Row, + property: string, + { metaValue, viewMetaValue }: { metaValue?: TableType; viewMetaValue?: ViewType } = {}, + ) { + await updateRowProperty(toUpdate, property, { metaValue, viewMetaValue }, true) + }, + args: [clone(toUpdate), property, clone({ metaValue, viewMetaValue })], + }, + undo: { + fn: async function undo( + toUpdate: Row, + property: string, + { metaValue, viewMetaValue }: { metaValue?: TableType; viewMetaValue?: ViewType } = {}, + ) { + await updateRowProperty( + { row: toUpdate.oldRow, oldRow: toUpdate.row, rowMeta: toUpdate.rowMeta }, + property, + { metaValue, viewMetaValue }, + true, + ) + }, + args: [clone(toUpdate), property, clone({ metaValue, viewMetaValue })], + }, + }) + + /** 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( + toUpdate.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!] = updatedRowData[col.title!] + return acc + }, {} as Record), + ) + Object.assign(toUpdate.oldRow, updatedRowData) + } else { + loadData() + } } catch (e: any) { message.error(`${t('msg.error.rowUpdateFailed')} ${await extractSdkResponseErrorMsg(e)}`) } finally { @@ -354,7 +424,10 @@ export function useViewData( $e('a:grid:pagination') } - async function deleteRowById(id: string) { + async function deleteRowById( + id: string, + { metaValue = meta.value, viewMetaValue = viewMeta.value }: { metaValue?: TableType; viewMetaValue?: ViewType } = {}, + ) { if (!id) { throw new Error("Delete not allowed for table which doesn't have primary Key") } @@ -362,8 +435,8 @@ export function useViewData( const res: any = await $api.dbViewRow.delete( 'noco', project.value.id as string, - meta.value?.id as string, - viewMeta.value?.id as string, + metaValue?.id as string, + viewMetaValue?.id as string, id, ) @@ -378,7 +451,7 @@ export function useViewData( return true } - async function deleteRow(rowIndex: number) { + async function deleteRow(rowIndex: number, undo?: boolean) { try { const row = formattedData.value[rowIndex] if (!row.rowMeta.new) { @@ -387,6 +460,38 @@ export function useViewData( .map((c) => row.row[c.title!]) .join('___') + if (!undo) { + const metaValue = meta.value + const viewMetaValue = viewMeta.value + addUndo({ + redo: { + fn: async function undo( + this: UndoRedoAction, + id: string, + { metaValue, viewMetaValue }: { metaValue?: TableType; viewMetaValue?: ViewType } = {}, + ) { + await deleteRowById(id, { metaValue, viewMetaValue }) + loadData() + }, + args: [id, clone({ metaValue, viewMetaValue })], + }, + undo: { + fn: async function redo( + this: UndoRedoAction, + row: Row, + ltarState: Record, + { metaValue, viewMetaValue }: { metaValue?: TableType; viewMetaValue?: ViewType }, + ) { + const pkData = rowPkData(row.row, metaValue?.columns as ColumnType[]) + row.row = { ...pkData, ...row.row } + await insertRow(row, ltarState, { metaValue, viewMetaValue }, true) + loadData() + }, + args: [clone(row), {}, clone({ metaValue, viewMetaValue })], + }, + }) + } + const deleted = await deleteRowById(id as string) if (!deleted) { return diff --git a/packages/nc-gui/lib/types.ts b/packages/nc-gui/lib/types.ts index 9ec003091a..a3357a9533 100644 --- a/packages/nc-gui/lib/types.ts +++ b/packages/nc-gui/lib/types.ts @@ -104,3 +104,8 @@ export type importFileList = (UploadFile & { data: string | ArrayBuffer })[] export type streamImportFileList = UploadFile[] export type Nullable = { [K in keyof T]: T[K] | null } + +export interface UndoRedoAction { + undo: { fn: Function; args: any[] } + redo: { fn: Function; args: any[] } +} diff --git a/packages/nc-gui/utils/dataUtils.ts b/packages/nc-gui/utils/dataUtils.ts index b962b72bc0..7c716b3b84 100644 --- a/packages/nc-gui/utils/dataUtils.ts +++ b/packages/nc-gui/utils/dataUtils.ts @@ -11,6 +11,17 @@ export const extractPkFromRow = (row: Record, columns: ColumnType[] ) } +export const rowPkData = (row: Record, columns: ColumnType[]) => { + const pkData: Record = {} + const pks = columns?.filter((c) => c.pk) + if (row && pks && pks.length) { + for (const pk of pks) { + if (pk.title) pkData[pk.title] = row[pk.title] + } + } + return pkData +} + // a function to populate insert object and verify if all required fields are present export async function populateInsertObject({ getMeta, @@ -18,12 +29,14 @@ export async function populateInsertObject({ meta, ltarState, throwError, + undo = false, }: { meta: TableType ltarState: Record getMeta: (tableIdOrTitle: string, force?: boolean) => Promise row: Record throwError?: boolean + undo?: boolean }) { const missingRequiredColumns = new Set() const insertObj = await meta.columns?.reduce(async (_o: Promise, col) => { @@ -51,7 +64,7 @@ export async function populateInsertObject({ missingRequiredColumns.add(col.title) } - if (!col.ai && row?.[col.title as string] !== null) { + if ((!col.ai || undo) && row?.[col.title as string] !== null) { o[col.title as string] = row?.[col.title as string] } From ec84cc9eff33907c6064f844c20629cb42a1de99 Mon Sep 17 00:00:00 2001 From: mertmit Date: Sat, 11 Mar 2023 16:25:14 +0300 Subject: [PATCH 03/67] feat: undo/redo for LTAR Signed-off-by: mertmit --- packages/nc-gui/composables/useLTARStore.ts | 37 ++++++++++++++++++--- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/packages/nc-gui/composables/useLTARStore.ts b/packages/nc-gui/composables/useLTARStore.ts index 443492b65d..f726c4e744 100644 --- a/packages/nc-gui/composables/useLTARStore.ts +++ b/packages/nc-gui/composables/useLTARStore.ts @@ -43,6 +43,8 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState( const { $api } = useNuxtApp() + const { addUndo, clone } = useUndoRedo() + const sharedViewPassword = inject(SharedViewPasswordInj, ref(null)) const childrenExcludedList = ref() @@ -245,7 +247,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState( }) } - const unlink = async (row: Record) => { + const unlink = async (row: Record, { metaValue = meta.value }: { metaValue?: TableType } = {}, undo = false) => { // const column = meta.columns.find(c => c.id === this.column.colOptions.fk_child_column_id); // todo: handle if new record // if (this.isNew) { @@ -264,12 +266,26 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState( await $api.dbTableRow.nestedRemove( NOCO, project.value.title as string, - meta.value.title, + metaValue.title, rowId.value, colOptions.type as 'mm' | 'hm', encodeURIComponent(column?.value?.title), getRelatedTableRowId(row) as string, ) + + if (!undo) { + addUndo({ + redo: { + fn: (row: Record, { metaValue }: { metaValue?: TableType } = {}) => unlink(row, { metaValue }, true), + args: [clone(row), clone({ metaValue })], + }, + undo: { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + fn: (row: Record, { metaValue }: { metaValue?: TableType } = {}) => link(row, { metaValue }, true), + args: [clone(row), clone({ metaValue })], + }, + }) + } } catch (e: any) { message.error(`${t('msg.error.unlinkFailed')}: ${await extractSdkResponseErrorMsg(e)}`) } @@ -277,7 +293,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState( reloadData?.(false) } - const link = async (row: Record) => { + const link = async (row: Record, { metaValue = meta.value }: { metaValue?: TableType } = {}, undo = false) => { // todo: handle new record // const pid = this._extractRowId(parent, this.parentMeta); // const id = this._extractRowId(this.row, this.meta); @@ -295,13 +311,26 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState( await $api.dbTableRow.nestedAdd( NOCO, project.value.title as string, - meta.value.title as string, + metaValue.title as string, rowId.value, colOptions.type as 'mm' | 'hm', encodeURIComponent(column?.value?.title), getRelatedTableRowId(row) as string, ) await loadChildrenList() + + if (!undo) { + addUndo({ + redo: { + fn: (row: Record, { metaValue }: { metaValue?: TableType } = {}) => link(row, { metaValue }, true), + args: [clone(row), clone({ metaValue })], + }, + undo: { + fn: (row: Record, { metaValue }: { metaValue?: TableType } = {}) => unlink(row, { metaValue }, true), + args: [clone(row), clone({ metaValue })], + }, + }) + } } catch (e: any) { message.error(`Linking failed: ${await extractSdkResponseErrorMsg(e)}`) } From 8b68145d0dd7c11db28ee709a8ea815c1733ed3a Mon Sep 17 00:00:00 2001 From: mertmit Date: Sat, 11 Mar 2023 17:14:32 +0300 Subject: [PATCH 04/67] chore: migrate to rfdc as deep-clone lib Signed-off-by: mertmit --- .../composables/useColumnCreateStore.ts | 4 +++- packages/nc-gui/composables/useUndoRedo.ts | 4 +++- packages/nc-gui/just-clone-shims.d.ts | 1 - packages/nc-gui/package-lock.json | 22 +++++++++---------- packages/nc-gui/package.json | 2 +- 5 files changed, 18 insertions(+), 15 deletions(-) delete mode 100644 packages/nc-gui/just-clone-shims.d.ts diff --git a/packages/nc-gui/composables/useColumnCreateStore.ts b/packages/nc-gui/composables/useColumnCreateStore.ts index 8681cba111..978f71fdd4 100644 --- a/packages/nc-gui/composables/useColumnCreateStore.ts +++ b/packages/nc-gui/composables/useColumnCreateStore.ts @@ -1,4 +1,4 @@ -import clone from 'just-clone' +import rfdc from 'rfdc' import type { ColumnReqType, ColumnType, TableType } from 'nocodb-sdk' import { UITypes } from 'nocodb-sdk' import type { Ref } from 'vue' @@ -18,6 +18,8 @@ import { watch, } from '#imports' +const clone = rfdc() + const useForm = Form.useForm const columnToValidate = [UITypes.Email, UITypes.URL, UITypes.PhoneNumber] diff --git a/packages/nc-gui/composables/useUndoRedo.ts b/packages/nc-gui/composables/useUndoRedo.ts index 4866a8554f..54b731a097 100644 --- a/packages/nc-gui/composables/useUndoRedo.ts +++ b/packages/nc-gui/composables/useUndoRedo.ts @@ -1,9 +1,11 @@ import type { Ref } from 'vue' -import clone from 'just-clone' +import rfdc from 'rfdc' import { createSharedComposable, ref } from '#imports' import type { UndoRedoAction } from '~/lib' export const useUndoRedo = createSharedComposable(() => { + const clone = rfdc() + const undoQueue: Ref = ref([]) const redoQueue: Ref = ref([]) diff --git a/packages/nc-gui/just-clone-shims.d.ts b/packages/nc-gui/just-clone-shims.d.ts deleted file mode 100644 index 667284c0d6..0000000000 --- a/packages/nc-gui/just-clone-shims.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'just-clone' diff --git a/packages/nc-gui/package-lock.json b/packages/nc-gui/package-lock.json index d887465561..38d47acc61 100644 --- a/packages/nc-gui/package-lock.json +++ b/packages/nc-gui/package-lock.json @@ -25,7 +25,6 @@ "httpsnippet": "^2.0.0", "jsbarcode": "^3.11.5", "jsep": "^1.3.6", - "just-clone": "^6.1.1", "jwt-decode": "^3.1.2", "leaflet": "^1.9.2", "leaflet.markercluster": "^1.5.3", @@ -35,6 +34,7 @@ "papaparse": "^5.3.2", "pinia": "^2.0.33", "qrcode": "^1.5.1", + "rfdc": "^1.3.0", "socket.io-client": "^4.5.1", "sortablejs": "^1.15.0", "tinycolor2": "^1.4.2", @@ -10911,11 +10911,6 @@ "node": ">=4.0" } }, - "node_modules/just-clone": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/just-clone/-/just-clone-6.1.1.tgz", - "integrity": "sha512-V24KLIid8uaG7ayOymGfheNHtxgrbpzj1UznQnF9vQZMHlKGTSLT3WWmFx62OXSQPwk1Tn+uo+H5/Xhb4bL9pA==" - }, "node_modules/jwt-decode": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", @@ -14623,6 +14618,11 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -26325,11 +26325,6 @@ "object.assign": "^4.1.2" } }, - "just-clone": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/just-clone/-/just-clone-6.1.1.tgz", - "integrity": "sha512-V24KLIid8uaG7ayOymGfheNHtxgrbpzj1UznQnF9vQZMHlKGTSLT3WWmFx62OXSQPwk1Tn+uo+H5/Xhb4bL9pA==" - }, "jwt-decode": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", @@ -29067,6 +29062,11 @@ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" }, + "rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" + }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", diff --git a/packages/nc-gui/package.json b/packages/nc-gui/package.json index 282dd405b1..847017b499 100644 --- a/packages/nc-gui/package.json +++ b/packages/nc-gui/package.json @@ -49,7 +49,6 @@ "httpsnippet": "^2.0.0", "jsbarcode": "^3.11.5", "jsep": "^1.3.6", - "just-clone": "^6.1.1", "jwt-decode": "^3.1.2", "leaflet": "^1.9.2", "leaflet.markercluster": "^1.5.3", @@ -59,6 +58,7 @@ "papaparse": "^5.3.2", "pinia": "^2.0.33", "qrcode": "^1.5.1", + "rfdc": "^1.3.0", "socket.io-client": "^4.5.1", "sortablejs": "^1.15.0", "tinycolor2": "^1.4.2", From 4d6e3c0c949301958ae2e6a2e17fd23c6e8bc462 Mon Sep 17 00:00:00 2001 From: mertmit Date: Wed, 15 Mar 2023 17:06:38 +0300 Subject: [PATCH 05/67] feat: add scope to undo/redo Signed-off-by: mertmit --- packages/nc-gui/composables/useLTARStore.ts | 18 +++--- packages/nc-gui/composables/useUndoRedo.ts | 63 ++++++++++++++++++- packages/nc-gui/composables/useViewData.ts | 69 +++++++-------------- packages/nc-gui/lib/types.ts | 1 + 4 files changed, 93 insertions(+), 58 deletions(-) diff --git a/packages/nc-gui/composables/useLTARStore.ts b/packages/nc-gui/composables/useLTARStore.ts index f726c4e744..dd45503e24 100644 --- a/packages/nc-gui/composables/useLTARStore.ts +++ b/packages/nc-gui/composables/useLTARStore.ts @@ -276,14 +276,15 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState( if (!undo) { addUndo({ redo: { - fn: (row: Record, { metaValue }: { metaValue?: TableType } = {}) => unlink(row, { metaValue }, true), - args: [clone(row), clone({ metaValue })], + fn: (row: Record) => unlink(row, {}, true), + args: [clone(row)], }, undo: { // eslint-disable-next-line @typescript-eslint/no-use-before-define - fn: (row: Record, { metaValue }: { metaValue?: TableType } = {}) => link(row, { metaValue }, true), - args: [clone(row), clone({ metaValue })], + fn: (row: Record) => link(row, {}, true), + args: [clone(row)], }, + scope: metaValue.id, }) } } catch (e: any) { @@ -322,13 +323,14 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState( if (!undo) { addUndo({ redo: { - fn: (row: Record, { metaValue }: { metaValue?: TableType } = {}) => link(row, { metaValue }, true), - args: [clone(row), clone({ metaValue })], + fn: (row: Record) => link(row, {}, true), + args: [clone(row)], }, undo: { - fn: (row: Record, { metaValue }: { metaValue?: TableType } = {}) => unlink(row, { metaValue }, true), - args: [clone(row), clone({ metaValue })], + fn: (row: Record) => unlink(row, {}, true), + args: [clone(row)], }, + scope: metaValue.id, }) } } catch (e: any) { diff --git a/packages/nc-gui/composables/useUndoRedo.ts b/packages/nc-gui/composables/useUndoRedo.ts index 54b731a097..ebb0069a8b 100644 --- a/packages/nc-gui/composables/useUndoRedo.ts +++ b/packages/nc-gui/composables/useUndoRedo.ts @@ -1,11 +1,34 @@ import type { Ref } from 'vue' import rfdc from 'rfdc' -import { createSharedComposable, ref } from '#imports' +import { createSharedComposable, ref, useRouter } from '#imports' import type { UndoRedoAction } from '~/lib' export const useUndoRedo = createSharedComposable(() => { const clone = rfdc() + const router = useRouter() + + const route = $(router.currentRoute) + + const scope = computed(() => { + let tempScope = ['root'] + for (const param of Object.values(route.params)) { + if (Array.isArray(param)) { + tempScope = tempScope.concat(param) + } else { + tempScope.push(param) + } + } + if ( + Object.keys(route.params).includes('viewTitle') && + Object.keys(route.params).includes('title') && + route.params.viewTitle.length + ) { + tempScope.splice(tempScope.indexOf(route.params.title as string), 1) + } + return tempScope + }) + const undoQueue: Ref = ref([]) const redoQueue: Ref = ref([]) @@ -19,7 +42,24 @@ export const useUndoRedo = createSharedComposable(() => { } const undo = () => { - const action = undoQueue.value.pop() + let actionIndex = -1 + for (let i = undoQueue.value.length - 1; i >= 0; i--) { + if (Array.isArray(undoQueue.value[i].scope)) { + if (scope.value.some((s) => undoQueue.value[i].scope?.includes(s))) { + actionIndex = i + break + } + } else { + if (scope.value.includes((undoQueue.value[i].scope as string) || 'root')) { + actionIndex = i + break + } + } + } + + if (actionIndex === -1) return + + const action = undoQueue.value.splice(actionIndex, 1)[0] if (action) { action.undo.fn.apply(action, action.undo.args) addRedo(action) @@ -27,7 +67,24 @@ export const useUndoRedo = createSharedComposable(() => { } const redo = () => { - const action = redoQueue.value.pop() + let actionIndex = -1 + for (let i = redoQueue.value.length - 1; i >= 0; i--) { + if (Array.isArray(redoQueue.value[i].scope)) { + if (scope.value.some((s) => redoQueue.value[i].scope?.includes(s))) { + actionIndex = i + break + } + } else { + if (scope.value.includes((redoQueue.value[i].scope as string) || 'root')) { + actionIndex = i + break + } + } + } + + if (actionIndex === -1) return + + const action = redoQueue.value.splice(actionIndex, 1)[0] if (action) { action.redo.fn.apply(action, action.redo.args) addUndo(action) diff --git a/packages/nc-gui/composables/useViewData.ts b/packages/nc-gui/composables/useViewData.ts index 1f0bc270b8..66ad3dfa7f 100644 --- a/packages/nc-gui/composables/useViewData.ts +++ b/packages/nc-gui/composables/useViewData.ts @@ -256,30 +256,22 @@ export function useViewData( addUndo({ redo: { - fn: async function redo( - this: UndoRedoAction, - row: Row, - ltarState: Record, - { metaValue, viewMetaValue }: { metaValue?: TableType; viewMetaValue?: ViewType }, - ) { + fn: async function redo(this: UndoRedoAction, row: Row, ltarState: Record) { const pkData = rowPkData(row.row, metaValue?.columns as ColumnType[]) row.row = { ...pkData, ...row.row } - await insertRow(row, ltarState, { metaValue, viewMetaValue }, true) + await insertRow(row, ltarState, undefined, true) loadData() }, - args: [clone(currentRow), clone(ltarState), clone({ metaValue, viewMetaValue })], + args: [clone(currentRow), clone(ltarState)], }, undo: { - fn: async function undo( - this: UndoRedoAction, - id: string, - { metaValue, viewMetaValue }: { metaValue?: TableType; viewMetaValue?: ViewType } = {}, - ) { - await deleteRowById(id, { metaValue, viewMetaValue }) + fn: async function undo(this: UndoRedoAction, id: string) { + await deleteRowById(id) loadData() }, - args: [id, clone({ metaValue, viewMetaValue })], + args: [id], }, + scope: viewMeta.value?.is_default ? [viewMeta.value.fk_model_id, viewMeta.value.title] : viewMeta.value?.title, }) } @@ -338,30 +330,23 @@ export function useViewData( if (!undo) { addUndo({ redo: { - fn: async function redo( - toUpdate: Row, - property: string, - { metaValue, viewMetaValue }: { metaValue?: TableType; viewMetaValue?: ViewType } = {}, - ) { - await updateRowProperty(toUpdate, property, { metaValue, viewMetaValue }, true) + fn: async function redo(toUpdate: Row, property: string) { + await updateRowProperty(toUpdate, property, undefined, true) }, - args: [clone(toUpdate), property, clone({ metaValue, viewMetaValue })], + args: [clone(toUpdate), property], }, undo: { - fn: async function undo( - toUpdate: Row, - property: string, - { metaValue, viewMetaValue }: { metaValue?: TableType; viewMetaValue?: ViewType } = {}, - ) { + fn: async function undo(toUpdate: Row, property: string) { await updateRowProperty( { row: toUpdate.oldRow, oldRow: toUpdate.row, rowMeta: toUpdate.rowMeta }, property, - { metaValue, viewMetaValue }, + undefined, true, ) }, - args: [clone(toUpdate), property, clone({ metaValue, viewMetaValue })], + args: [clone(toUpdate), property], }, + scope: viewMeta.value?.is_default ? [viewMeta.value.fk_model_id, viewMeta.value.title] : viewMeta.value?.title, }) /** update row data(to sync formula and other related columns) @@ -461,34 +446,24 @@ export function useViewData( .join('___') if (!undo) { - const metaValue = meta.value - const viewMetaValue = viewMeta.value addUndo({ redo: { - fn: async function undo( - this: UndoRedoAction, - id: string, - { metaValue, viewMetaValue }: { metaValue?: TableType; viewMetaValue?: ViewType } = {}, - ) { - await deleteRowById(id, { metaValue, viewMetaValue }) + fn: async function undo(this: UndoRedoAction, id: string) { + await deleteRowById(id) loadData() }, - args: [id, clone({ metaValue, viewMetaValue })], + args: [id], }, undo: { - fn: async function redo( - this: UndoRedoAction, - row: Row, - ltarState: Record, - { metaValue, viewMetaValue }: { metaValue?: TableType; viewMetaValue?: ViewType }, - ) { - const pkData = rowPkData(row.row, metaValue?.columns as ColumnType[]) + fn: async function redo(this: UndoRedoAction, row: Row, ltarState: Record) { + const pkData = rowPkData(row.row, meta.value?.columns as ColumnType[]) row.row = { ...pkData, ...row.row } - await insertRow(row, ltarState, { metaValue, viewMetaValue }, true) + await insertRow(row, ltarState, {}, true) loadData() }, - args: [clone(row), {}, clone({ metaValue, viewMetaValue })], + args: [clone(row), {}], }, + scope: viewMeta.value?.is_default ? [viewMeta.value.fk_model_id, viewMeta.value.title] : viewMeta.value?.title, }) } diff --git a/packages/nc-gui/lib/types.ts b/packages/nc-gui/lib/types.ts index a3357a9533..daf81406e4 100644 --- a/packages/nc-gui/lib/types.ts +++ b/packages/nc-gui/lib/types.ts @@ -108,4 +108,5 @@ export type Nullable = { [K in keyof T]: T[K] | null } export interface UndoRedoAction { undo: { fn: Function; args: any[] } redo: { fn: Function; args: any[] } + scope?: string | string[] } From 8cd87f92b7d08f64a8b836835a10fee378ce70fc Mon Sep 17 00:00:00 2001 From: mertmit Date: Wed, 15 Mar 2023 18:15:58 +0300 Subject: [PATCH 06/67] feat: undo/redo fields menu Signed-off-by: mertmit --- .../smartsheet/toolbar/FieldsMenu.vue | 83 ++++++++++++++++++- 1 file changed, 80 insertions(+), 3 deletions(-) diff --git a/packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue b/packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue index 22d767530b..bd99475cb7 100644 --- a/packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue +++ b/packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue @@ -3,6 +3,7 @@ import type { ColumnType, GalleryType, KanbanType } from 'nocodb-sdk' import { UITypes, ViewTypes, isVirtualCol } from 'nocodb-sdk' import Draggable from 'vuedraggable' import type { SelectProps } from 'ant-design-vue' +import type { CheckboxChangeEvent } from 'ant-design-vue/es/checkbox/interface' import { ActiveViewInj, FieldsInj, @@ -18,6 +19,7 @@ import { useMenuCloseOnEsc, useNuxtApp, useSmartsheetStoreOrThrow, + useUndoRedo, useViewColumns, watch, } from '#imports' @@ -55,6 +57,8 @@ const { const { eventBus } = useSmartsheetStoreOrThrow() +const { addUndo } = useUndoRedo() + eventBus.on((event) => { if (event === SmartsheetStoreEvents.FIELD_RELOAD) { loadViewColumns() @@ -79,10 +83,44 @@ const gridDisplayValueField = computed(() => { return filteredFieldList.value?.find((field) => field.fk_column_id === pvCol?.id) }) -const onMove = (_event: { moved: { newIndex: number } }) => { +const onMove = (_event: { moved: { newIndex: number; oldIndex: number } }, undo = false) => { // todo : sync with server if (!fields.value) return + if (!undo) { + addUndo({ + undo: { + fn: () => { + if (!fields.value) return + const temp = fields.value[_event.moved.newIndex] + fields.value[_event.moved.newIndex] = fields.value[_event.moved.oldIndex] + fields.value[_event.moved.oldIndex] = temp + onMove( + { + moved: { + newIndex: _event.moved.oldIndex, + oldIndex: _event.moved.newIndex, + }, + }, + true, + ) + }, + args: [], + }, + redo: { + fn: () => { + if (!fields.value) return + const temp = fields.value[_event.moved.oldIndex] + fields.value[_event.moved.oldIndex] = fields.value[_event.moved.newIndex] + fields.value[_event.moved.newIndex] = temp + onMove(_event, true) + }, + args: [], + }, + scope: activeView.value?.is_default ? [activeView.value.fk_model_id, activeView.value.title] : activeView.value?.title, + }) + } + if (fields.value.length < 2) return fields.value.forEach((field, index) => { @@ -149,6 +187,45 @@ const getIcon = (c: ColumnType) => const open = ref(false) +const toggleFieldVisibility = (e: CheckboxChangeEvent, field: any, index: number) => { + addUndo({ + undo: { + fn: (v: boolean) => { + field.show = !v + saveOrUpdate(field, index) + }, + args: [e.target.checked], + }, + redo: { + fn: (v: boolean) => { + field.show = v + saveOrUpdate(field, index) + }, + args: [e.target.checked], + }, + scope: activeView.value?.is_default ? [activeView.value.fk_model_id, activeView.value.title] : activeView.value?.title, + }) + saveOrUpdate(field, index) +} + +const toggleSystemFields = (e: CheckboxChangeEvent) => { + addUndo({ + undo: { + fn: (v: boolean) => { + showSystemFields.value = !v + }, + args: [e.target.checked], + }, + redo: { + fn: (v: boolean) => { + showSystemFields.value = v + }, + args: [e.target.checked], + }, + scope: activeView.value?.is_default ? [activeView.value.fk_model_id, activeView.value.title] : activeView.value?.title, + }) +} + useMenuCloseOnEsc(open) @@ -208,7 +285,7 @@ useMenuCloseOnEsc(open) v-e="['a:fields:show-hide']" class="shrink" :disabled="field.isViewEssentialField" - @change="saveOrUpdate(field, index)" + @change="toggleFieldVisibility($event, field, index)" >
@@ -253,7 +330,7 @@ useMenuCloseOnEsc(open)
- + {{ $t('activity.showSystemFields') }}
From 6a21b2e7216a9e3acb189818df3df6be8d574eb6 Mon Sep 17 00:00:00 2001 From: mertmit Date: Thu, 16 Mar 2023 20:01:45 +0300 Subject: [PATCH 07/67] fix: local mode fields for shared base Signed-off-by: mertmit --- packages/nc-gui/composables/useViewColumns.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/nc-gui/composables/useViewColumns.ts b/packages/nc-gui/composables/useViewColumns.ts index 65f5abbb7b..c71f4ef0d8 100644 --- a/packages/nc-gui/composables/useViewColumns.ts +++ b/packages/nc-gui/composables/useViewColumns.ts @@ -25,6 +25,8 @@ export function useViewColumns( () => isPublic.value || !isUIAllowed('hideAllColumns') || !isUIAllowed('showAllColumns') || isSharedBase.value, ) + const localChanges = ref([]) + const isColumnViewEssential = (column: ColumnType) => { // TODO: consider at some point ti delegate this via a cleaner design pattern to view specific check logic // which could be inside of a view specific helper class (and generalized via an interface) @@ -76,6 +78,16 @@ export function useViewColumns( } }) .sort((a: Field, b: Field) => a.order - b.order) + + if (isLocalMode.value && fields.value) { + for (const field of localChanges.value) { + const fieldIndex = fields.value.findIndex((f) => f.fk_column_id === field.fk_column_id) + if (fieldIndex !== undefined && fieldIndex > -1) { + fields.value[fieldIndex] = field + fields.value = fields.value.sort((a: Field, b: Field) => a.order - b.order) + } + } + } } } @@ -128,20 +140,24 @@ export function useViewColumns( } const saveOrUpdate = async (field: any, index: number) => { - if (isPublic.value && fields.value) { + if (isLocalMode.value && fields.value && !isUIAllowed('fieldsSync')) { fields.value[index] = field meta.value!.columns = meta.value!.columns?.map((column: ColumnType) => { if (column.id === field.fk_column_id) { return { ...column, ...field, + id: field.fk_column_id, } } return column }) + localChanges.value.push(field) + await loadViewColumns() reloadData?.() + return } if (isUIAllowed('fieldsSync')) { From d1caec323b869fc01fe12502bf61f9eeea0b9153 Mon Sep 17 00:00:00 2001 From: mertmit Date: Fri, 17 Mar 2023 22:08:51 +0300 Subject: [PATCH 08/67] fix: prevent default undo event Signed-off-by: mertmit --- packages/nc-gui/composables/useUndoRedo.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/nc-gui/composables/useUndoRedo.ts b/packages/nc-gui/composables/useUndoRedo.ts index ebb0069a8b..a13318caa4 100644 --- a/packages/nc-gui/composables/useUndoRedo.ts +++ b/packages/nc-gui/composables/useUndoRedo.ts @@ -96,6 +96,7 @@ export const useUndoRedo = createSharedComposable(() => { if (cmdOrCtrl && !e.altKey) { switch (e.keyCode) { case 90: { + e.preventDefault() // CMD + z and CMD + shift + z if (!e.shiftKey) { if (undoQueue.value.length) { From 43aae02016500963927664eee084c53b669dd2e1 Mon Sep 17 00:00:00 2001 From: mertmit Date: Fri, 17 Mar 2023 22:23:09 +0300 Subject: [PATCH 09/67] fix: add default value to row height injection Signed-off-by: mertmit --- packages/nc-gui/components/cell/ClampedText.vue | 2 +- packages/nc-gui/components/cell/Text.vue | 5 ++++- packages/nc-gui/components/cell/TextArea.vue | 5 ++++- packages/nc-gui/components/virtual-cell/QrCode.vue | 6 +++++- packages/nc-gui/components/virtual-cell/barcode/Barcode.vue | 5 ++++- 5 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/nc-gui/components/cell/ClampedText.vue b/packages/nc-gui/components/cell/ClampedText.vue index e6b4fa3eb0..515a95bed3 100644 --- a/packages/nc-gui/components/cell/ClampedText.vue +++ b/packages/nc-gui/components/cell/ClampedText.vue @@ -31,7 +31,7 @@ onMounted(() => { :key="`clamp-${key}-${props.value?.toString().length || 0}`" class="w-full h-full break-word" :text="`${props.value || ' '}`" - :max-lines="props.lines" + :max-lines="props.lines || 1" />
diff --git a/packages/nc-gui/components/cell/Text.vue b/packages/nc-gui/components/cell/Text.vue index 1f744070f3..05fcbc4856 100644 --- a/packages/nc-gui/components/cell/Text.vue +++ b/packages/nc-gui/components/cell/Text.vue @@ -14,7 +14,10 @@ const { showNull } = useGlobal() const editEnabled = inject(EditModeInj) -const rowHeight = inject(RowHeightInj) +const rowHeight = inject( + RowHeightInj, + computed(() => undefined), +) const readonly = inject(ReadonlyInj, ref(false)) diff --git a/packages/nc-gui/components/cell/TextArea.vue b/packages/nc-gui/components/cell/TextArea.vue index 21ebfd8e0b..859b495bbd 100644 --- a/packages/nc-gui/components/cell/TextArea.vue +++ b/packages/nc-gui/components/cell/TextArea.vue @@ -10,7 +10,10 @@ const emits = defineEmits(['update:modelValue']) const editEnabled = inject(EditModeInj) -const rowHeight = inject(RowHeightInj) +const rowHeight = inject( + RowHeightInj, + computed(() => undefined), +) const { showNull } = useGlobal() diff --git a/packages/nc-gui/components/virtual-cell/QrCode.vue b/packages/nc-gui/components/virtual-cell/QrCode.vue index e4c59a21e4..9835d4174c 100644 --- a/packages/nc-gui/components/virtual-cell/QrCode.vue +++ b/packages/nc-gui/components/virtual-cell/QrCode.vue @@ -21,7 +21,11 @@ const qrCodeOptions: QRCode.QRCodeToDataURLOptions = { quality: 1, }, } -const rowHeight = inject(RowHeightInj) + +const rowHeight = inject( + RowHeightInj, + computed(() => undefined), +) const qrCode = useQRCode(qrValue, { ...qrCodeOptions, diff --git a/packages/nc-gui/components/virtual-cell/barcode/Barcode.vue b/packages/nc-gui/components/virtual-cell/barcode/Barcode.vue index 6aceb6cc90..8cf6de8b78 100644 --- a/packages/nc-gui/components/virtual-cell/barcode/Barcode.vue +++ b/packages/nc-gui/components/virtual-cell/barcode/Barcode.vue @@ -32,7 +32,10 @@ const showBarcode = computed(() => barcodeValue?.value.length > 0 && !tooManyCha const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning } = useShowNotEditableWarning() -const rowHeight = inject(RowHeightInj) +const rowHeight = inject( + RowHeightInj, + computed(() => undefined), +) - +