Browse Source

feat: undo/redo for row changes

Signed-off-by: mertmit <mertmit99@gmail.com>
pull/5332/head
mertmit 2 years ago
parent
commit
c8e1fc6c04
  1. 61
      packages/nc-gui/composables/useUndoRedo.ts
  2. 153
      packages/nc-gui/composables/useViewData.ts
  3. 5
      packages/nc-gui/lib/types.ts
  4. 15
      packages/nc-gui/utils/dataUtils.ts

61
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<UndoRedoAction[]> = ref([])
const redoQueue: Ref<UndoRedoAction[]> = 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,
}
})

153
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<string, any>[]) =>
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<PaginatedType>({ page: 1, pageSize: appInfoDefaultLimit })
@ -225,6 +228,7 @@ export function useViewData(
currentRow: Row,
ltarState: Record<string, any> = {},
{ 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<string, any>,
{ 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<Record<string, any>>((acc: Record<string, any>, 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<string, any>),
)
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<Record<string, any>>((acc: Record<string, any>, 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<string, any>),
)
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<string, any>,
{ 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

5
packages/nc-gui/lib/types.ts

@ -104,3 +104,8 @@ export type importFileList = (UploadFile & { data: string | ArrayBuffer })[]
export type streamImportFileList = UploadFile[]
export type Nullable<T> = { [K in keyof T]: T[K] | null }
export interface UndoRedoAction {
undo: { fn: Function; args: any[] }
redo: { fn: Function; args: any[] }
}

15
packages/nc-gui/utils/dataUtils.ts

@ -11,6 +11,17 @@ export const extractPkFromRow = (row: Record<string, any>, columns: ColumnType[]
)
}
export const rowPkData = (row: Record<string, any>, columns: ColumnType[]) => {
const pkData: Record<string, string> = {}
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<string, any>
getMeta: (tableIdOrTitle: string, force?: boolean) => Promise<TableType | null>
row: Record<string, any>
throwError?: boolean
undo?: boolean
}) {
const missingRequiredColumns = new Set()
const insertObj = await meta.columns?.reduce(async (_o: Promise<any>, 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]
}

Loading…
Cancel
Save