Skip to content

Commit

Permalink
feat(facade): add redo undo facade hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
hexf00 committed Aug 27, 2024
1 parent c6ee109 commit a2916c7
Show file tree
Hide file tree
Showing 4 changed files with 243 additions and 7 deletions.
32 changes: 31 additions & 1 deletion packages/facade/src/apis/f-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@
* limitations under the License.
*/

import { Inject, LifecycleService, LifecycleStages, toDisposable } from '@univerjs/core';
import { Inject, Injector, LifecycleService, LifecycleStages, toDisposable } from '@univerjs/core';
import type { IDisposable } from '@univerjs/core';
import { filter } from 'rxjs';
import { FUndoRedoHooks } from './hooks/f-undoredo-hooks';

export class FHooks {
constructor(
@Inject(Injector) protected readonly _injector: Injector,
@Inject(LifecycleService) private readonly _lifecycleService: LifecycleService
) {
// empty
Expand Down Expand Up @@ -60,4 +62,32 @@ export class FHooks {
onSteady(callback: () => void): IDisposable {
return toDisposable(this._lifecycleService.lifecycle$.pipe(filter((lifecycle) => lifecycle === LifecycleStages.Steady)).subscribe(callback));
}

/**
* Hook that fires before an undo operation is executed.
* @param callback Function to be called when the event is triggered
* @returns A disposable object that can be used to unsubscribe from the event
*/
beforeUndo = FUndoRedoHooks.beforeUndo.bind(this);

/**
* Hook that fires after an undo operation is executed.
* @param callback Function to be called when the event is triggered
* @returns A disposable object that can be used to unsubscribe from the event
*/
afterUndo = FUndoRedoHooks.afterUndo.bind(this);

/**
* Hook that fires before a redo operation is executed.
* @param callback Function to be called when the event is triggered
* @returns A disposable object that can be used to unsubscribe from the event
*/
beforeRedo = FUndoRedoHooks.beforeRedo.bind(this);

/**
* Hook that fires after a redo operation is executed.
* @param callback Function to be called when the event is triggered
* @returns A disposable object that can be used to unsubscribe from the event
*/
afterRedo = FUndoRedoHooks.afterRedo.bind(this);
}
13 changes: 7 additions & 6 deletions packages/facade/src/apis/facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
Injector,
IUniverInstanceService,
Quantity,
RedoCommand,
toDisposable,
UndoCommand,
Univer, UniverInstanceType, WrapStrategy,
Expand Down Expand Up @@ -71,9 +72,7 @@ export class FUniver {
constructor(
@Inject(Injector) protected readonly _injector: Injector,
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
@ICommandService private readonly _commandService: ICommandService,
@ISocketService private readonly _ws: ISocketService,
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService
@ICommandService private readonly _commandService: ICommandService
) {
this._initialize();
}
Expand Down Expand Up @@ -328,7 +327,7 @@ export class FUniver {
* @returns {Promise<boolean>} redo result
*/
redo(): Promise<boolean> {
return this._commandService.executeCommand(UndoCommand.id);
return this._commandService.executeCommand(RedoCommand.id);
}

// #endregion
Expand Down Expand Up @@ -384,7 +383,8 @@ export class FUniver {
* @returns {ISocket} WebSocket instance
*/
createSocket(url: string): ISocket {
const ws = this._ws.createSocket(url);
const wsService = this._injector.get(ISocketService);
const ws = wsService.createSocket(url);

if (!ws) {
throw new Error('[WebSocketService]: failed to create socket!');
Expand Down Expand Up @@ -421,7 +421,8 @@ export class FUniver {
* @returns {Nullable<RenderComponentType>} The render component.
*/
private _getSheetRenderComponent(unitId: string, viewKey: SHEET_VIEW_KEY): Nullable<RenderComponentType> {
const render = this._renderManagerService.getRenderById(unitId);
const renderManagerService = this._injector.get(IRenderManagerService);
const render = renderManagerService.getRenderById(unitId);
if (!render) {
throw new Error('Render not found');
}
Expand Down
111 changes: 111 additions & 0 deletions packages/facade/src/apis/hooks/__tests__/f-undoredo-hooks.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { type Injector, IUndoRedoService, IUniverInstanceService, Univer, UniverInstanceType } from '@univerjs/core';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { UniverSheetsPlugin } from '@univerjs/sheets';
import { UniverRenderEnginePlugin } from '@univerjs/engine-render';
import { FUniver } from '../../facade';
import type { FWorksheet } from '../../sheets/f-worksheet';

function createUnitTestBed(): {
univer: Univer;
get: Injector['get'];
univerAPI: FUniver;
injector: Injector;
} {
const univer = new Univer();
const injector = univer.__getInjector();
univer.registerPlugin(UniverSheetsPlugin);
univer.registerPlugin(UniverRenderEnginePlugin);

const sheet = univer.createUnit(UniverInstanceType.UNIVER_SHEET, {});
const univerInstanceService = injector.get(IUniverInstanceService);
univerInstanceService.focusUnit(sheet.getUnitId());
const univerAPI = FUniver.newAPI(univer);

return {
univer,
get: injector.get.bind(injector),
univerAPI,
injector,
};
}

describe('Test Undo Redo Hooks', () => {
let get: Injector['get'];
let univerAPI: FUniver;

beforeEach(() => {
const testBed = createUnitTestBed();
get = testBed.get;
univerAPI = testBed.univerAPI;
});

it('undoredo normal case', async () => {
const sheet = univerAPI.getActiveWorkbook()?.getActiveSheet() as FWorksheet;
expect(sheet).not.toBeUndefined();
const range = sheet.getRange(0, 0);
const emptyFlag = '';
const text = 'Hello World';
await range.setValue(emptyFlag);
await range.setValue(text);
univerAPI.getHooks().beforeUndo(() => {
expect(range.getValue()).toBe(text);
});
univerAPI.getHooks().afterUndo(() => {
expect(range.getValue()).toBe(emptyFlag);
});
univerAPI.getHooks().beforeRedo(() => {
expect(range.getValue()).toBe(emptyFlag);
});
univerAPI.getHooks().afterRedo(() => {
expect(range.getValue()).toBe(text);
});
expect(range.getValue()).toBe(text);
await univerAPI.undo();
expect(range.getValue()).toBe(emptyFlag);
await univerAPI.redo();
expect(range.getValue()).toBe(text);
});

it('undoredo edge case', async () => {
const sheet = univerAPI.getActiveWorkbook()?.getActiveSheet() as FWorksheet;
expect(sheet).not.toBeUndefined();

// manually construct undo redo service
get(IUndoRedoService);

const beforeUndoFn = vi.fn();
const afterUndoFn = vi.fn();
const beforeRedoFn = vi.fn();
const afterRedoFn = vi.fn();

univerAPI.getHooks().beforeUndo(beforeUndoFn);
univerAPI.getHooks().afterUndo(afterUndoFn);
univerAPI.getHooks().beforeRedo(beforeRedoFn);
univerAPI.getHooks().afterRedo(afterRedoFn);

await univerAPI.undo();
await univerAPI.redo();

expect(beforeUndoFn).toHaveBeenCalledTimes(0);
expect(afterUndoFn).toHaveBeenCalledTimes(0);
expect(beforeRedoFn).toHaveBeenCalledTimes(0);
expect(afterRedoFn).toHaveBeenCalledTimes(0);
});
});

94 changes: 94 additions & 0 deletions packages/facade/src/apis/hooks/f-undoredo-hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import type { IDisposable, IUndoRedoItem } from '@univerjs/core';
import { ICommandService, IUndoRedoService, RedoCommand, UndoCommand } from '@univerjs/core';
import type { FHooks } from '../f-hooks';

export const FUndoRedoHooks = {
/**
* Hook that fires before an undo operation is executed.
* @param callback Function to be called when the event is triggered
* @returns A disposable object that can be used to unsubscribe from the event
*/
beforeUndo(this: FHooks, callback: (action: IUndoRedoItem) => void): IDisposable {
const commandService = this._injector.get(ICommandService);

return commandService.beforeCommandExecuted((command) => {
if (command.id === UndoCommand.id) {
const undoredoService = this._injector.get(IUndoRedoService);
const action = undoredoService.pitchTopUndoElement();
if (action) {
callback(action);
}
}
});
},
/**
* Hook that fires after an undo operation is executed.
* @param callback Function to be called when the event is triggered
* @returns A disposable object that can be used to unsubscribe from the event
*/
afterUndo(this: FHooks, callback: (action: IUndoRedoItem) => void): IDisposable {
const commandService = this._injector.get(ICommandService);

return commandService.onCommandExecuted((command) => {
if (command.id === UndoCommand.id) {
const undoredoService = this._injector.get(IUndoRedoService);
const action = undoredoService.pitchTopUndoElement();
if (action) {
callback(action);
}
}
});
},
/**
* Hook that fires before a redo operation is executed.
* @param callback Function to be called when the event is triggered
* @returns A disposable object that can be used to unsubscribe from the event
*/
beforeRedo(this: FHooks, callback: (action: IUndoRedoItem) => void): IDisposable {
const commandService = this._injector.get(ICommandService);

return commandService.beforeCommandExecuted((command) => {
if (command.id === RedoCommand.id) {
const undoredoService = this._injector.get(IUndoRedoService);
const action = undoredoService.pitchTopRedoElement();
if (action) {
callback(action);
}
}
});
},
/**
* Hook that fires after a redo operation is executed.
* @param callback Function to be called when the event is triggered
* @returns A disposable object that can be used to unsubscribe from the event
*/
afterRedo(this: FHooks, callback: (action: IUndoRedoItem) => void): IDisposable {
const commandService = this._injector.get(ICommandService);

return commandService.onCommandExecuted((command) => {
if (command.id === RedoCommand.id) {
const undoredoService = this._injector.get(IUndoRedoService);
const action = undoredoService.pitchTopRedoElement();
if (action) {
callback(action);
}
}
});
},
};

0 comments on commit a2916c7

Please sign in to comment.