合并批量组件绑定历史记录
批处理中的前置绑定批次跳过历史,最后一批使用非跳过替换统一形成撤销边界 为跳过后记录历史的状态基线补充撤销回归测试
This commit is contained in:
@@ -563,6 +563,7 @@ export function useUiEditorState(initialState: State = EMPTY_UI_EDITOR_STATE) {
|
||||
const isLockedRef = useRef(false);
|
||||
const undoStackRef = useRef<Array<{ before: State; after: State }>>([]);
|
||||
const redoStackRef = useRef<Array<{ before: State; after: State }>>([]);
|
||||
const pendingHistoryBeforeRef = useRef<State | null>(null);
|
||||
stateRef.current = state;
|
||||
|
||||
const syncHistoryState = useCallback(() => {
|
||||
@@ -580,15 +581,20 @@ export function useUiEditorState(initialState: State = EMPTY_UI_EDITOR_STATE) {
|
||||
const commit = useCallback(
|
||||
(nextState: State) => {
|
||||
const current = stateRef.current;
|
||||
if (sameResource(current, nextState)) return false;
|
||||
const before = pendingHistoryBeforeRef.current ?? current;
|
||||
if (sameResource(before, nextState)) {
|
||||
pendingHistoryBeforeRef.current = null;
|
||||
return false;
|
||||
}
|
||||
undoStackRef.current.push({
|
||||
before: cloneState(current),
|
||||
before: cloneState(before),
|
||||
after: nextState,
|
||||
});
|
||||
if (undoStackRef.current.length > MAX_HISTORY_LENGTH) {
|
||||
undoStackRef.current.shift();
|
||||
}
|
||||
redoStackRef.current = [];
|
||||
pendingHistoryBeforeRef.current = null;
|
||||
syncHistoryState();
|
||||
applyState(nextState);
|
||||
return true;
|
||||
@@ -599,6 +605,7 @@ export function useUiEditorState(initialState: State = EMPTY_UI_EDITOR_STATE) {
|
||||
const resetHistory = useCallback(() => {
|
||||
undoStackRef.current = [];
|
||||
redoStackRef.current = [];
|
||||
pendingHistoryBeforeRef.current = null;
|
||||
syncHistoryState();
|
||||
}, [syncHistoryState]);
|
||||
|
||||
@@ -606,6 +613,7 @@ export function useUiEditorState(initialState: State = EMPTY_UI_EDITOR_STATE) {
|
||||
if (isLockedRef.current) return false;
|
||||
const entry = undoStackRef.current.pop();
|
||||
if (!entry) return false;
|
||||
pendingHistoryBeforeRef.current = null;
|
||||
redoStackRef.current.push(entry);
|
||||
applyState(cloneState(entry.before));
|
||||
syncHistoryState();
|
||||
@@ -616,6 +624,7 @@ export function useUiEditorState(initialState: State = EMPTY_UI_EDITOR_STATE) {
|
||||
if (isLockedRef.current) return false;
|
||||
const entry = redoStackRef.current.pop();
|
||||
if (!entry) return false;
|
||||
pendingHistoryBeforeRef.current = null;
|
||||
undoStackRef.current.push(entry);
|
||||
applyState(cloneState(entry.after));
|
||||
syncHistoryState();
|
||||
@@ -642,6 +651,7 @@ export function useUiEditorState(initialState: State = EMPTY_UI_EDITOR_STATE) {
|
||||
try {
|
||||
return await operation(snapshot);
|
||||
} finally {
|
||||
pendingHistoryBeforeRef.current = null;
|
||||
isLockedRef.current = false;
|
||||
setIsLocked(false);
|
||||
}
|
||||
@@ -1518,6 +1528,9 @@ export function useUiEditorState(initialState: State = EMPTY_UI_EDITOR_STATE) {
|
||||
resetHistory();
|
||||
applyState(next);
|
||||
} else if (options.history === 'skip') {
|
||||
if (!pendingHistoryBeforeRef.current) {
|
||||
pendingHistoryBeforeRef.current = cloneState(stateRef.current);
|
||||
}
|
||||
applyState(next);
|
||||
} else {
|
||||
commit(next);
|
||||
|
||||
@@ -1028,7 +1028,9 @@ export function useUiEditorSession(
|
||||
spriteIds,
|
||||
});
|
||||
current = applyBindingResult(current, result);
|
||||
editor.replaceState(current);
|
||||
editor.replaceState(current, {
|
||||
history: index < batches.length - 1 ? 'skip' : 'record',
|
||||
});
|
||||
}
|
||||
setBindingStatus(
|
||||
`组件绑定完成(${batches.length}/${batches.length})。`,
|
||||
|
||||
@@ -705,4 +705,47 @@ describe('useUiEditorState', () => {
|
||||
'中间',
|
||||
);
|
||||
});
|
||||
|
||||
it('records one history entry after skipped replacement batches', () => {
|
||||
const initial: State = {
|
||||
...structuredClone(EMPTY_UI_EDITOR_STATE),
|
||||
ui_design_images: { page: image('Page') },
|
||||
};
|
||||
const { result } = renderHook(() => useUiEditorState(initial));
|
||||
|
||||
act(() => {
|
||||
result.current.replaceState(
|
||||
{
|
||||
...initial,
|
||||
ui_design_images: { page: image('第一批') },
|
||||
},
|
||||
{ history: 'skip' },
|
||||
);
|
||||
result.current.replaceState(
|
||||
{
|
||||
...initial,
|
||||
ui_design_images: { page: image('最终') },
|
||||
},
|
||||
{ history: 'record' },
|
||||
);
|
||||
});
|
||||
|
||||
expect(result.current.state.ui_design_images.page?.metadata.name).toBe(
|
||||
'最终',
|
||||
);
|
||||
expect(result.current.historyState).toEqual({
|
||||
canUndo: true,
|
||||
canRedo: false,
|
||||
});
|
||||
act(() => {
|
||||
expect(result.current.undo()).toBe(true);
|
||||
});
|
||||
expect(result.current.state.ui_design_images.page?.metadata.name).toBe(
|
||||
'Page',
|
||||
);
|
||||
expect(result.current.historyState).toEqual({
|
||||
canUndo: false,
|
||||
canRedo: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user