合并批量组件绑定历史记录
Project CI / Repository checks (pull_request) Failing after 11s
Project CI / Backend tests (pull_request) Failing after 10s
Project CI / Frontend tests (pull_request) Successful in 3m7s
Project CI / Native shell tests (pull_request) Successful in 22m4s

批处理中的前置绑定批次跳过历史,最后一批使用非跳过替换统一形成撤销边界

为跳过后记录历史的状态基线补充撤销回归测试
This commit is contained in:
2026-09-03 14:37:28 +08:00
parent e4680d6f70
commit 9fd160efc5
3 changed files with 61 additions and 3 deletions
@@ -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,
});
});
});