Skip to content
This repository was archived by the owner on Jul 8, 2025. It is now read-only.

Commit d26e34b

Browse files
committed
feat(editor): supporting time-traveling
1 parent 669e6a5 commit d26e34b

File tree

21 files changed

+147
-67
lines changed

21 files changed

+147
-67
lines changed

packages/editor/src/components/Header/OperationBar/Items/SaveAndHistory.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,16 @@ function ISaveAndHistory() {
5151
<OperationItem title={t('copy')} icon={FiCopy} action={unImplemented} />
5252
<OperationItem
5353
title={
54-
debugPort ? t('{{type}} not allowed with DebugMode', { type: 'save as template' }) : t('save as template')
54+
debugPort ? t('{{type}} not allowed with debug-mode', { type: 'save as template' }) : t('save as template')
5555
}
5656
icon={FiFilePlus}
5757
action={unImplemented}
5858
disabled={!!debugPort}
5959
/>
6060
<OperationItem
61-
title={debugPort ? t('{{type}} not allowed with DebugMode', { type: 'history manager' }) : t('history manager')}
61+
title={
62+
debugPort ? t('{{type}} not allowed with debug-mode', { type: 'history manager' }) : t('history manager')
63+
}
6264
icon={FiGitMerge}
6365
action={unImplemented}
6466
disabled={!!debugPort}

packages/editor/src/components/Header/OperationBar/Items/UndoAndClear.tsx

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as React from 'react';
22
import { observer } from 'mobx-react';
3-
import { useEffect } from 'react';
3+
import { useCallback, useEffect } from 'react';
4+
import { message } from 'antd';
45
import { FiCornerUpLeft, FiCornerUpRight, FiX } from 'react-icons/fi';
56
import { Trans, useTranslation } from 'react-i18next';
67
import { unImplemented } from 'utils';
@@ -11,7 +12,37 @@ import { hotKeyPrefix } from './utils';
1112

1213
function IUndoAndClear() {
1314
const { t } = useTranslation();
14-
const { canUndo, canRedo, undo, redo } = timeTraveler;
15+
const { canUndo, canRedo, undo: un, redo: re } = timeTraveler;
16+
17+
const undo = useCallback(() => {
18+
if (!timeTraveler.canUndo) {
19+
return;
20+
}
21+
message.destroy();
22+
message.open({
23+
type: 'info',
24+
content: 'undo',
25+
icon: <FiCornerUpLeft />,
26+
duration: 1,
27+
className: 'undo-message',
28+
});
29+
un();
30+
}, []);
31+
32+
const redo = useCallback(() => {
33+
if (!timeTraveler.canRedo) {
34+
return;
35+
}
36+
message.destroy();
37+
message.open({
38+
type: 'info',
39+
content: 'redo',
40+
icon: <FiCornerUpRight />,
41+
duration: 1,
42+
className: 'redo-message',
43+
});
44+
re();
45+
}, []);
1546

1647
useEffect(() => {
1748
hotkeyEvents.only(HotKeyEventTypes.UNDO, undo);

packages/editor/src/components/Header/OperationBar/index.scss

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151
}
5252

5353
.vize-editor-operation-item-tooltip {
54+
max-width: 400px !important;
55+
5456
.ant-tooltip-inner {
5557
& > p {
5658
margin: 0;
@@ -109,3 +111,19 @@
109111
}
110112
}
111113
}
114+
115+
.undo-message,
116+
.redo-message {
117+
.ant-message-notice-content {
118+
background-color: rgba(0, 0, 0, 0.75);
119+
color: white;
120+
}
121+
122+
.ant-message-info > svg {
123+
width: 16px;
124+
height: auto;
125+
margin-right: 4px;
126+
position: relative;
127+
top: 2px;
128+
}
129+
}

packages/editor/src/i18n/resources/zh.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export const zh = {
8383
save: '保存',
8484
'save as template': '存为模板',
8585
'history manager': '历史管理',
86-
'{{type}} not allowed with DebugMode': 'Debug 模式下不支持 $t({{type}})',
86+
'{{type}} not allowed with debug-mode': 'Debug 模式下不支持 $t({{type}})',
8787
saving: '保存中',
8888
saved: '保存成功',
8989
'failed to save': '保存失败',

packages/editor/src/libs/history/configure.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { TimeTravelerConfig } from './types';
22

33
export const config: TimeTravelerConfig = {
44
maxStacks: 20,
5+
debounceTime: 2000,
56
};
67

78
export function configure(newConfig: Partial<TimeTravelerConfig>) {

packages/editor/src/libs/history/decorators.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export const withSnapshot = (payload?: Record<string, any>) => (
3333
if (!inUpdating) {
3434
updating = true;
3535
}
36-
const result = initializer.apply(this).apply(this, args);
36+
const result = initializer.call(target).apply(target, args);
3737
if (!inUpdating) {
3838
setTimeout(async () => {
3939
if (result instanceof Promise) {
@@ -45,6 +45,7 @@ export const withSnapshot = (payload?: Record<string, any>) => (
4545
}
4646
return result;
4747
};
48+
return descriptor as any;
4849
};
4950

5051
interface ActionWithSnapshot {
@@ -60,12 +61,12 @@ export const actionWithSnapshot: ActionWithSnapshot = (
6061
if (!propertyKey) {
6162
const payload = payloadOrTarget as Record<string, any>;
6263
return (target: object, propertyKey: string, descriptor?: PropertyDescriptor) => {
63-
withSnapshot(payload)(target, propertyKey, descriptor);
64-
return action(target, propertyKey, descriptor);
64+
withSnapshot.call(target, payload)(target, propertyKey, descriptor);
65+
return action.call(target, target, propertyKey, descriptor);
6566
};
6667
}
6768

6869
const target = payloadOrTarget as object;
69-
withSnapshot(undefined)(target, propertyKey, descriptor);
70-
return action(target, propertyKey, descriptor) as any;
70+
withSnapshot.call(target)(target, propertyKey, descriptor);
71+
return action.call(target, target, propertyKey, descriptor) as any;
7172
};

packages/editor/src/libs/history/record.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Snapshots } from './types';
2-
import { runInAction } from 'mobx';
2+
import { observable, runInAction } from 'mobx';
33

44
export const recordedStores: Snapshots = {};
55

packages/editor/src/libs/history/timeTraveling.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1-
import { action, computed, observable } from 'mobx';
1+
import { action, computed, observable, IObservableValue } from 'mobx';
22
import { RestoreCallback, Snapshots } from './types';
33
import { recordedStores, restoreSnapshot } from './record';
44
import { dehydrate } from './hydrate';
5+
import { config } from './configure';
56

67
export class TimeTraveler {
78
@observable
8-
private stacks: Snapshots[] = [];
9+
private stacks: IObservableValue<Snapshots>[] = [];
910

1011
@observable
1112
private cursor = 0;
1213

14+
private lastUpdate = 0;
15+
1316
public getSnapshots = (): Snapshots => {
1417
return Object.entries(recordedStores).reduce<Snapshots>((accu, [k, v]) => {
1518
accu[k] = dehydrate(v);
@@ -23,19 +26,32 @@ export class TimeTraveler {
2326
if (payload) {
2427
item.payload = payload;
2528
}
26-
this.stacks = [item];
29+
this.stacks = [observable.box(item, { deep: false })];
2730
this.cursor = 0;
2831
};
2932

3033
@action
3134
public updateSnapshots = (payload?: Record<string, any>, snapshots?: Snapshots) => {
32-
const { stacks } = this;
35+
const { stacks, cursor } = this;
3336
const item = snapshots || this.getSnapshots();
3437
if (payload) {
3538
item.payload = payload;
3639
}
37-
stacks.push(item);
40+
41+
if (cursor < stacks.length - 1) {
42+
stacks.splice(cursor + 1, stacks.length - cursor);
43+
}
44+
45+
const boxedItem = observable.box(item, { deep: false });
46+
const debounce = Date.now() - this.lastUpdate < config.debounceTime;
47+
if (debounce) {
48+
stacks[stacks.length - 1] = boxedItem;
49+
} else {
50+
stacks.push(boxedItem);
51+
}
52+
3853
this.cursor = stacks.length - 1;
54+
this.lastUpdate = Date.now();
3955
};
4056

4157
@computed
@@ -56,8 +72,8 @@ export class TimeTraveler {
5672
if (!this.canUndo) {
5773
return;
5874
}
59-
const currentSnapshots = stacks[cursor];
60-
const snapshots = stacks[cursor - 1];
75+
const currentSnapshots = stacks[cursor].get();
76+
const snapshots = stacks[cursor - 1].get();
6177
this.cursor -= 1;
6278
return restoreSnapshot(snapshots, () => {
6379
this.restoreCallbacks.forEach(callback => callback('undo', snapshots, currentSnapshots));
@@ -70,8 +86,8 @@ export class TimeTraveler {
7086
if (!this.canRedo) {
7187
return;
7288
}
73-
const currentSnapshots = stacks[cursor];
74-
const snapshots = stacks[cursor + 1];
89+
const currentSnapshots = stacks[cursor].get();
90+
const snapshots = stacks[cursor + 1].get();
7591
this.cursor += 1;
7692
return restoreSnapshot(snapshots, () => {
7793
this.restoreCallbacks.forEach(callback => callback('redo', snapshots, currentSnapshots));
@@ -92,3 +108,5 @@ export class TimeTraveler {
92108
}
93109

94110
export const timeTraveler = new TimeTraveler();
111+
112+
(window as any).timeTraveler = timeTraveler;

packages/editor/src/libs/history/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export interface Snapshots {
55

66
export interface TimeTravelerConfig {
77
maxStacks: number;
8+
debounceTime: number;
89
}
910

1011
export type RestoreCallback = (

packages/editor/src/libs/indexMap/componentIndexMap.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,9 @@ export function generateComponentsIndex(componentInstances: ComponentInstance[])
9898
}
9999

100100
export function regenerateAllPagesComponentsIndex() {
101-
pagesStore.pages.forEach(page => regeneratePageComponentsIndex(page.key, page.componentInstances));
101+
return pagesStore.pages.forEach(({ key, componentInstances }) => {
102+
return regeneratePageComponentsIndex(key, componentInstances);
103+
});
102104
}
103105

104106
export function regeneratePageComponentsIndex(pageKey: number, pageComponentInstances: ComponentInstance[]) {

0 commit comments

Comments
 (0)