mirror of https://github.com/nocodb/nocodb
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
201 lines
5.8 KiB
201 lines
5.8 KiB
import type { Ref } from 'vue' |
|
import rfdc from 'rfdc' |
|
import type { BaseType, TableType, ViewType } from 'nocodb-sdk' |
|
|
|
export const useUndoRedo = createSharedComposable(() => { |
|
const clone = rfdc() |
|
|
|
const router = useRouter() |
|
|
|
const route = router.currentRoute |
|
|
|
// keys: baseType | baseId | type | title | viewTitle |
|
const scope = computed<{ key: string; param: string }[]>(() => { |
|
const tempScope: { key: string; param: string }[] = [{ key: 'root', param: 'root' }] |
|
for (const [key, param] of Object.entries(route.value.params)) { |
|
if (Array.isArray(param)) { |
|
tempScope.push({ key, param: param.join(',') }) |
|
} else { |
|
tempScope.push({ key, param }) |
|
} |
|
} |
|
|
|
return tempScope |
|
}) |
|
|
|
const isSameScope = (sc: { key: string; param: string | string[] }[]) => { |
|
// TODO - improve this logic |
|
// for now we disable undo/redo for webhook, field, api, relation |
|
const slugs = scope.value.find((s) => s.key === 'slugs') |
|
if (slugs) { |
|
const params = Array.isArray(slugs.param) ? slugs.param : slugs.param.split(',') |
|
if (params.some((el) => ['webhook', 'field', 'api', 'relation'].includes(el))) { |
|
return false |
|
} |
|
} |
|
|
|
return sc.every((s) => { |
|
return scope.value.some( |
|
// viewTitle is optional for default view |
|
(s2) => { |
|
if (Array.isArray(s.param)) { |
|
return ( |
|
(s.key === 'viewTitle' && s2.key === 'viewTitle' && s2.param === '') || |
|
(s.key === s2.key && s.param.includes(s2.param)) |
|
) |
|
} else { |
|
return ( |
|
(s.key === 'viewTitle' && s2.key === 'viewTitle' && s2.param === '') || (s.key === s2.key && s.param === s2.param) |
|
) |
|
} |
|
}, |
|
) |
|
}) |
|
} |
|
|
|
const undoQueue: Ref<UndoRedoAction[]> = ref([]) |
|
|
|
const redoQueue: Ref<UndoRedoAction[]> = ref([]) |
|
|
|
const addUndo = (action: UndoRedoAction, fromRedo = false) => { |
|
// remove all redo actions that are in the same scope |
|
if (!fromRedo) redoQueue.value = redoQueue.value.filter((a) => !isSameScope(a.scope || [])) |
|
undoQueue.value.push(action) |
|
} |
|
|
|
const addRedo = (action: UndoRedoAction) => { |
|
redoQueue.value.push(action) |
|
} |
|
|
|
const undo = async () => { |
|
let actionIndex = -1 |
|
for (let i = undoQueue.value.length - 1; i >= 0; i--) { |
|
const elScope = undoQueue.value[i].scope || [{ key: 'root', param: 'root' }] |
|
if (isSameScope(elScope)) { |
|
actionIndex = i |
|
break |
|
} |
|
} |
|
|
|
if (actionIndex === -1) return |
|
|
|
const action = undoQueue.value.splice(actionIndex, 1)[0] |
|
if (action) { |
|
try { |
|
await action.undo.fn.apply(action, action.undo.args) |
|
addRedo(action) |
|
} catch (e) { |
|
message.warn('Error while undoing action, it is skipped.') |
|
} |
|
} |
|
} |
|
|
|
const redo = async () => { |
|
let actionIndex = -1 |
|
for (let i = redoQueue.value.length - 1; i >= 0; i--) { |
|
const elScope = redoQueue.value[i].scope || [{ key: 'root', param: 'root' }] |
|
if (isSameScope(elScope)) { |
|
actionIndex = i |
|
break |
|
} |
|
} |
|
|
|
if (actionIndex === -1) return |
|
|
|
const action = redoQueue.value.splice(actionIndex, 1)[0] |
|
if (action) { |
|
try { |
|
await action.redo.fn.apply(action, action.redo.args) |
|
addUndo(action, true) |
|
} catch (e) { |
|
message.warn('Error while redoing action, it is skipped.') |
|
} |
|
} |
|
} |
|
|
|
const defineRootScope = () => { |
|
return [{ key: 'root', param: 'root' }] |
|
} |
|
|
|
const defineProjectScope = (param: { base?: BaseType; model?: TableType; view?: ViewType; base_id?: string }) => { |
|
if (param.base) { |
|
return [{ key: 'baseId', param: param.base.id! }] |
|
} else if (param.model) { |
|
return [{ key: 'baseId', param: param.model.base_id! }] |
|
} else if (param.view) { |
|
return [{ key: 'baseId', param: param.view.base_id! }] |
|
} else { |
|
return [{ key: 'baseId', param: param.base_id! }] |
|
} |
|
} |
|
|
|
const defineModelScope = (param: { model?: TableType; view?: ViewType; base_id?: string; model_id?: string }) => { |
|
if (param.model) { |
|
return [ |
|
{ key: 'baseId', param: param.model.base_id! }, |
|
{ key: 'viewId', param: param.model.id! }, |
|
] |
|
} else if (param.view) { |
|
return [ |
|
{ key: 'baseId', param: param.view.base_id! }, |
|
{ key: 'viewId', param: param.view.fk_model_id! }, |
|
] |
|
} else { |
|
return [ |
|
{ key: 'baseId', param: param.base_id! }, |
|
{ key: 'viewId', param: param.model_id! }, |
|
] |
|
} |
|
} |
|
|
|
const defineViewScope = (param: { view?: ViewType; base_id?: string; model_id?: string; title?: string; id?: string }) => { |
|
if (param.view) { |
|
return [ |
|
{ key: 'baseId', param: param.view.base_id! }, |
|
{ key: 'viewId', param: param.view.fk_model_id! }, |
|
{ key: 'viewTitle', param: [param.view.title, param.view.id!] }, |
|
] |
|
} else { |
|
return [ |
|
{ key: 'baseId', param: param.base_id! }, |
|
{ key: 'viewId', param: param.model_id! }, |
|
{ key: 'viewTitle', param: [param.title!, param.id!] }, |
|
] |
|
} |
|
} |
|
|
|
useEventListener(document, 'keydown', async (e: KeyboardEvent) => { |
|
const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey |
|
|
|
if ((e && (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement)) || isExpandedFormOpen()) return |
|
|
|
if (cmdOrCtrl && !e.altKey) { |
|
switch (e.keyCode) { |
|
case 90: { |
|
e.preventDefault() |
|
// 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, |
|
defineRootScope, |
|
defineProjectScope, |
|
defineModelScope, |
|
defineViewScope, |
|
} |
|
})
|
|
|