Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: record & replay commands with multi-mode #3734

Merged
merged 2 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,16 @@
import { CommandType, type ICommand } from '@univerjs/core';
import { ActionRecorderService } from '../../services/action-recorder.service';

export const StartRecordingActionCommand: ICommand = {
interface IStartRecordingActionCommandParams {
replaceId?: boolean;
}

export const StartRecordingActionCommand: ICommand<IStartRecordingActionCommandParams> = {
id: 'action-recorder.command.start-recording',
type: CommandType.COMMAND,
handler: (accessor) => {
handler: (accessor, params) => {
const actionRecorderService = accessor.get(ActionRecorderService);
actionRecorderService.startRecording();
actionRecorderService.startRecording(!!params?.replaceId);
return true;
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import { CommandType, type ICommand } from '@univerjs/core';
import { MessageType } from '@univerjs/design';
import { IMessageService } from '@univerjs/ui';
import { ActionReplayService } from '../../services/replay.service';
import { ActionReplayService, ReplayMode } from '../../services/replay.service';

export const ReplayLocalRecordCommand: ICommand = {
id: 'action-recorder.command.replay-local-records',
Expand All @@ -38,3 +38,41 @@ export const ReplayLocalRecordCommand: ICommand = {
},
};

export const ReplayLocalRecordOnNamesakeCommand: ICommand = {
id: 'action-recorder.command.replay-local-records-name',
type: CommandType.COMMAND,
handler: async (accessor) => {
const replayService = accessor.get(ActionReplayService);
const result = await replayService.replayLocalJSON(ReplayMode.NAME);

if (result) {
const messageService = accessor.get(IMessageService);
messageService.show({
type: MessageType.Success,
content: 'Successfully replayed local records',
});
}

return result;
},
};

export const ReplayLocalRecordOnActiveCommand: ICommand = {
id: 'action-recorder.command.replay-local-records-active',
type: CommandType.COMMAND,
handler: async (accessor) => {
const replayService = accessor.get(ActionReplayService);
const result = await replayService.replayLocalJSON(ReplayMode.ACTIVE);

if (result) {
const messageService = accessor.get(IMessageService);
messageService.show({
type: MessageType.Success,
content: 'Successfully replayed local records',
});
}

return result;
},
};

Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ import {
IUIPartsService,
} from '@univerjs/ui';
import { CompleteRecordingActionCommand, StartRecordingActionCommand, StopRecordingActionCommand } from '../commands/commands/record.command';
import { ReplayLocalRecordCommand } from '../commands/commands/replay.command';
import { ReplayLocalRecordCommand, ReplayLocalRecordOnActiveCommand, ReplayLocalRecordOnNamesakeCommand } from '../commands/commands/replay.command';
import { CloseRecordPanelOperation, OpenRecordPanelOperation } from '../commands/operations/operation';
import { ActionRecorderService } from '../services/action-recorder.service';
import { RecorderPanel } from '../views/components/RecorderPanel';
Expand Down Expand Up @@ -104,6 +104,8 @@ export class ActionRecorderController extends Disposable {
OpenRecordPanelOperation,
CloseRecordPanelOperation,
ReplayLocalRecordCommand,
ReplayLocalRecordOnNamesakeCommand,
ReplayLocalRecordOnActiveCommand,
]).forEach((command) => this._commandSrv.registerCommand(command));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
* limitations under the License.
*/

import { MenuItemType, RibbonStartGroup } from '@univerjs/ui';
import type { IAccessor } from '@univerjs/core';
import type { IMenuButtonItem, IMenuSelectorItem, MenuSchemaType } from '@univerjs/ui';
import { ReplayLocalRecordCommand } from '../commands/commands/replay.command';
import { MenuItemType, RibbonStartGroup } from '@univerjs/ui';
import { ReplayLocalRecordCommand, ReplayLocalRecordOnActiveCommand, ReplayLocalRecordOnNamesakeCommand } from '../commands/commands/replay.command';
import { OpenRecordPanelOperation } from '../commands/operations/operation';
import { ActionRecorderService } from '../services/action-recorder.service';

Expand Down Expand Up @@ -50,6 +50,22 @@ export function ReplayLocalRecordMenuItemFactory(): IMenuButtonItem {
};
}

export function ReplayLocalRecordOnNamesakeMenuItemFactory(): IMenuButtonItem {
return {
id: ReplayLocalRecordOnNamesakeCommand.id,
title: 'action-recorder.menu.replay-local-name',
type: MenuItemType.BUTTON,
};
}

export function ReplayLocalRecordOnActiveMenuItemFactory(): IMenuButtonItem {
return {
id: ReplayLocalRecordOnActiveCommand.id,
title: 'action-recorder.menu.replay-local-active',
type: MenuItemType.BUTTON,
};
}

export const menuSchema: MenuSchemaType = {
[RibbonStartGroup.OTHERS]: {
[RECORD_MENU_ITEM_ID]: {
Expand All @@ -63,6 +79,14 @@ export const menuSchema: MenuSchemaType = {
order: 2,
menuItemFactory: ReplayLocalRecordMenuItemFactory,
},
[ReplayLocalRecordOnNamesakeCommand.id]: {
order: 3,
menuItemFactory: ReplayLocalRecordOnNamesakeMenuItemFactory,
},
[ReplayLocalRecordOnActiveCommand.id]: {
order: 4,
menuItemFactory: ReplayLocalRecordOnActiveMenuItemFactory,
},
},

},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@
* limitations under the License.
*/

import { CommandType, Disposable, ICommandService, ILogService } from '@univerjs/core';
import type { ICommand, ICommandInfo, IDisposable, Nullable, Workbook } from '@univerjs/core';
import type { ISheetCommandSharedParams } from '@univerjs/sheets';
import { CommandType, Disposable, ICommandService, ILogService, IUniverInstanceService } from '@univerjs/core';
import { SetSelectionsOperation } from '@univerjs/sheets';
import { ILocalFileService } from '@univerjs/ui';
import { BehaviorSubject } from 'rxjs';
import type { ICommand, ICommandInfo, IDisposable, Nullable } from '@univerjs/core';

/**
* This service is for recording commands. What commands should be recorded can be configured by other
Expand Down Expand Up @@ -46,7 +47,8 @@ export class ActionRecorderService extends Disposable {
constructor(
@ICommandService private readonly _commandSrv: ICommandService,
@ILogService private readonly _logService: ILogService,
@ILocalFileService private readonly _localFileService: ILocalFileService
@ILocalFileService private readonly _localFileService: ILocalFileService,
@IUniverInstanceService private readonly _instanceService: IUniverInstanceService
) {
super();
}
Expand All @@ -62,12 +64,28 @@ export class ActionRecorderService extends Disposable {
if (visible === false) this.stopRecording();
}

startRecording(): void {
this._recorder = this._commandSrv.onCommandExecuted((commandInfo) => {
if (this._shouldRecordCommands.has(commandInfo.id)) {
startRecording(replaceId = false): void {
this._recorder = this._commandSrv.onCommandExecuted((rawCommandInfo) => {
if (this._shouldRecordCommands.has(rawCommandInfo.id)) {
const recorded = this._recorded;
const commands = this._recordedCommands;

let commandInfo = { ...rawCommandInfo };

const focusUnitId = this._instanceService.getFocusedUnit()?.getUnitId();
const { unitId = focusUnitId, subUnitId } = commandInfo?.params as ISheetCommandSharedParams;

if (replaceId && unitId && subUnitId) {
const subUnitName = (this._instanceService.getUnit(unitId) as Workbook).getSheetBySheetId(subUnitId)?.getName();
commandInfo = {
...commandInfo,
params: {
...commandInfo.params,
subUnitId: subUnitName,
},
};
}

if (
commandInfo.id === SetSelectionsOperation.id &&
recorded.length > 0 &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,17 @@
* limitations under the License.
*/

import type { ICommandInfo, Workbook } from '@univerjs/core';
import type { ISheetCommandSharedParams } from '@univerjs/sheets';
import { awaitTime, Disposable, ICommandService, ILogService, IUniverInstanceService } from '@univerjs/core';
import { MessageType } from '@univerjs/design';
import { ILocalFileService, IMessageService } from '@univerjs/ui';
import type { ICommandInfo } from '@univerjs/core';

export enum ReplayMode {
DEFAULT = 'default',
NAME = 'name',
ACTIVE = 'active',
}

/**
* This service is for replaying user actions.
Expand All @@ -36,13 +43,13 @@ export class ActionReplayService extends Disposable {
/**
* Read a local file and try to replay commands in this JSON.
*/
async replayLocalJSON(): Promise<boolean> {
async replayLocalJSON(mode: ReplayMode = ReplayMode.DEFAULT): Promise<boolean> {
const files = await this._localFileService.openFile({ multiple: false, accept: '.json' });
if (files.length !== 1) return false;

const file = files[0];
try {
return this.replayCommands(JSON.parse(await file.text()));
return this.replayCommands(JSON.parse(await file.text()), { mode });
} catch {
this._messageService.show({
type: MessageType.Error,
Expand All @@ -58,17 +65,38 @@ export class ActionReplayService extends Disposable {
* @param commands - The commands to replay.
* @returns If the replay is successful.
*/
async replayCommands(commands: ICommandInfo[]): Promise<boolean> {
async replayCommands(commands: ICommandInfo[], options?: { mode: ReplayMode }): Promise<boolean> {
const focusedUnitId = this._instanceService.getFocusedUnit()?.getUnitId();
if (!focusedUnitId) {
this._logService.error('[ReplayService]', 'no focused unit to replay commands');
}

const { mode } = options || {};

for (const command of commands) {
const { id, params } = command;
if (params) {
if (typeof (params as ISharedCommandParams).unitId !== 'undefined') {
(params as ISharedCommandParams).unitId = focusedUnitId;
const commandParams = params as ISheetCommandSharedParams;
if (commandParams) {
if (typeof commandParams.unitId !== 'undefined') {
commandParams.unitId = focusedUnitId!;
}

if (mode === ReplayMode.NAME && commandParams.subUnitId !== 'undefined') {
const realSubUnitId = (this._instanceService.getFocusedUnit() as Workbook).getSheetBySheetName(commandParams.subUnitId)?.getSheetId();
if (realSubUnitId) {
commandParams.subUnitId = realSubUnitId;
} else {
this._logService.error('[ReplayService]', `failed to find subunit by subUnitName = ${commandParams.subUnitId}`);
}
}

if (mode === ReplayMode.ACTIVE && commandParams.subUnitId !== 'undefined') {
const realSubUnitId = (this._instanceService.getFocusedUnit() as Workbook).getActiveSheet()?.getSheetId();
if (realSubUnitId) {
commandParams.subUnitId = realSubUnitId;
} else {
this._logService.error('[ReplayService]', 'failed to find active subunit');
}
}

const result = await this._commandService.executeCommand(id, params);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ function RecordPanelImpl() {
if (!recording) commandService.executeCommand(CloseRecordPanelOperation.id);
}, [commandService, recording]);

const startRecording = useCallback(() => {
if (!recording) commandService.executeCommand(StartRecordingActionCommand.id);
const startRecording = useCallback((replaceId?: boolean) => {
if (!recording) commandService.executeCommand(StartRecordingActionCommand.id, { replaceId });
}, [commandService, recording]);

const completeRecording = useCallback(() => {
Expand All @@ -69,7 +69,8 @@ function RecordPanelImpl() {
<div className={styles.actionRecorderPanelTitle}>{titleText}</div>
<div className={styles.actionRecorderPanelActions}>
<Button type="default" size="small" onClick={recording ? stopRecording : closePanel}>{ recording ? 'Cancel' : 'Close' }</Button>
<Button type="primary" size="small" onClick={recording ? completeRecording : startRecording}>{ recording ? 'Save' : 'Start' }</Button>
<Button type="primary" size="small" onClick={recording ? completeRecording : () => startRecording()}>{ recording ? 'Save' : 'Start' }</Button>
{ !recording && <Button type="primary" size="small" onClick={() => startRecording(true)}>Start(N)</Button>}
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
display: flex;
flex-grow: 0;
flex-shrink: 0;
width: 180px;
width: 260px;
justify-content: space-between;

> button {
Expand Down
Loading