snapshot of https://github.com/dream-num/univer.git (dev, 2026-09-16; blobs > 131072B omitted)
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled
This commit is contained in:
41
packages/action-recorder/README.md
Normal file
41
packages/action-recorder/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# @univerjs/action-recorder
|
||||
|
||||
[](https://npmjs.com/package/@univerjs/action-recorder)
|
||||
[](https://npmjs.com/package/@univerjs/action-recorder)
|
||||
[](https://npmjs.com/package/@univerjs/action-recorder)
|
||||
|
||||
`@univerjs/action-recorder` records user actions in Univer and can replay them for debugging, demos, or automated workflow reproduction.
|
||||
|
||||
## Package Overview
|
||||
|
||||
| Package | UMD global | CSS | Locales | Facade entry |
|
||||
| --- | --- | :---: | :---: | :---: |
|
||||
| `@univerjs/action-recorder` | `UniverActionRecorder` | Yes | Yes | No |
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
pnpm add @univerjs/action-recorder
|
||||
# or
|
||||
npm install @univerjs/action-recorder
|
||||
```
|
||||
|
||||
Keep all `@univerjs/*` packages on the same version.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import '@univerjs/action-recorder/lib/index.css';
|
||||
import EnUS from '@univerjs/action-recorder/locale/en-US';
|
||||
import { UniverActionRecorderPlugin } from '@univerjs/action-recorder';
|
||||
|
||||
univer.registerPlugin(UniverActionRecorderPlugin);
|
||||
|
||||
// Merge EnUS into your Univer locale map when this package contributes UI text.
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Documentation](https://docs.univer.ai)
|
||||
- [NPM package](https://npmjs.com/package/@univerjs/action-recorder)
|
||||
- [GitHub repository](https://github.com/dream-num/univer)
|
||||
93
packages/action-recorder/package.json
Normal file
93
packages/action-recorder/package.json
Normal file
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"name": "@univerjs/action-recorder",
|
||||
"version": "1.0.0-rc.0",
|
||||
"private": false,
|
||||
"description": "Record and replay user actions in Univer.",
|
||||
"author": "DreamNum Co., Ltd. <developer@univer.ai>",
|
||||
"license": "Apache-2.0",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/univer"
|
||||
},
|
||||
"homepage": "https://univer.ai",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dream-num/univer"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/dream-num/univer/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"univer",
|
||||
"action-recorder",
|
||||
"replay",
|
||||
"automation",
|
||||
"plugin"
|
||||
],
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./*": "./src/*",
|
||||
"./locale/*": "./src/locale/*.ts"
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"types": "./lib/types/index.d.ts",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "./lib/es/index.js",
|
||||
"module": "./lib/es/index.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./lib/es/index.js",
|
||||
"require": "./lib/cjs/index.js",
|
||||
"types": "./lib/types/index.d.ts"
|
||||
},
|
||||
"./*": {
|
||||
"import": "./lib/es/*",
|
||||
"require": "./lib/cjs/*",
|
||||
"types": "./lib/types/index.d.ts"
|
||||
},
|
||||
"./locale/*": {
|
||||
"import": "./lib/es/locale/*.js",
|
||||
"require": "./lib/cjs/locale/*.js",
|
||||
"types": "./lib/types/locale/*.d.ts"
|
||||
},
|
||||
"./lib/*": "./lib/*"
|
||||
}
|
||||
},
|
||||
"directories": {
|
||||
"lib": "lib"
|
||||
},
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"coverage": "vitest run --coverage",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build:bundle": "univer-cli build",
|
||||
"build:types": "tsc -p tsconfig.node.json",
|
||||
"build": "pnpm run build:bundle && pnpm run build:types"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.9.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc",
|
||||
"rxjs": ">=7.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@univerjs/core": "workspace:*",
|
||||
"@univerjs/design": "workspace:*",
|
||||
"@univerjs/icons": "1.38.0",
|
||||
"@univerjs/sheets": "workspace:*",
|
||||
"@univerjs/sheets-filter": "workspace:*",
|
||||
"@univerjs/sheets-ui": "workspace:*",
|
||||
"@univerjs/ui": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@univerjs-infra/shared": "workspace:*",
|
||||
"react": "18.3.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"tailwindcss": "3.4.18",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^5.0.0"
|
||||
}
|
||||
}
|
||||
1
packages/action-recorder/postcss.config.mjs
Normal file
1
packages/action-recorder/postcss.config.mjs
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from '@univerjs-infra/shared/postcss';
|
||||
@@ -0,0 +1,305 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 } from '@univerjs/core';
|
||||
import type { IMessageProps } from '@univerjs/design';
|
||||
import {
|
||||
CommandService,
|
||||
CommandType,
|
||||
ConfigService,
|
||||
ContextService,
|
||||
DesktopLogService,
|
||||
ICommandService,
|
||||
IConfigService,
|
||||
IContextService,
|
||||
ILogService,
|
||||
Injector,
|
||||
IUniverInstanceService,
|
||||
LogLevel,
|
||||
toDisposable,
|
||||
} from '@univerjs/core';
|
||||
import { MessageType } from '@univerjs/design';
|
||||
import { ILocalFileService, IMessageService } from '@univerjs/ui';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { ActionRecorderService } from '../../../services/action-recorder.service';
|
||||
import { ActionReplayService } from '../../../services/replay.service';
|
||||
import {
|
||||
CompleteRecordingActionCommand,
|
||||
StartRecordingActionCommand,
|
||||
StopRecordingActionCommand,
|
||||
} from '../record.command';
|
||||
import {
|
||||
ReplayLocalRecordCommand,
|
||||
ReplayLocalRecordOnActiveCommand,
|
||||
ReplayLocalRecordOnNamesakeCommand,
|
||||
} from '../replay.command';
|
||||
|
||||
const APPLY_CELL_VALUE_COMMAND_ID = 'action-recorder.command-test.apply-cell-value';
|
||||
|
||||
interface ITestFile {
|
||||
name: string;
|
||||
text(): Promise<string>;
|
||||
}
|
||||
|
||||
interface IAppliedCommandParams {
|
||||
unitId?: string;
|
||||
subUnitId?: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
class TestState {
|
||||
static files: ITestFile[] = [];
|
||||
static downloads: Array<{ data: Blob; fileName: string }> = [];
|
||||
static messages: IMessageProps[] = [];
|
||||
static appliedParams: IAppliedCommandParams[] = [];
|
||||
|
||||
static reset() {
|
||||
this.files = [];
|
||||
this.downloads = [];
|
||||
this.messages = [];
|
||||
this.appliedParams = [];
|
||||
}
|
||||
}
|
||||
|
||||
class TestLocalFileService implements ILocalFileService {
|
||||
openFile(): Promise<File[]> {
|
||||
return Promise.resolve(TestState.files as File[]);
|
||||
}
|
||||
|
||||
downloadFile(data: Blob, fileName: string): void {
|
||||
TestState.downloads.push({ data, fileName });
|
||||
}
|
||||
}
|
||||
|
||||
class TestMessageService implements IMessageService {
|
||||
show(options: IMessageProps): IDisposable {
|
||||
TestState.messages.push(options);
|
||||
return toDisposable(() => {});
|
||||
}
|
||||
|
||||
remove(): void {
|
||||
// no container in command tests
|
||||
}
|
||||
|
||||
removeAll(): void {
|
||||
TestState.messages = [];
|
||||
}
|
||||
}
|
||||
|
||||
class TestWorkbook {
|
||||
getUnitId() {
|
||||
return 'focused-workbook';
|
||||
}
|
||||
|
||||
getSheetBySheetName(sheetName: string) {
|
||||
if (sheetName !== 'Recorded Sheet') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { getSheetId: () => 'actual-sheet-id' };
|
||||
}
|
||||
|
||||
getActiveSheet() {
|
||||
return { getSheetId: () => 'active-sheet-id' };
|
||||
}
|
||||
|
||||
getSheetBySheetId(sheetId: string) {
|
||||
if (sheetId !== 'sheet-1') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { getName: () => 'Recorded Sheet' };
|
||||
}
|
||||
}
|
||||
|
||||
class TestUniverInstanceService {
|
||||
private readonly _workbook = new TestWorkbook();
|
||||
|
||||
getFocusedUnit() {
|
||||
return this._workbook;
|
||||
}
|
||||
|
||||
getUnit() {
|
||||
return this._workbook;
|
||||
}
|
||||
}
|
||||
|
||||
function createCommandTestBed() {
|
||||
const injector = new Injector();
|
||||
injector.add([ICommandService, { useClass: CommandService }]);
|
||||
injector.add([ILogService, { useClass: DesktopLogService }]);
|
||||
injector.add([IContextService, { useClass: ContextService }]);
|
||||
injector.add([IConfigService, { useClass: ConfigService }]);
|
||||
injector.add([ILocalFileService, { useClass: TestLocalFileService }]);
|
||||
injector.add([IMessageService, { useClass: TestMessageService }]);
|
||||
injector.add([IUniverInstanceService, { useClass: TestUniverInstanceService as never }]);
|
||||
injector.add([ActionRecorderService]);
|
||||
injector.add([ActionReplayService]);
|
||||
|
||||
const logService = injector.get(ILogService);
|
||||
logService.setLogLevel(LogLevel.SILENT);
|
||||
|
||||
const commandService = injector.get(ICommandService);
|
||||
commandService.registerCommand(StartRecordingActionCommand);
|
||||
commandService.registerCommand(CompleteRecordingActionCommand);
|
||||
commandService.registerCommand(StopRecordingActionCommand);
|
||||
commandService.registerCommand(ReplayLocalRecordCommand);
|
||||
commandService.registerCommand(ReplayLocalRecordOnNamesakeCommand);
|
||||
commandService.registerCommand(ReplayLocalRecordOnActiveCommand);
|
||||
commandService.registerCommand({
|
||||
id: APPLY_CELL_VALUE_COMMAND_ID,
|
||||
type: CommandType.COMMAND,
|
||||
handler: (_accessor, params?: IAppliedCommandParams) => {
|
||||
TestState.appliedParams.push({ ...params });
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
commandService,
|
||||
recorderService: injector.get(ActionRecorderService),
|
||||
};
|
||||
}
|
||||
|
||||
describe('action-recorder commands', () => {
|
||||
beforeEach(() => {
|
||||
TestState.reset();
|
||||
});
|
||||
|
||||
it('records whitelisted user commands and exports them through the complete command', async () => {
|
||||
const { commandService, recorderService } = createCommandTestBed();
|
||||
const recordingStates: boolean[] = [];
|
||||
const commandIdsVisibleInPanel: string[][] = [];
|
||||
recorderService.recording$.subscribe((state) => recordingStates.push(state));
|
||||
recorderService.recordedCommands$.subscribe((commands) => {
|
||||
const ids: string[] = [];
|
||||
for (const command of commands) {
|
||||
ids.push(command.id);
|
||||
}
|
||||
commandIdsVisibleInPanel.push(ids);
|
||||
});
|
||||
recorderService.registerRecordedCommand({
|
||||
id: APPLY_CELL_VALUE_COMMAND_ID,
|
||||
type: CommandType.COMMAND,
|
||||
handler: () => true,
|
||||
});
|
||||
|
||||
await commandService.executeCommand(StartRecordingActionCommand.id, { replaceId: true });
|
||||
await commandService.executeCommand(APPLY_CELL_VALUE_COMMAND_ID, {
|
||||
unitId: 'focused-workbook',
|
||||
subUnitId: 'sheet-1',
|
||||
value: '42',
|
||||
});
|
||||
await commandService.executeCommand(CompleteRecordingActionCommand.id);
|
||||
|
||||
expect(recordingStates.at(-1)).toBe(false);
|
||||
expect(commandIdsVisibleInPanel.at(-1)).toEqual([]);
|
||||
expect(TestState.downloads).toHaveLength(1);
|
||||
expect(TestState.downloads[0].fileName).toBe('recorded-commands.json');
|
||||
|
||||
const exportedCommands = JSON.parse(await TestState.downloads[0].data.text());
|
||||
expect(exportedCommands).toEqual([
|
||||
{
|
||||
id: APPLY_CELL_VALUE_COMMAND_ID,
|
||||
type: CommandType.COMMAND,
|
||||
params: {
|
||||
unitId: 'focused-workbook',
|
||||
subUnitId: 'Recorded Sheet',
|
||||
value: '42',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses the same finalized export flow when the user stops recording', async () => {
|
||||
const { commandService, recorderService } = createCommandTestBed();
|
||||
|
||||
await commandService.executeCommand(StartRecordingActionCommand.id);
|
||||
expect(recorderService.recording).toBe(true);
|
||||
|
||||
await commandService.executeCommand(StopRecordingActionCommand.id);
|
||||
|
||||
expect(recorderService.recording).toBe(false);
|
||||
expect(TestState.downloads).toHaveLength(1);
|
||||
expect(TestState.downloads[0].fileName).toBe('recorded-commands.json');
|
||||
});
|
||||
|
||||
it('replays local records against the matching sheet name', async () => {
|
||||
const { commandService } = createCommandTestBed();
|
||||
TestState.files = [{
|
||||
name: 'recorded-commands.json',
|
||||
text: () => Promise.resolve(JSON.stringify([
|
||||
{
|
||||
id: APPLY_CELL_VALUE_COMMAND_ID,
|
||||
params: {
|
||||
unitId: 'old-workbook',
|
||||
subUnitId: 'Recorded Sheet',
|
||||
value: 'from-file',
|
||||
},
|
||||
},
|
||||
])),
|
||||
}];
|
||||
|
||||
const result = await commandService.executeCommand(ReplayLocalRecordOnNamesakeCommand.id);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(TestState.appliedParams).toEqual([{
|
||||
unitId: 'focused-workbook',
|
||||
subUnitId: 'actual-sheet-id',
|
||||
value: 'from-file',
|
||||
}]);
|
||||
expect(TestState.messages).toEqual([{
|
||||
type: MessageType.Success,
|
||||
content: 'Successfully replayed local records',
|
||||
}]);
|
||||
});
|
||||
|
||||
it('replays local records against the active sheet when requested', async () => {
|
||||
const { commandService } = createCommandTestBed();
|
||||
TestState.files = [{
|
||||
name: 'recorded-commands.json',
|
||||
text: () => Promise.resolve(JSON.stringify([
|
||||
{
|
||||
id: APPLY_CELL_VALUE_COMMAND_ID,
|
||||
params: {
|
||||
unitId: 'old-workbook',
|
||||
subUnitId: 'any-recorded-sheet',
|
||||
value: 'active-sheet-value',
|
||||
},
|
||||
},
|
||||
])),
|
||||
}];
|
||||
|
||||
const result = await commandService.executeCommand(ReplayLocalRecordOnActiveCommand.id);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(TestState.appliedParams).toEqual([{
|
||||
unitId: 'focused-workbook',
|
||||
subUnitId: 'active-sheet-id',
|
||||
value: 'active-sheet-value',
|
||||
}]);
|
||||
});
|
||||
|
||||
it('does not announce replay success when the user cancels local file selection', async () => {
|
||||
const { commandService } = createCommandTestBed();
|
||||
|
||||
const result = await commandService.executeCommand(ReplayLocalRecordCommand.id);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(TestState.appliedParams).toEqual([]);
|
||||
expect(TestState.messages).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { ICommand } from '@univerjs/core';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
import { ActionRecorderService } from '../../services/action-recorder.service';
|
||||
|
||||
interface IStartRecordingActionCommandParams {
|
||||
replaceId?: boolean;
|
||||
}
|
||||
|
||||
export const StartRecordingActionCommand: ICommand<IStartRecordingActionCommandParams> = {
|
||||
id: 'action-recorder.command.start-recording',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor, params) => {
|
||||
const actionRecorderService = accessor.get(ActionRecorderService);
|
||||
actionRecorderService.startRecording(!!params?.replaceId);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const CompleteRecordingActionCommand: ICommand = {
|
||||
id: 'action-recorder.command.complete-recording',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor) => {
|
||||
const actionRecorderService = accessor.get(ActionRecorderService);
|
||||
actionRecorderService.completeRecording();
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const StopRecordingActionCommand: ICommand = {
|
||||
id: 'action-recorder.command.stop-recording',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor) => {
|
||||
const actionRecorderService = accessor.get(ActionRecorderService);
|
||||
actionRecorderService.completeRecording();
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { ICommand } from '@univerjs/core';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
import { MessageType } from '@univerjs/design';
|
||||
import { IMessageService } from '@univerjs/ui';
|
||||
import { ActionReplayService, ReplayMode } from '../../services/replay.service';
|
||||
|
||||
export const ReplayLocalRecordCommand: ICommand = {
|
||||
id: 'action-recorder.command.replay-local-records',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor) => {
|
||||
const replayService = accessor.get(ActionReplayService);
|
||||
const result = await replayService.replayLocalJSON();
|
||||
|
||||
if (result) {
|
||||
const messageService = accessor.get(IMessageService);
|
||||
messageService.show({
|
||||
type: MessageType.Success,
|
||||
content: 'Successfully replayed local records',
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
};
|
||||
|
||||
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;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 {
|
||||
CommandService,
|
||||
ConfigService,
|
||||
ContextService,
|
||||
DesktopLogService,
|
||||
ICommandService,
|
||||
IConfigService,
|
||||
IContextService,
|
||||
ILogService,
|
||||
Injector,
|
||||
IUniverInstanceService,
|
||||
LogLevel,
|
||||
} from '@univerjs/core';
|
||||
import { ILocalFileService } from '@univerjs/ui';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { ActionRecorderService } from '../../../services/action-recorder.service';
|
||||
import { CloseRecordPanelOperation, OpenRecordPanelOperation } from '../operation';
|
||||
|
||||
class TestLocalFileService implements ILocalFileService {
|
||||
static downloads = 0;
|
||||
|
||||
static reset() {
|
||||
this.downloads = 0;
|
||||
}
|
||||
|
||||
openFile(): Promise<File[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
downloadFile(): void {
|
||||
TestLocalFileService.downloads += 1;
|
||||
}
|
||||
}
|
||||
|
||||
class TestFocusedUnit {
|
||||
getUnitId() {
|
||||
return 'focused-workbook';
|
||||
}
|
||||
}
|
||||
|
||||
class TestUniverInstanceService {
|
||||
private readonly _focusedUnit = new TestFocusedUnit();
|
||||
|
||||
getFocusedUnit() {
|
||||
return this._focusedUnit;
|
||||
}
|
||||
}
|
||||
|
||||
function createOperationTestBed() {
|
||||
const injector = new Injector();
|
||||
injector.add([ICommandService, { useClass: CommandService }]);
|
||||
injector.add([ILogService, { useClass: DesktopLogService }]);
|
||||
injector.add([IContextService, { useClass: ContextService }]);
|
||||
injector.add([IConfigService, { useClass: ConfigService }]);
|
||||
injector.add([ILocalFileService, { useClass: TestLocalFileService }]);
|
||||
injector.add([IUniverInstanceService, { useClass: TestUniverInstanceService as never }]);
|
||||
injector.add([ActionRecorderService]);
|
||||
|
||||
injector.get(ILogService).setLogLevel(LogLevel.SILENT);
|
||||
|
||||
const commandService = injector.get(ICommandService);
|
||||
commandService.registerCommand(OpenRecordPanelOperation);
|
||||
commandService.registerCommand(CloseRecordPanelOperation);
|
||||
|
||||
return {
|
||||
commandService,
|
||||
recorderService: injector.get(ActionRecorderService),
|
||||
};
|
||||
}
|
||||
|
||||
describe('action-recorder panel operations', () => {
|
||||
beforeEach(() => {
|
||||
TestLocalFileService.reset();
|
||||
});
|
||||
|
||||
it('opens the record panel and closes an active recording session without exporting', async () => {
|
||||
const { commandService, recorderService } = createOperationTestBed();
|
||||
const panelStates: boolean[] = [];
|
||||
recorderService.panelOpened$.subscribe((state) => panelStates.push(state));
|
||||
|
||||
await commandService.executeCommand(OpenRecordPanelOperation.id);
|
||||
recorderService.startRecording();
|
||||
await commandService.executeCommand(CloseRecordPanelOperation.id);
|
||||
|
||||
expect(panelStates).toEqual([false, true, false]);
|
||||
expect(recorderService.recording).toBe(false);
|
||||
expect(TestLocalFileService.downloads).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { IOperation } from '@univerjs/core';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
import { ActionRecorderService } from '../../services/action-recorder.service';
|
||||
|
||||
export const OpenRecordPanelOperation: IOperation = {
|
||||
id: 'action-recorder.operation.open-panel',
|
||||
type: CommandType.OPERATION,
|
||||
handler(accessor) {
|
||||
const s = accessor.get(ActionRecorderService);
|
||||
s.togglePanel(true);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const CloseRecordPanelOperation: IOperation = {
|
||||
id: 'action-recorder.operation.close-panel',
|
||||
type: CommandType.OPERATION,
|
||||
handler(accessor) {
|
||||
const s = accessor.get(ActionRecorderService);
|
||||
s.togglePanel(false);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
28
packages/action-recorder/src/config/config.ts
Normal file
28
packages/action-recorder/src/config/config.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { MenuConfig } from '@univerjs/ui';
|
||||
|
||||
export const ACTION_RECORDER_PLUGIN_CONFIG_KEY = 'action-recorder.config';
|
||||
|
||||
export const configSymbol = Symbol(ACTION_RECORDER_PLUGIN_CONFIG_KEY);
|
||||
|
||||
export interface IUniverActionRecorderConfig {
|
||||
menu?: MenuConfig;
|
||||
replayOnly?: boolean;
|
||||
}
|
||||
|
||||
export const defaultPluginConfig: IUniverActionRecorderConfig = {};
|
||||
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { Disposable, ICommandService, Inject, Injector } from '@univerjs/core';
|
||||
import {
|
||||
AddWorksheetMergeAllCommand,
|
||||
AddWorksheetMergeCommand,
|
||||
AddWorksheetMergeHorizontalCommand,
|
||||
AddWorksheetMergeVerticalCommand,
|
||||
AutoFillCommand,
|
||||
CancelFrozenCommand,
|
||||
CopySheetCommand,
|
||||
DeleteRangeMoveLeftCommand,
|
||||
DeleteRangeMoveUpCommand,
|
||||
DeltaColumnWidthCommand,
|
||||
DeltaRowHeightCommand,
|
||||
InsertColAfterCommand,
|
||||
InsertColBeforeCommand,
|
||||
InsertRowAfterCommand,
|
||||
InsertRowBeforeCommand,
|
||||
InsertSheetCommand,
|
||||
RefillCommand,
|
||||
RemoveSheetCommand,
|
||||
SetFrozenCommand,
|
||||
SetHorizontalTextAlignCommand,
|
||||
SetOverlineCommand,
|
||||
SetRangeValuesCommand,
|
||||
SetSelectionsOperation,
|
||||
SetStrikeThroughCommand,
|
||||
SetStyleCommand,
|
||||
SetTextColorCommand,
|
||||
SetTextRotationCommand,
|
||||
SetTextWrapCommand,
|
||||
SetVerticalTextAlignCommand,
|
||||
SetWorksheetActivateCommand,
|
||||
SetWorksheetActiveOperation,
|
||||
} from '@univerjs/sheets';
|
||||
import { RemoveSheetFilterCommand, SetSheetFilterRangeCommand, SetSheetsFilterCriteriaCommand } from '@univerjs/sheets-filter';
|
||||
import {
|
||||
SetRangeBoldCommand,
|
||||
SetRangeFontFamilyCommand,
|
||||
SetRangeFontSizeCommand,
|
||||
SetRangeItalicCommand,
|
||||
SetRangeStrickThroughCommand,
|
||||
SetRangeSubscriptCommand,
|
||||
SetRangeSuperscriptCommand,
|
||||
SetRangeTextColorCommand,
|
||||
SetRangeUnderlineCommand,
|
||||
SheetCopyCommand,
|
||||
SheetCutCommand,
|
||||
SheetPasteBesidesBorderCommand,
|
||||
SheetPasteColWidthCommand,
|
||||
SheetPasteCommand,
|
||||
SheetPasteFormatCommand,
|
||||
SheetPasteShortKeyCommand,
|
||||
SheetPasteValueCommand,
|
||||
} from '@univerjs/sheets-ui';
|
||||
import {
|
||||
BuiltInUIPart,
|
||||
connectInjector,
|
||||
IMenuManagerService,
|
||||
IUIPartsService,
|
||||
} from '@univerjs/ui';
|
||||
import { CompleteRecordingActionCommand, StartRecordingActionCommand, StopRecordingActionCommand } from '../commands/commands/record.command';
|
||||
import { ReplayLocalRecordCommand, ReplayLocalRecordOnActiveCommand, ReplayLocalRecordOnNamesakeCommand } from '../commands/commands/replay.command';
|
||||
import { CloseRecordPanelOperation, OpenRecordPanelOperation } from '../commands/operations/operation';
|
||||
import { menuSchema } from '../menu/action-recorder.menu';
|
||||
import { ActionRecorderService } from '../services/action-recorder.service';
|
||||
import { RecorderPanel } from '../views/components/RecorderPanel';
|
||||
|
||||
export class ActionRecorderController extends Disposable {
|
||||
constructor(
|
||||
@ICommandService private readonly _commandSrv: ICommandService,
|
||||
@IUIPartsService private readonly _uiPartsSrv: IUIPartsService,
|
||||
@IMenuManagerService private readonly _menuManagerService: IMenuManagerService,
|
||||
@Inject(ActionRecorderService) private readonly _actionRecorderService: ActionRecorderService,
|
||||
@Inject(Injector) private readonly _injector: Injector
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initCommands();
|
||||
this._initUI();
|
||||
this._initSheetsCommands();
|
||||
this._initDocsCommands();
|
||||
}
|
||||
|
||||
private _initCommands(): void {
|
||||
([
|
||||
StartRecordingActionCommand,
|
||||
StopRecordingActionCommand,
|
||||
CompleteRecordingActionCommand,
|
||||
OpenRecordPanelOperation,
|
||||
CloseRecordPanelOperation,
|
||||
ReplayLocalRecordCommand,
|
||||
ReplayLocalRecordOnNamesakeCommand,
|
||||
ReplayLocalRecordOnActiveCommand,
|
||||
]).forEach((command) => this._commandSrv.registerCommand(command));
|
||||
}
|
||||
|
||||
private _initUI(): void {
|
||||
this._uiPartsSrv.registerComponent(BuiltInUIPart.GLOBAL, () => connectInjector(RecorderPanel, this._injector));
|
||||
this._menuManagerService.mergeMenu(menuSchema);
|
||||
}
|
||||
|
||||
private _initSheetsCommands(): void {
|
||||
([
|
||||
// InsertColCommand,
|
||||
// InsertRowCommand,
|
||||
// #region basic commands
|
||||
CopySheetCommand,
|
||||
DeleteRangeMoveLeftCommand,
|
||||
DeleteRangeMoveUpCommand,
|
||||
DeltaColumnWidthCommand,
|
||||
DeltaRowHeightCommand,
|
||||
InsertSheetCommand,
|
||||
InsertColAfterCommand,
|
||||
InsertColBeforeCommand,
|
||||
InsertRowAfterCommand,
|
||||
InsertRowBeforeCommand,
|
||||
RemoveSheetCommand,
|
||||
SetStyleCommand,
|
||||
AddWorksheetMergeCommand,
|
||||
AddWorksheetMergeAllCommand,
|
||||
AddWorksheetMergeVerticalCommand,
|
||||
AddWorksheetMergeHorizontalCommand,
|
||||
// ResetBackgroundColorCommand,
|
||||
// ResetTextColorCommand,
|
||||
// SetBackgroundColorCommand,
|
||||
// SetBoldCommand,
|
||||
// SetFontFamilyCommand,
|
||||
// SetFontSizeCommand,
|
||||
SetFrozenCommand,
|
||||
CancelFrozenCommand,
|
||||
SetHorizontalTextAlignCommand,
|
||||
// SetItalicCommand,
|
||||
SetOverlineCommand,
|
||||
SetRangeBoldCommand,
|
||||
SetRangeFontFamilyCommand,
|
||||
SetRangeFontSizeCommand,
|
||||
SetRangeItalicCommand,
|
||||
SetRangeStrickThroughCommand,
|
||||
SetRangeSubscriptCommand,
|
||||
SetRangeSuperscriptCommand,
|
||||
SetRangeTextColorCommand,
|
||||
SetRangeUnderlineCommand,
|
||||
SetRangeValuesCommand,
|
||||
SetStrikeThroughCommand,
|
||||
SetTextColorCommand,
|
||||
SetTextRotationCommand,
|
||||
SetTextWrapCommand,
|
||||
// SetUnderlineCommand,
|
||||
SetVerticalTextAlignCommand,
|
||||
SheetCopyCommand,
|
||||
SheetCutCommand,
|
||||
SheetPasteBesidesBorderCommand,
|
||||
SheetPasteColWidthCommand,
|
||||
SheetPasteCommand,
|
||||
SheetPasteFormatCommand,
|
||||
SheetPasteShortKeyCommand,
|
||||
SheetPasteValueCommand,
|
||||
AutoFillCommand,
|
||||
RefillCommand,
|
||||
|
||||
SetWorksheetActivateCommand,
|
||||
SetWorksheetActiveOperation,
|
||||
SetSelectionsOperation,
|
||||
// #endregion
|
||||
|
||||
// #region data validation command
|
||||
// #endregion
|
||||
|
||||
// #region conditional formatting command
|
||||
// #endregion
|
||||
|
||||
// #region filter command
|
||||
SetSheetFilterRangeCommand,
|
||||
SetSheetsFilterCriteriaCommand,
|
||||
RemoveSheetFilterCommand,
|
||||
// #endregion
|
||||
]).forEach((command) => this._actionRecorderService.registerRecordedCommand(command));
|
||||
}
|
||||
|
||||
private _initDocsCommands(): void {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { Disposable, Inject } from '@univerjs/core';
|
||||
import { RecordIcon } from '@univerjs/icons';
|
||||
import { ComponentManager, IconManager } from '@univerjs/ui';
|
||||
|
||||
export class ComponentsController extends Disposable {
|
||||
constructor(
|
||||
@Inject(ComponentManager) private readonly _componentManager: ComponentManager,
|
||||
@Inject(IconManager) private readonly _iconManager: IconManager
|
||||
) {
|
||||
super();
|
||||
|
||||
this._registerIcons();
|
||||
}
|
||||
|
||||
private _registerIcons(): void {
|
||||
this.disposeWithMe(this._iconManager.register({
|
||||
RecordIcon,
|
||||
}));
|
||||
}
|
||||
}
|
||||
2
packages/action-recorder/src/global.css
Normal file
2
packages/action-recorder/src/global.css
Normal file
@@ -0,0 +1,2 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
23
packages/action-recorder/src/index.ts
Normal file
23
packages/action-recorder/src/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 './global.css';
|
||||
|
||||
export type { IUniverActionRecorderConfig } from './config/config';
|
||||
export { menuSchema as ActionRecorderMenuSchema } from './menu/action-recorder.menu';
|
||||
export { UniverActionRecorderPlugin } from './plugin';
|
||||
export { ActionRecorderService } from './services/action-recorder.service';
|
||||
export { ActionReplayService } from './services/replay.service';
|
||||
40
packages/action-recorder/src/locale/ar-SA.ts
Normal file
40
packages/action-recorder/src/locale/ar-SA.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'action-recorder': {
|
||||
panel: {
|
||||
recording: 'جارٍ التسجيل...',
|
||||
startRecording: 'بدء التسجيل',
|
||||
cancel: 'إلغاء',
|
||||
close: 'إغلاق',
|
||||
save: 'حفظ',
|
||||
start: 'بدء',
|
||||
startWithId: 'بدء (N)',
|
||||
},
|
||||
menu: {
|
||||
title: 'تسجيل الإجراءات',
|
||||
record: 'تسجيل الإجراءات...',
|
||||
'replay-local': 'استبدال السجل المحلي...',
|
||||
'replay-local-name': 'استبدال السجل المحلي بوحدة فرعية...',
|
||||
'replay-local-active': 'استبدال السجل المحلي بالوحدة الفرعية الحالية...',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
40
packages/action-recorder/src/locale/ca-ES.ts
Normal file
40
packages/action-recorder/src/locale/ca-ES.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'action-recorder': {
|
||||
panel: {
|
||||
recording: 'S\'està enregistrant...',
|
||||
startRecording: 'Inicia l’enregistrament',
|
||||
cancel: 'Cancel·la',
|
||||
close: 'Tanca',
|
||||
save: 'Desa',
|
||||
start: 'Inicia',
|
||||
startWithId: 'Inicia (N)',
|
||||
},
|
||||
menu: {
|
||||
title: 'Grava accions',
|
||||
record: 'Grava accions...',
|
||||
'replay-local': 'Reemplaça gravació local...',
|
||||
'replay-local-name': 'Reemplaça gravació local per subunitat...',
|
||||
'replay-local-active': 'Reemplaça gravació local per subunitat actual...',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
40
packages/action-recorder/src/locale/de-DE.ts
Normal file
40
packages/action-recorder/src/locale/de-DE.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'action-recorder': {
|
||||
panel: {
|
||||
recording: 'Aufnahme läuft...',
|
||||
startRecording: 'Aufnahme starten',
|
||||
cancel: 'Abbrechen',
|
||||
close: 'Schließen',
|
||||
save: 'Speichern',
|
||||
start: 'Starten',
|
||||
startWithId: 'Starten (N)',
|
||||
},
|
||||
menu: {
|
||||
title: 'Aktionen aufzeichnen',
|
||||
record: 'Aktionen aufzeichnen...',
|
||||
'replay-local': 'Lokale Aufzeichnung ersetzen...',
|
||||
'replay-local-name': 'Lokale Aufzeichnung nach Untereinheit ersetzen...',
|
||||
'replay-local-active': 'Lokale Aufzeichnung nach aktueller Untereinheit ersetzen...',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
38
packages/action-recorder/src/locale/en-US.ts
Normal file
38
packages/action-recorder/src/locale/en-US.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const locale = {
|
||||
'action-recorder': {
|
||||
panel: {
|
||||
recording: 'Recording...',
|
||||
startRecording: 'Start Recording',
|
||||
cancel: 'Cancel',
|
||||
close: 'Close',
|
||||
save: 'Save',
|
||||
start: 'Start',
|
||||
startWithId: 'Start(N)',
|
||||
},
|
||||
menu: {
|
||||
title: 'Record Actions',
|
||||
record: 'Record Actions...',
|
||||
'replay-local': 'Replace Local Record...',
|
||||
'replay-local-name': 'Replace Local Record by Subunit...',
|
||||
'replay-local-active': 'Replace Local Record by Current Subunit...',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
40
packages/action-recorder/src/locale/es-ES.ts
Normal file
40
packages/action-recorder/src/locale/es-ES.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'action-recorder': {
|
||||
panel: {
|
||||
recording: 'Grabando...',
|
||||
startRecording: 'Iniciar grabación',
|
||||
cancel: 'Cancelar',
|
||||
close: 'Cerrar',
|
||||
save: 'Guardar',
|
||||
start: 'Iniciar',
|
||||
startWithId: 'Iniciar (N)',
|
||||
},
|
||||
menu: {
|
||||
title: 'Grabar acciones',
|
||||
record: 'Grabar acciones...',
|
||||
'replay-local': 'Reemplazar grabación local...',
|
||||
'replay-local-name': 'Reemplazar grabación local por subunidad...',
|
||||
'replay-local-active': 'Reemplazar grabación local por subunidad actual...',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
40
packages/action-recorder/src/locale/fa-IR.ts
Normal file
40
packages/action-recorder/src/locale/fa-IR.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'action-recorder': {
|
||||
panel: {
|
||||
recording: 'در حال ضبط...',
|
||||
startRecording: 'شروع ضبط',
|
||||
cancel: 'لغو',
|
||||
close: 'بستن',
|
||||
save: 'ذخیره',
|
||||
start: 'شروع',
|
||||
startWithId: 'شروع (N)',
|
||||
},
|
||||
menu: {
|
||||
title: 'ثبت اقدامات',
|
||||
record: 'ثبت اقدامات...',
|
||||
'replay-local': 'بازپخش محلی ثبت...',
|
||||
'replay-local-name': 'بازپخش محلی ثبت بر اساس نام زیر واحد...',
|
||||
'replay-local-active': 'بازپخش محلی ثبت بر اساس زیر واحد فعلی...',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
40
packages/action-recorder/src/locale/fr-FR.ts
Normal file
40
packages/action-recorder/src/locale/fr-FR.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'action-recorder': {
|
||||
panel: {
|
||||
recording: 'Enregistrement...',
|
||||
startRecording: 'Démarrer l’enregistrement',
|
||||
cancel: 'Annuler',
|
||||
close: 'Fermer',
|
||||
save: 'Enregistrer',
|
||||
start: 'Démarrer',
|
||||
startWithId: 'Démarrer (N)',
|
||||
},
|
||||
menu: {
|
||||
title: 'Enregistrer les actions',
|
||||
record: 'Enregistrer les actions...',
|
||||
'replay-local': 'Lecture de l\'enregistrement local...',
|
||||
'replay-local-name': 'Remplacer l\'enregistrement local par le sous-unité...',
|
||||
'replay-local-active': 'Remplacer l\'enregistrement local par le sous-unité actuel...',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
40
packages/action-recorder/src/locale/id-ID.ts
Normal file
40
packages/action-recorder/src/locale/id-ID.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'action-recorder': {
|
||||
panel: {
|
||||
recording: 'Merekam...',
|
||||
startRecording: 'Mulai merekam',
|
||||
cancel: 'Batal',
|
||||
close: 'Tutup',
|
||||
save: 'Simpan',
|
||||
start: 'Mulai',
|
||||
startWithId: 'Mulai (N)',
|
||||
},
|
||||
menu: {
|
||||
title: 'Rekam Aksi',
|
||||
record: 'Rekam Aksi...',
|
||||
'replay-local': 'Putar Ulang Rekaman Lokal...',
|
||||
'replay-local-name': 'Putar Ulang Rekaman Lokal berdasarkan Subunit...',
|
||||
'replay-local-active': 'Putar Ulang Rekaman Lokal berdasarkan Subunit Saat Ini...',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
40
packages/action-recorder/src/locale/it-IT.ts
Normal file
40
packages/action-recorder/src/locale/it-IT.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'action-recorder': {
|
||||
panel: {
|
||||
recording: 'Registrazione...',
|
||||
startRecording: 'Avvia registrazione',
|
||||
cancel: 'Annulla',
|
||||
close: 'Chiudi',
|
||||
save: 'Salva',
|
||||
start: 'Avvia',
|
||||
startWithId: 'Avvia (N)',
|
||||
},
|
||||
menu: {
|
||||
title: 'Registra Azioni',
|
||||
record: 'Registra Azioni...',
|
||||
'replay-local': 'Riproduci Registrazione Locale...',
|
||||
'replay-local-name': 'Riproduci Registrazione Locale per Sottoelemento...',
|
||||
'replay-local-active': 'Riproduci Registrazione Locale per Sottoelemento Corrente...',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
40
packages/action-recorder/src/locale/ja-JP.ts
Normal file
40
packages/action-recorder/src/locale/ja-JP.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'action-recorder': {
|
||||
panel: {
|
||||
recording: '記録中...',
|
||||
startRecording: '記録を開始',
|
||||
cancel: 'キャンセル',
|
||||
close: '閉じる',
|
||||
save: '保存',
|
||||
start: '開始',
|
||||
startWithId: '開始 (N)',
|
||||
},
|
||||
menu: {
|
||||
title: '操作記録',
|
||||
record: '記録を開始...',
|
||||
'replay-local': 'ローカルの記録で置き換え...',
|
||||
'replay-local-name': 'サブ単位ごとにローカルの記録で置き換え...',
|
||||
'replay-local-active': '現在のサブ単位をローカルの記録で置き換え...',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
40
packages/action-recorder/src/locale/ko-KR.ts
Normal file
40
packages/action-recorder/src/locale/ko-KR.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'action-recorder': {
|
||||
panel: {
|
||||
recording: '기록 중...',
|
||||
startRecording: '기록 시작',
|
||||
cancel: '취소',
|
||||
close: '닫기',
|
||||
save: '저장',
|
||||
start: '시작',
|
||||
startWithId: '시작 (N)',
|
||||
},
|
||||
menu: {
|
||||
title: '동작 기록',
|
||||
record: '동작 기록 시작...',
|
||||
'replay-local': '로컬 기록 대체...',
|
||||
'replay-local-name': '하위 단위로 로컬 기록 대체...',
|
||||
'replay-local-active': '현재 하위 단위로 로컬 기록 대체...',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
40
packages/action-recorder/src/locale/pl-PL.ts
Normal file
40
packages/action-recorder/src/locale/pl-PL.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'action-recorder': {
|
||||
panel: {
|
||||
recording: 'Nagrywanie...',
|
||||
startRecording: 'Rozpocznij nagrywanie',
|
||||
cancel: 'Anuluj',
|
||||
close: 'Zamknij',
|
||||
save: 'Zapisz',
|
||||
start: 'Rozpocznij',
|
||||
startWithId: 'Rozpocznij (N)',
|
||||
},
|
||||
menu: {
|
||||
title: 'Nagraj działania',
|
||||
record: 'Nagraj działania...',
|
||||
'replay-local': 'Odtwórz nagranie lokalne...',
|
||||
'replay-local-name': 'Odtwórz nagranie lokalne według jednostki podrzędnej...',
|
||||
'replay-local-active': 'Odtwórz nagranie lokalne według bieżącej jednostki podrzędnej...',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
40
packages/action-recorder/src/locale/pt-BR.ts
Normal file
40
packages/action-recorder/src/locale/pt-BR.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'action-recorder': {
|
||||
panel: {
|
||||
recording: 'Gravando...',
|
||||
startRecording: 'Iniciar gravação',
|
||||
cancel: 'Cancelar',
|
||||
close: 'Fechar',
|
||||
save: 'Salvar',
|
||||
start: 'Iniciar',
|
||||
startWithId: 'Iniciar (N)',
|
||||
},
|
||||
menu: {
|
||||
title: 'Gravar Ações',
|
||||
record: 'Gravar Ações...',
|
||||
'replay-local': 'Substituir Gravação Local...',
|
||||
'replay-local-name': 'Substituir Gravação Local por Subunidade...',
|
||||
'replay-local-active': 'Substituir Gravação Local pela Subunidade Atual...',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
40
packages/action-recorder/src/locale/ru-RU.ts
Normal file
40
packages/action-recorder/src/locale/ru-RU.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'action-recorder': {
|
||||
panel: {
|
||||
recording: 'Запись...',
|
||||
startRecording: 'Начать запись',
|
||||
cancel: 'Отмена',
|
||||
close: 'Закрыть',
|
||||
save: 'Сохранить',
|
||||
start: 'Начать',
|
||||
startWithId: 'Начать (N)',
|
||||
},
|
||||
menu: {
|
||||
title: 'Запись действий',
|
||||
record: 'Записать действия...',
|
||||
'replay-local': 'Заменить локальную запись...',
|
||||
'replay-local-name': 'Заменить локальную запись по подразделению...',
|
||||
'replay-local-active': 'Заменить локальную запись по текущему подразделению...',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
40
packages/action-recorder/src/locale/sk-SK.ts
Normal file
40
packages/action-recorder/src/locale/sk-SK.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'action-recorder': {
|
||||
panel: {
|
||||
recording: 'Nahrávanie...',
|
||||
startRecording: 'Spustiť nahrávanie',
|
||||
cancel: 'Zrušiť',
|
||||
close: 'Zavrieť',
|
||||
save: 'Uložiť',
|
||||
start: 'Spustiť',
|
||||
startWithId: 'Spustiť (N)',
|
||||
},
|
||||
menu: {
|
||||
title: 'Zaznamenať akcie',
|
||||
record: 'Zaznamenať akcie...',
|
||||
'replay-local': 'Nahradiť miestny záznam...',
|
||||
'replay-local-name': 'Nahradiť miestny záznam podľa podjednotky...',
|
||||
'replay-local-active': 'Nahradiť miestny záznam podľa aktuálnej podjednotky...',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
20
packages/action-recorder/src/locale/types.ts
Normal file
20
packages/action-recorder/src/locale/types.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { LocaleLeafKeys } from '@univerjs/core';
|
||||
import type enUS from './en-US';
|
||||
|
||||
export type LocaleKey = LocaleLeafKeys<typeof enUS>;
|
||||
40
packages/action-recorder/src/locale/vi-VN.ts
Normal file
40
packages/action-recorder/src/locale/vi-VN.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'action-recorder': {
|
||||
panel: {
|
||||
recording: 'Đang ghi...',
|
||||
startRecording: 'Bắt đầu ghi',
|
||||
cancel: 'Hủy',
|
||||
close: 'Đóng',
|
||||
save: 'Lưu',
|
||||
start: 'Bắt đầu',
|
||||
startWithId: 'Bắt đầu (N)',
|
||||
},
|
||||
menu: {
|
||||
title: 'Ghi hành động',
|
||||
record: 'Ghi hành động...',
|
||||
'replay-local': 'Thay bản ghi cục bộ...',
|
||||
'replay-local-name': 'Thay bản ghi cục bộ theo bộ phận...',
|
||||
'replay-local-active': 'Thay bản ghi cục bộ theo bộ phận hiện tại...',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
40
packages/action-recorder/src/locale/zh-CN.ts
Normal file
40
packages/action-recorder/src/locale/zh-CN.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'action-recorder': {
|
||||
panel: {
|
||||
recording: '录制中...',
|
||||
startRecording: '开始录制',
|
||||
cancel: '取消',
|
||||
close: '关闭',
|
||||
save: '保存',
|
||||
start: '开始',
|
||||
startWithId: '开始 (N)',
|
||||
},
|
||||
menu: {
|
||||
title: '操作录制',
|
||||
record: '录制操作...',
|
||||
'replay-local': '回放本地录制...',
|
||||
'replay-local-name': '根据子表名回放本地录制...',
|
||||
'replay-local-active': '根据当前子表回放本地录制...',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
40
packages/action-recorder/src/locale/zh-HK.ts
Normal file
40
packages/action-recorder/src/locale/zh-HK.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'action-recorder': {
|
||||
panel: {
|
||||
recording: '錄製中...',
|
||||
startRecording: '開始錄製',
|
||||
cancel: '取消',
|
||||
close: '關閉',
|
||||
save: '儲存',
|
||||
start: '開始',
|
||||
startWithId: '開始 (N)',
|
||||
},
|
||||
menu: {
|
||||
title: '操作錄製',
|
||||
record: '錄製操作...',
|
||||
'replay-local': '回放本地錄製...',
|
||||
'replay-local-name': '根據子表名回放本地錄製...',
|
||||
'replay-local-active': '根據當前子表回放本地錄製...',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
40
packages/action-recorder/src/locale/zh-TW.ts
Normal file
40
packages/action-recorder/src/locale/zh-TW.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'action-recorder': {
|
||||
panel: {
|
||||
recording: '錄製中...',
|
||||
startRecording: '開始錄製',
|
||||
cancel: '取消',
|
||||
close: '關閉',
|
||||
save: '儲存',
|
||||
start: '開始',
|
||||
startWithId: '開始 (N)',
|
||||
},
|
||||
menu: {
|
||||
title: '操作錄製',
|
||||
record: '錄製操作...',
|
||||
'replay-local': '回放本地錄製...',
|
||||
'replay-local-name': '根據子表名回放本地錄製...',
|
||||
'replay-local-active': '根據當前子表回放本地錄製...',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
98
packages/action-recorder/src/menu/action-recorder.menu.ts
Normal file
98
packages/action-recorder/src/menu/action-recorder.menu.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { IAccessor } from '@univerjs/core';
|
||||
import type { IMenuButtonItem, IMenuSelectorItem, MenuSchemaType } from '@univerjs/ui';
|
||||
import type { LocaleKey } from '../locale/types';
|
||||
import { MenuItemType, RibbonOthersGroup } 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';
|
||||
|
||||
export const RECORD_MENU_ITEM_ID = 'RECORD_MENU_ITEM';
|
||||
export function RecordMenuItemFactory(): IMenuSelectorItem<LocaleKey> {
|
||||
return {
|
||||
id: RECORD_MENU_ITEM_ID,
|
||||
type: MenuItemType.SUBITEMS,
|
||||
icon: 'RecordIcon',
|
||||
tooltip: 'action-recorder.menu.title',
|
||||
};
|
||||
}
|
||||
|
||||
export function OpenRecorderMenuItemFactory(accessor: IAccessor): IMenuButtonItem<LocaleKey> {
|
||||
const actionRecorderService = accessor.get(ActionRecorderService);
|
||||
|
||||
return {
|
||||
id: OpenRecordPanelOperation.id,
|
||||
title: 'action-recorder.menu.record',
|
||||
type: MenuItemType.BUTTON,
|
||||
disabled$: actionRecorderService.panelOpened$,
|
||||
};
|
||||
}
|
||||
|
||||
export function ReplayLocalRecordMenuItemFactory(): IMenuButtonItem<LocaleKey> {
|
||||
return {
|
||||
id: ReplayLocalRecordCommand.id,
|
||||
title: 'action-recorder.menu.replay-local',
|
||||
type: MenuItemType.BUTTON,
|
||||
};
|
||||
}
|
||||
|
||||
export function ReplayLocalRecordOnNamesakeMenuItemFactory(): IMenuButtonItem<LocaleKey> {
|
||||
return {
|
||||
id: ReplayLocalRecordOnNamesakeCommand.id,
|
||||
title: 'action-recorder.menu.replay-local-name',
|
||||
type: MenuItemType.BUTTON,
|
||||
};
|
||||
}
|
||||
|
||||
export function ReplayLocalRecordOnActiveMenuItemFactory(): IMenuButtonItem<LocaleKey> {
|
||||
return {
|
||||
id: ReplayLocalRecordOnActiveCommand.id,
|
||||
title: 'action-recorder.menu.replay-local-active',
|
||||
type: MenuItemType.BUTTON,
|
||||
};
|
||||
}
|
||||
|
||||
export const menuSchema: MenuSchemaType = {
|
||||
[RibbonOthersGroup.OTHERS]: {
|
||||
[RECORD_MENU_ITEM_ID]: {
|
||||
order: 1,
|
||||
menuItemFactory: RecordMenuItemFactory,
|
||||
[OpenRecordPanelOperation.id]: {
|
||||
order: 1,
|
||||
menuItemFactory: OpenRecorderMenuItemFactory,
|
||||
},
|
||||
[ReplayLocalRecordCommand.id]: {
|
||||
order: 2,
|
||||
menuItemFactory: ReplayLocalRecordMenuItemFactory,
|
||||
},
|
||||
[ReplayLocalRecordOnNamesakeCommand.id]: {
|
||||
order: 3,
|
||||
menuItemFactory: ReplayLocalRecordOnNamesakeMenuItemFactory,
|
||||
},
|
||||
[ReplayLocalRecordOnActiveCommand.id]: {
|
||||
order: 4,
|
||||
menuItemFactory: ReplayLocalRecordOnActiveMenuItemFactory,
|
||||
},
|
||||
},
|
||||
|
||||
},
|
||||
};
|
||||
75
packages/action-recorder/src/plugin.ts
Normal file
75
packages/action-recorder/src/plugin.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { Dependency } from '@univerjs/core';
|
||||
import type { IUniverActionRecorderConfig } from './config/config';
|
||||
import { IConfigService, Inject, Injector, merge, Plugin } from '@univerjs/core';
|
||||
import pkg from '../package.json';
|
||||
import { ACTION_RECORDER_PLUGIN_CONFIG_KEY, defaultPluginConfig } from './config/config';
|
||||
import { ActionRecorderController } from './controllers/action-recorder.controller';
|
||||
import { ComponentsController } from './controllers/components.controller';
|
||||
import { ActionRecorderService } from './services/action-recorder.service';
|
||||
import { ActionReplayService } from './services/replay.service';
|
||||
|
||||
/**
|
||||
* This plugin provides a recorder for user's interactions with Univer,
|
||||
* it only records commands (and some special operations) so that it can be replayed later.
|
||||
*/
|
||||
|
||||
export class UniverActionRecorderPlugin extends Plugin {
|
||||
static override pluginName = 'UNIVER_ACTION_RECORDER_PLUGIN';
|
||||
static override packageName = pkg.name;
|
||||
static override version = pkg.version;
|
||||
|
||||
constructor(
|
||||
private readonly _config: Partial<IUniverActionRecorderConfig> = defaultPluginConfig,
|
||||
@Inject(Injector) protected readonly _injector: Injector,
|
||||
@IConfigService private readonly _configService: IConfigService
|
||||
) {
|
||||
super();
|
||||
|
||||
// Manage the plugin configuration.
|
||||
const { menu, ...rest } = merge(
|
||||
{},
|
||||
defaultPluginConfig,
|
||||
this._config
|
||||
);
|
||||
if (menu) {
|
||||
this._configService.setConfig('menu', menu, { merge: true });
|
||||
}
|
||||
this._configService.setConfig(ACTION_RECORDER_PLUGIN_CONFIG_KEY, rest);
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
this._injector.add([ComponentsController]);
|
||||
this._injector.get(ComponentsController);
|
||||
const dependency = this._config.replayOnly
|
||||
? [[ActionReplayService]]
|
||||
: [
|
||||
[ActionRecorderService],
|
||||
[ActionReplayService],
|
||||
[ActionRecorderController],
|
||||
];
|
||||
(dependency as Dependency[]).forEach((d) => this._injector.add(d));
|
||||
}
|
||||
|
||||
override onSteady(): void {
|
||||
if (this._config.replayOnly) {
|
||||
return;
|
||||
}
|
||||
this._injector.get(ActionRecorderController);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 {
|
||||
CommandService,
|
||||
CommandType,
|
||||
ConfigService,
|
||||
ContextService,
|
||||
DesktopLogService,
|
||||
ICommandService,
|
||||
IConfigService,
|
||||
IContextService,
|
||||
ILogService,
|
||||
Injector,
|
||||
IUniverInstanceService,
|
||||
} from '@univerjs/core';
|
||||
import { SetSelectionsOperation } from '@univerjs/sheets';
|
||||
import { DesktopLocalFileService, ILocalFileService } from '@univerjs/ui';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { ActionRecorderService } from '../action-recorder.service';
|
||||
|
||||
class TestUniverInstanceService {
|
||||
getFocusedUnit() {
|
||||
return { getUnitId: () => 'unit-1' };
|
||||
}
|
||||
|
||||
getUnit() {
|
||||
return {
|
||||
getSheetBySheetId: () => ({ getName: () => 'Sheet-A' }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
describe('ActionRecorderService', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('should record commands, replace selection entries and complete/stop', () => {
|
||||
const link = { click: vi.fn(), download: '', href: '' };
|
||||
vi.stubGlobal('document', { createElement: () => link });
|
||||
vi.stubGlobal('window', { URL: { createObjectURL: () => 'blob:recording' } });
|
||||
|
||||
const injector = new Injector();
|
||||
injector.add([ICommandService, { useClass: CommandService }]);
|
||||
injector.add([ILogService, { useClass: DesktopLogService }]);
|
||||
injector.add([IContextService, { useClass: ContextService }]);
|
||||
injector.add([IConfigService, { useClass: ConfigService }]);
|
||||
injector.add([ILocalFileService, { useClass: DesktopLocalFileService }]);
|
||||
injector.add([IUniverInstanceService, { useClass: TestUniverInstanceService as never }]);
|
||||
injector.add([ActionRecorderService]);
|
||||
const service = injector.get(ActionRecorderService);
|
||||
const commandService = injector.get(ICommandService);
|
||||
const logService = injector.get(ILogService);
|
||||
const logError = vi.spyOn(logService, 'error');
|
||||
|
||||
expect(() =>
|
||||
service.registerRecordedCommand({
|
||||
id: 'mutation-id',
|
||||
type: CommandType.MUTATION,
|
||||
} as never)
|
||||
).toThrow('[CommandRecorderService] Cannot record mutation commands.');
|
||||
|
||||
service.registerRecordedCommand({ id: 'cmd-1', type: CommandType.COMMAND } as never);
|
||||
service.registerRecordedCommand({ id: SetSelectionsOperation.id, type: CommandType.OPERATION } as never);
|
||||
commandService.registerCommand({
|
||||
id: 'cmd-1',
|
||||
type: CommandType.COMMAND,
|
||||
handler: () => true,
|
||||
});
|
||||
commandService.registerCommand({
|
||||
id: SetSelectionsOperation.id,
|
||||
type: CommandType.OPERATION,
|
||||
handler: () => true,
|
||||
});
|
||||
commandService.registerCommand({
|
||||
id: 'ignored',
|
||||
type: CommandType.COMMAND,
|
||||
handler: () => true,
|
||||
});
|
||||
|
||||
const panelStates: boolean[] = [];
|
||||
const recordingStates: boolean[] = [];
|
||||
const commandStates: string[][] = [];
|
||||
service.panelOpened$.subscribe((v) => panelStates.push(v));
|
||||
service.recording$.subscribe((v) => recordingStates.push(v));
|
||||
service.recordedCommands$.subscribe((v) => commandStates.push(v.map((cmd) => cmd.id)));
|
||||
|
||||
service.togglePanel(true);
|
||||
service.startRecording(true);
|
||||
expect(service.recording).toBe(true);
|
||||
expect(recordingStates[recordingStates.length - 1]).toBe(true);
|
||||
|
||||
commandService.syncExecuteCommand('cmd-1', {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
});
|
||||
commandService.syncExecuteCommand(SetSelectionsOperation.id, {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
mark: 1,
|
||||
});
|
||||
commandService.syncExecuteCommand(SetSelectionsOperation.id, {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
mark: 2,
|
||||
});
|
||||
commandService.syncExecuteCommand('ignored', {});
|
||||
|
||||
expect(commandStates[commandStates.length - 1]).toEqual(['cmd-1']);
|
||||
|
||||
service.completeRecording();
|
||||
expect(link.download).toBe('recorded-commands.json');
|
||||
expect(link.href).toBe('blob:recording');
|
||||
expect(link.click).toHaveBeenCalledTimes(1);
|
||||
expect(logError).toHaveBeenCalled();
|
||||
expect(recordingStates[recordingStates.length - 1]).toBe(false);
|
||||
|
||||
service.startRecording();
|
||||
commandService.syncExecuteCommand('cmd-1', {
|
||||
params: { unitId: 'unit-1', subUnitId: 'sheet-1' },
|
||||
});
|
||||
service.togglePanel(false);
|
||||
expect(panelStates[panelStates.length - 1]).toBe(false);
|
||||
expect(recordingStates[recordingStates.length - 1]).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,394 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { ICommandInfo } from '@univerjs/core';
|
||||
import {
|
||||
CommandService,
|
||||
CommandType,
|
||||
ConfigService,
|
||||
DesktopLogService,
|
||||
ICommandService,
|
||||
IConfigService,
|
||||
ILogService,
|
||||
Injector,
|
||||
IUniverInstanceService,
|
||||
} from '@univerjs/core';
|
||||
import { MessageType } from '@univerjs/design';
|
||||
import { ILocalFileService, IMessageService } from '@univerjs/ui';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { ActionReplayService, ReplayMode } from '../replay.service';
|
||||
|
||||
const APPLY_CELL_VALUE_COMMAND_ID = 'action-recorder.test.apply-cell-value';
|
||||
const STOP_REPLAY_COMMAND_ID = 'action-recorder.test.stop-replay';
|
||||
|
||||
interface IAppliedCommand {
|
||||
id: string;
|
||||
params?: {
|
||||
unitId?: string;
|
||||
subUnitId?: string;
|
||||
value?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ITestFile {
|
||||
name: string;
|
||||
text(): Promise<string>;
|
||||
}
|
||||
|
||||
class TestReplayState {
|
||||
static appliedCommands: IAppliedCommand[] = [];
|
||||
|
||||
static reset() {
|
||||
this.appliedCommands = [];
|
||||
}
|
||||
}
|
||||
|
||||
class TestLocalFileService {
|
||||
static files: ITestFile[] = [];
|
||||
|
||||
static reset() {
|
||||
this.files = [];
|
||||
}
|
||||
|
||||
openFile() {
|
||||
return Promise.resolve(TestLocalFileService.files);
|
||||
}
|
||||
}
|
||||
|
||||
class TestMessageService {
|
||||
static messages: Array<{ type: MessageType; content: string }> = [];
|
||||
|
||||
static reset() {
|
||||
this.messages = [];
|
||||
}
|
||||
|
||||
show(message: { type: MessageType; content: string }) {
|
||||
TestMessageService.messages.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
class TestWorkbook {
|
||||
getUnitId() {
|
||||
return 'focused-workbook';
|
||||
}
|
||||
|
||||
getSheetBySheetName(sheetName: string) {
|
||||
if (sheetName !== 'Recorded Sheet') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { getSheetId: () => 'actual-sheet-id' };
|
||||
}
|
||||
|
||||
getActiveSheet() {
|
||||
return { getSheetId: () => 'active-sheet-id' };
|
||||
}
|
||||
}
|
||||
|
||||
class TestUniverInstanceService {
|
||||
private readonly _workbook = new TestWorkbook();
|
||||
|
||||
getFocusedUnit() {
|
||||
return this._workbook;
|
||||
}
|
||||
}
|
||||
|
||||
class TestUniverInstanceServiceWithoutFocusedUnit {
|
||||
getFocusedUnit() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function cloneCommandParams(params?: IAppliedCommand['params']) {
|
||||
if (!params) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { ...params };
|
||||
}
|
||||
|
||||
function createJsonFile(name: string, commands: ICommandInfo[]): ITestFile {
|
||||
return {
|
||||
name,
|
||||
text: () => Promise.resolve(JSON.stringify(commands)),
|
||||
};
|
||||
}
|
||||
|
||||
function createInvalidJsonFile(name: string): ITestFile {
|
||||
return {
|
||||
name,
|
||||
text: () => Promise.resolve('{ invalid json'),
|
||||
};
|
||||
}
|
||||
|
||||
function createReplayTestBed(instanceService: typeof TestUniverInstanceService | typeof TestUniverInstanceServiceWithoutFocusedUnit = TestUniverInstanceService) {
|
||||
const injector = new Injector();
|
||||
injector.add([ICommandService, { useClass: CommandService }]);
|
||||
injector.add([IConfigService, { useClass: ConfigService }]);
|
||||
injector.add([ILogService, { useClass: DesktopLogService }]);
|
||||
injector.add([IMessageService, { useClass: TestMessageService as never }]);
|
||||
injector.add([ILocalFileService, { useClass: TestLocalFileService as never }]);
|
||||
injector.add([IUniverInstanceService, { useClass: instanceService as never }]);
|
||||
injector.add([ActionReplayService]);
|
||||
|
||||
const commandService = injector.get(ICommandService);
|
||||
commandService.registerCommand({
|
||||
id: APPLY_CELL_VALUE_COMMAND_ID,
|
||||
type: CommandType.COMMAND,
|
||||
handler: (_accessor, params?: IAppliedCommand['params']) => {
|
||||
TestReplayState.appliedCommands.push({
|
||||
id: APPLY_CELL_VALUE_COMMAND_ID,
|
||||
params: cloneCommandParams(params),
|
||||
});
|
||||
return true;
|
||||
},
|
||||
});
|
||||
commandService.registerCommand({
|
||||
id: STOP_REPLAY_COMMAND_ID,
|
||||
type: CommandType.COMMAND,
|
||||
handler: (_accessor, params?: IAppliedCommand['params']) => {
|
||||
TestReplayState.appliedCommands.push({
|
||||
id: STOP_REPLAY_COMMAND_ID,
|
||||
params: cloneCommandParams(params),
|
||||
});
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
commandService,
|
||||
replayService: injector.get(ActionReplayService),
|
||||
};
|
||||
}
|
||||
|
||||
describe('ActionReplayService', () => {
|
||||
beforeEach(() => {
|
||||
TestReplayState.reset();
|
||||
TestLocalFileService.reset();
|
||||
TestMessageService.reset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('replays recorded sheet actions into the focused workbook and resolves recorded sheet names', async () => {
|
||||
const { replayService } = createReplayTestBed();
|
||||
|
||||
const result = await replayService.replayCommands(
|
||||
[
|
||||
{
|
||||
id: APPLY_CELL_VALUE_COMMAND_ID,
|
||||
params: {
|
||||
unitId: 'recorded-workbook',
|
||||
subUnitId: 'Recorded Sheet',
|
||||
value: 'replayed by sheet name',
|
||||
},
|
||||
},
|
||||
],
|
||||
{ mode: ReplayMode.NAME }
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(TestReplayState.appliedCommands).toEqual([
|
||||
{
|
||||
id: APPLY_CELL_VALUE_COMMAND_ID,
|
||||
params: {
|
||||
unitId: 'focused-workbook',
|
||||
subUnitId: 'actual-sheet-id',
|
||||
value: 'replayed by sheet name',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('can replay recorded actions into the active sheet instead of the recorded sheet', async () => {
|
||||
const { replayService } = createReplayTestBed();
|
||||
|
||||
const result = await replayService.replayCommands(
|
||||
[
|
||||
{
|
||||
id: APPLY_CELL_VALUE_COMMAND_ID,
|
||||
params: {
|
||||
unitId: 'recorded-workbook',
|
||||
subUnitId: 'recorded-sheet-id',
|
||||
value: 'replayed into active sheet',
|
||||
},
|
||||
},
|
||||
],
|
||||
{ mode: ReplayMode.ACTIVE }
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(TestReplayState.appliedCommands).toEqual([
|
||||
{
|
||||
id: APPLY_CELL_VALUE_COMMAND_ID,
|
||||
params: {
|
||||
unitId: 'focused-workbook',
|
||||
subUnitId: 'active-sheet-id',
|
||||
value: 'replayed into active sheet',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('stops replaying after a command reports failure', async () => {
|
||||
const { replayService } = createReplayTestBed();
|
||||
|
||||
const result = await replayService.replayCommands([
|
||||
{
|
||||
id: STOP_REPLAY_COMMAND_ID,
|
||||
params: {
|
||||
unitId: 'recorded-workbook',
|
||||
subUnitId: 'recorded-sheet-id',
|
||||
value: 'cannot apply',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: APPLY_CELL_VALUE_COMMAND_ID,
|
||||
params: {
|
||||
unitId: 'recorded-workbook',
|
||||
subUnitId: 'recorded-sheet-id',
|
||||
value: 'must not be applied',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(TestReplayState.appliedCommands).toEqual([
|
||||
{
|
||||
id: STOP_REPLAY_COMMAND_ID,
|
||||
params: {
|
||||
unitId: 'focused-workbook',
|
||||
subUnitId: 'recorded-sheet-id',
|
||||
value: 'cannot apply',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('loads a local JSON recording and replays it against the focused workbook', async () => {
|
||||
const { replayService } = createReplayTestBed();
|
||||
TestLocalFileService.files = [
|
||||
createJsonFile('recording.json', [
|
||||
{
|
||||
id: APPLY_CELL_VALUE_COMMAND_ID,
|
||||
params: {
|
||||
unitId: 'recorded-workbook',
|
||||
subUnitId: 'Recorded Sheet',
|
||||
value: 'loaded from json',
|
||||
},
|
||||
},
|
||||
]),
|
||||
];
|
||||
|
||||
const result = await replayService.replayLocalJSON(ReplayMode.NAME);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(TestReplayState.appliedCommands).toEqual([
|
||||
{
|
||||
id: APPLY_CELL_VALUE_COMMAND_ID,
|
||||
params: {
|
||||
unitId: 'focused-workbook',
|
||||
subUnitId: 'actual-sheet-id',
|
||||
value: 'loaded from json',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not replay anything when the local recording cannot be parsed', async () => {
|
||||
const { replayService } = createReplayTestBed();
|
||||
TestLocalFileService.files = [createInvalidJsonFile('broken-recording.json')];
|
||||
|
||||
const result = await replayService.replayLocalJSON();
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(TestReplayState.appliedCommands).toEqual([]);
|
||||
expect(TestMessageService.messages).toEqual([
|
||||
{
|
||||
type: MessageType.Error,
|
||||
content: 'Failed to replay commands from local file broken-recording.json.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not replay anything when the user cancels local recording selection', async () => {
|
||||
const { replayService } = createReplayTestBed();
|
||||
TestLocalFileService.files = [];
|
||||
|
||||
const result = await replayService.replayLocalJSON();
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(TestReplayState.appliedCommands).toEqual([]);
|
||||
expect(TestMessageService.messages).toEqual([]);
|
||||
});
|
||||
|
||||
it('delays command execution while still targeting the focused workbook', async () => {
|
||||
vi.useFakeTimers();
|
||||
const { replayService } = createReplayTestBed();
|
||||
|
||||
const result = replayService.replayCommandsWithDelay([
|
||||
{
|
||||
id: APPLY_CELL_VALUE_COMMAND_ID,
|
||||
params: {
|
||||
unitId: 'recorded-workbook',
|
||||
subUnitId: 'recorded-sheet-id',
|
||||
value: 'delayed replay',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(TestReplayState.appliedCommands).toEqual([]);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
|
||||
await expect(result).resolves.toBe(true);
|
||||
expect(TestReplayState.appliedCommands).toEqual([
|
||||
{
|
||||
id: APPLY_CELL_VALUE_COMMAND_ID,
|
||||
params: {
|
||||
unitId: 'focused-workbook',
|
||||
subUnitId: 'recorded-sheet-id',
|
||||
value: 'delayed replay',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps replaying commands without workbook params when no workbook is focused', async () => {
|
||||
const { replayService } = createReplayTestBed(TestUniverInstanceServiceWithoutFocusedUnit);
|
||||
|
||||
const result = await replayService.replayCommands([
|
||||
{
|
||||
id: APPLY_CELL_VALUE_COMMAND_ID,
|
||||
params: {
|
||||
value: 'global action',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(TestReplayState.appliedCommands).toEqual([
|
||||
{
|
||||
id: APPLY_CELL_VALUE_COMMAND_ID,
|
||||
params: {
|
||||
value: 'global action',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
126
packages/action-recorder/src/services/action-recorder.service.ts
Normal file
126
packages/action-recorder/src/services/action-recorder.service.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { 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';
|
||||
|
||||
/**
|
||||
* This service is for recording commands. What commands should be recorded can be configured by other
|
||||
* plugins.
|
||||
*/
|
||||
export class ActionRecorderService extends Disposable {
|
||||
private readonly _shouldRecordCommands = new Set<string>();
|
||||
|
||||
private readonly _panelOpened$ = new BehaviorSubject<boolean>(false);
|
||||
readonly panelOpened$ = this._panelOpened$.asObservable();
|
||||
|
||||
private _recorder: Nullable<IDisposable> = null;
|
||||
|
||||
private readonly _recording$ = new BehaviorSubject<boolean>(false);
|
||||
readonly recording$ = this._recording$.asObservable();
|
||||
get recording(): boolean { return this._recording$.getValue(); }
|
||||
|
||||
private _recorded$ = new BehaviorSubject<ICommandInfo[]>([]);
|
||||
private get _recorded(): ICommandInfo[] { return this._recorded$.getValue(); }
|
||||
|
||||
private _recordedCommands$ = new BehaviorSubject<ICommandInfo[]>([]);
|
||||
readonly recordedCommands$ = this._recordedCommands$.asObservable();
|
||||
private get _recordedCommands(): ICommandInfo[] { return this._recordedCommands$.getValue(); }
|
||||
|
||||
constructor(
|
||||
@ICommandService private readonly _commandSrv: ICommandService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@ILocalFileService private readonly _localFileService: ILocalFileService,
|
||||
@IUniverInstanceService private readonly _instanceService: IUniverInstanceService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
registerRecordedCommand(command: ICommand): void {
|
||||
if (command.type === CommandType.MUTATION) throw new Error('[CommandRecorderService] Cannot record mutation commands.');
|
||||
this._shouldRecordCommands.add(command.id);
|
||||
}
|
||||
|
||||
togglePanel(visible: boolean): void {
|
||||
this._panelOpened$.next(visible);
|
||||
|
||||
if (visible === false) this.stopRecording();
|
||||
}
|
||||
|
||||
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 &&
|
||||
recorded[recorded.length - 1].id === SetSelectionsOperation.id
|
||||
) {
|
||||
recorded[recorded.length - 1] = commandInfo;
|
||||
} else {
|
||||
recorded.push(commandInfo);
|
||||
this._recorded$.next(recorded);
|
||||
|
||||
if (commandInfo.type === CommandType.COMMAND) {
|
||||
commands.push(commandInfo);
|
||||
this._recordedCommands$.next(commands);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this._recording$.next(true);
|
||||
}
|
||||
|
||||
stopRecording(): void {
|
||||
this._recorder?.dispose();
|
||||
this._recorder = null;
|
||||
|
||||
this._recorded$.next([]);
|
||||
this._recordedCommands$.next([]);
|
||||
this._recording$.next(false);
|
||||
}
|
||||
|
||||
completeRecording(): void {
|
||||
const commands = this._recorded.slice();
|
||||
this._localFileService.downloadFile(new Blob([JSON.stringify(commands, null, 2)]), 'recorded-commands.json');
|
||||
|
||||
this._logService.error('Recorded commands:', commands);
|
||||
this.stopRecording();
|
||||
}
|
||||
}
|
||||
150
packages/action-recorder/src/services/replay.service.ts
Normal file
150
packages/action-recorder/src/services/replay.service.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { 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';
|
||||
|
||||
export enum ReplayMode {
|
||||
DEFAULT = 'default',
|
||||
NAME = 'name',
|
||||
ACTIVE = 'active',
|
||||
}
|
||||
|
||||
/**
|
||||
* This service is for replaying user actions.
|
||||
*/
|
||||
export class ActionReplayService extends Disposable {
|
||||
constructor(
|
||||
@IMessageService private readonly _messageService: IMessageService,
|
||||
@IUniverInstanceService private readonly _instanceService: IUniverInstanceService,
|
||||
@ILocalFileService private readonly _localFileService: ILocalFileService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@ICommandService private readonly _commandService: ICommandService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a local file and try to replay commands in this JSON.
|
||||
*/
|
||||
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()), { mode });
|
||||
} catch {
|
||||
this._messageService.show({
|
||||
type: MessageType.Error,
|
||||
content: `Failed to replay commands from local file ${file.name}.`,
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replay a list of commands. Note that `unitId` of these commands would be changed to the focused unit.
|
||||
* @param commands - The commands to replay.
|
||||
* @returns If the replay is successful.
|
||||
*/
|
||||
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;
|
||||
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);
|
||||
if (!result) return false;
|
||||
} else {
|
||||
const result = await this._commandService.executeCommand(id);
|
||||
if (!result) return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replay a list of commands with a random delay between each command.
|
||||
* @param commands - The commands to replay.
|
||||
*/
|
||||
async replayCommandsWithDelay(commands: ICommandInfo[]): Promise<boolean> {
|
||||
const focusedUnitId = this._instanceService.getFocusedUnit()?.getUnitId();
|
||||
if (!focusedUnitId) {
|
||||
this._logService.error('[ReplayService]', 'no focused unit to replay commands');
|
||||
}
|
||||
|
||||
for (const command of commands) {
|
||||
await awaitTime(randomBetween200And1k());
|
||||
|
||||
const { id, params } = command;
|
||||
if (params) {
|
||||
if (typeof (params as ISharedCommandParams).unitId !== 'undefined') {
|
||||
(params as ISharedCommandParams).unitId = focusedUnitId;
|
||||
}
|
||||
|
||||
const result = await this._commandService.executeCommand(id, params);
|
||||
if (!result) return false;
|
||||
} else {
|
||||
const result = await this._commandService.executeCommand(id);
|
||||
if (!result) return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
interface ISharedCommandParams {
|
||||
unitId?: string;
|
||||
}
|
||||
|
||||
function randomBetween200And1k() {
|
||||
return Math.floor(Math.random() * 800) + 200;
|
||||
}
|
||||
101
packages/action-recorder/src/views/components/RecorderPanel.tsx
Normal file
101
packages/action-recorder/src/views/components/RecorderPanel.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { LocaleKey } from '../../locale/types';
|
||||
import { ICommandService, LocaleService } from '@univerjs/core';
|
||||
import { Button } from '@univerjs/design';
|
||||
import { RecordIcon } from '@univerjs/icons';
|
||||
import { useDependency, useObservable } from '@univerjs/ui';
|
||||
import { useCallback } from 'react';
|
||||
import { CompleteRecordingActionCommand, StartRecordingActionCommand, StopRecordingActionCommand } from '../../commands/commands/record.command';
|
||||
import { CloseRecordPanelOperation } from '../../commands/operations/operation';
|
||||
import { ActionRecorderService } from '../../services/action-recorder.service';
|
||||
|
||||
export function RecorderPanel() {
|
||||
const s = useDependency(ActionRecorderService);
|
||||
const opened = useObservable(s.panelOpened$);
|
||||
|
||||
if (!opened) return null;
|
||||
return <RecordPanelImpl />;
|
||||
}
|
||||
|
||||
function RecordPanelImpl() {
|
||||
const commandService = useDependency(ICommandService);
|
||||
const localeService = useDependency(LocaleService);
|
||||
const actionRecorderService = useDependency(ActionRecorderService);
|
||||
|
||||
const recording = useObservable(actionRecorderService.recording$);
|
||||
const recordedCommands = useObservable(actionRecorderService.recordedCommands$);
|
||||
const len = recordedCommands?.length ?? 0;
|
||||
|
||||
const closePanel = useCallback(() => {
|
||||
if (!recording) commandService.executeCommand(CloseRecordPanelOperation.id);
|
||||
}, [commandService, recording]);
|
||||
|
||||
const startRecording = useCallback((replaceId?: boolean) => {
|
||||
if (!recording) commandService.executeCommand(StartRecordingActionCommand.id, { replaceId });
|
||||
}, [commandService, recording]);
|
||||
|
||||
const completeRecording = useCallback(() => {
|
||||
if (recording) commandService.executeCommand(CompleteRecordingActionCommand.id);
|
||||
}, [commandService, recording]);
|
||||
|
||||
const stopRecording = useCallback(() => {
|
||||
if (recording) commandService.executeCommand(StopRecordingActionCommand.id);
|
||||
}, [commandService, recording]);
|
||||
|
||||
const titleText = recording
|
||||
? len === 0 ? localeService.t<LocaleKey>('action-recorder.panel.recording') : (`${len}: ${recordedCommands![len - 1].id}`)
|
||||
: localeService.t<LocaleKey>('action-recorder.panel.startRecording');
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
univer-fixed univer-bottom-20 univer-left-1/2 univer-z-[1000] univer-flex univer-h-16 univer-w-[512px]
|
||||
-univer-translate-x-1/2 univer-items-center univer-rounded-lg univer-bg-gray-0 univer-px-5
|
||||
univer-shadow-lg
|
||||
`}
|
||||
>
|
||||
<div className="univer-mr-2 univer-size-5 univer-shrink-0 univer-grow-0 univer-text-xl">
|
||||
<RecordIcon />
|
||||
</div>
|
||||
<div className="univer-flex-1 univer-text-sm">{titleText}</div>
|
||||
<div className="univer-flex univer-w-64 univer-shrink-0 univer-grow-0 univer-justify-between">
|
||||
<Button
|
||||
className="univer-w-20"
|
||||
onClick={recording ? stopRecording : closePanel}
|
||||
>
|
||||
{localeService.t<LocaleKey>(recording ? 'action-recorder.panel.cancel' : 'action-recorder.panel.close')}
|
||||
</Button>
|
||||
<Button
|
||||
className="univer-w-20"
|
||||
variant="primary"
|
||||
onClick={recording ? completeRecording : () => startRecording()}
|
||||
>
|
||||
{localeService.t<LocaleKey>(recording ? 'action-recorder.panel.save' : 'action-recorder.panel.start')}
|
||||
</Button>
|
||||
{!recording && (
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => startRecording(true)}
|
||||
>
|
||||
{localeService.t<LocaleKey>('action-recorder.panel.startWithId')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { IAccessor } from '@univerjs/core';
|
||||
import type { Root } from 'react-dom/client';
|
||||
import {
|
||||
CommandService,
|
||||
CommandType,
|
||||
ConfigService,
|
||||
ContextService,
|
||||
DesktopLogService,
|
||||
ICommandService,
|
||||
IConfigService,
|
||||
IContextService,
|
||||
ILogService,
|
||||
Injector,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
LocaleType,
|
||||
LogLevel,
|
||||
} from '@univerjs/core';
|
||||
import { ILocalFileService, RediContext } from '@univerjs/ui';
|
||||
import { act } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import {
|
||||
CompleteRecordingActionCommand,
|
||||
StartRecordingActionCommand,
|
||||
StopRecordingActionCommand,
|
||||
} from '../../../commands/commands/record.command';
|
||||
import { CloseRecordPanelOperation, OpenRecordPanelOperation } from '../../../commands/operations/operation';
|
||||
import enUS from '../../../locale/en-US';
|
||||
import { ActionRecorderService } from '../../../services/action-recorder.service';
|
||||
import { RecorderPanel } from '../RecorderPanel';
|
||||
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
const APPLY_CELL_VALUE_COMMAND_ID = 'action-recorder.panel-test.apply-cell-value';
|
||||
|
||||
interface IAppliedCommandParams {
|
||||
unitId?: string;
|
||||
subUnitId?: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
class TestState {
|
||||
static downloads: Array<{ data: Blob; fileName: string }> = [];
|
||||
static appliedParams: IAppliedCommandParams[] = [];
|
||||
|
||||
static reset() {
|
||||
this.downloads = [];
|
||||
this.appliedParams = [];
|
||||
}
|
||||
}
|
||||
|
||||
class TestLocalFileService implements ILocalFileService {
|
||||
openFile(): Promise<File[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
downloadFile(data: Blob, fileName: string): void {
|
||||
TestState.downloads.push({ data, fileName });
|
||||
}
|
||||
}
|
||||
|
||||
class TestWorkbook {
|
||||
getUnitId() {
|
||||
return 'focused-workbook';
|
||||
}
|
||||
|
||||
getSheetBySheetId(sheetId: string) {
|
||||
if (sheetId !== 'sheet-1') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { getName: () => 'Recorded Sheet' };
|
||||
}
|
||||
}
|
||||
|
||||
class TestUniverInstanceService {
|
||||
private readonly _workbook = new TestWorkbook();
|
||||
|
||||
getFocusedUnit() {
|
||||
return this._workbook;
|
||||
}
|
||||
|
||||
getUnit() {
|
||||
return this._workbook;
|
||||
}
|
||||
}
|
||||
|
||||
function createRecorderPanelTestBed() {
|
||||
const injector = new Injector();
|
||||
injector.add([ICommandService, { useClass: CommandService }]);
|
||||
injector.add([ILogService, { useClass: DesktopLogService }]);
|
||||
injector.add([IContextService, { useClass: ContextService }]);
|
||||
injector.add([IConfigService, { useClass: ConfigService }]);
|
||||
injector.add([ILocalFileService, { useClass: TestLocalFileService }]);
|
||||
injector.add([IUniverInstanceService, { useClass: TestUniverInstanceService as never }]);
|
||||
injector.add([LocaleService]);
|
||||
injector.add([ActionRecorderService]);
|
||||
|
||||
injector.get(LocaleService).load({ [LocaleType.EN_US]: enUS });
|
||||
injector.get(LocaleService).setLocale(LocaleType.EN_US);
|
||||
|
||||
injector.get(ILogService).setLogLevel(LogLevel.SILENT);
|
||||
|
||||
const commandService = injector.get(ICommandService);
|
||||
[
|
||||
OpenRecordPanelOperation,
|
||||
CloseRecordPanelOperation,
|
||||
StartRecordingActionCommand,
|
||||
CompleteRecordingActionCommand,
|
||||
StopRecordingActionCommand,
|
||||
{
|
||||
id: APPLY_CELL_VALUE_COMMAND_ID,
|
||||
type: CommandType.COMMAND,
|
||||
handler: (_accessor: IAccessor, params?: IAppliedCommandParams) => {
|
||||
TestState.appliedParams.push({ ...params });
|
||||
return true;
|
||||
},
|
||||
},
|
||||
].forEach((command) => commandService.registerCommand(command));
|
||||
|
||||
const recorderService = injector.get(ActionRecorderService);
|
||||
recorderService.registerRecordedCommand({
|
||||
id: APPLY_CELL_VALUE_COMMAND_ID,
|
||||
type: CommandType.COMMAND,
|
||||
handler: () => true,
|
||||
});
|
||||
|
||||
return {
|
||||
injector,
|
||||
commandService,
|
||||
recorderService,
|
||||
};
|
||||
}
|
||||
|
||||
async function renderRecorderPanel(root: Root, injector: Injector) {
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<RediContext.Provider value={{ injector }}>
|
||||
<RecorderPanel />
|
||||
</RediContext.Provider>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function getButton(container: HTMLElement, text: string) {
|
||||
const button = Array.from(container.querySelectorAll('[data-u-comp="button"]'))
|
||||
.find((item) => item.textContent === text);
|
||||
expect(button).toBeTruthy();
|
||||
return button as HTMLElement;
|
||||
}
|
||||
|
||||
describe('RecorderPanel', () => {
|
||||
let root: Root | undefined;
|
||||
let container: HTMLDivElement | undefined;
|
||||
let currentTestBed: ReturnType<typeof createRecorderPanelTestBed> | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
TestState.reset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => {
|
||||
root?.unmount();
|
||||
});
|
||||
container?.remove();
|
||||
currentTestBed = undefined;
|
||||
root = undefined;
|
||||
container = undefined;
|
||||
});
|
||||
|
||||
it('opens the panel and starts recording from the Start action', async () => {
|
||||
currentTestBed = createRecorderPanelTestBed();
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
|
||||
await renderRecorderPanel(root, currentTestBed.injector);
|
||||
expect(container.textContent).toBe('');
|
||||
|
||||
await act(async () => {
|
||||
await currentTestBed!.commandService.executeCommand(OpenRecordPanelOperation.id);
|
||||
});
|
||||
expect(container.textContent).toContain('Start Recording');
|
||||
|
||||
await act(async () => {
|
||||
getButton(container!, 'Start').dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(currentTestBed.recorderService.recording).toBe(true);
|
||||
expect(container.textContent).toContain('Recording...');
|
||||
expect(container.textContent).toContain('Cancel');
|
||||
expect(container.textContent).toContain('Save');
|
||||
});
|
||||
|
||||
it('records a command with sheet names from Start(N) and exports it on Save', async () => {
|
||||
currentTestBed = createRecorderPanelTestBed();
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
|
||||
await renderRecorderPanel(root, currentTestBed.injector);
|
||||
|
||||
await act(async () => {
|
||||
await currentTestBed!.commandService.executeCommand(OpenRecordPanelOperation.id);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
getButton(container!, 'Start(N)').dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await currentTestBed!.commandService.executeCommand(APPLY_CELL_VALUE_COMMAND_ID, {
|
||||
unitId: 'focused-workbook',
|
||||
subUnitId: 'sheet-1',
|
||||
value: '42',
|
||||
});
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain(`1: ${APPLY_CELL_VALUE_COMMAND_ID}`);
|
||||
|
||||
await act(async () => {
|
||||
getButton(container!, 'Save').dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(currentTestBed.recorderService.recording).toBe(false);
|
||||
expect(TestState.downloads).toHaveLength(1);
|
||||
expect(TestState.downloads[0].fileName).toBe('recorded-commands.json');
|
||||
expect(JSON.parse(await TestState.downloads[0].data.text())).toEqual([
|
||||
{
|
||||
id: APPLY_CELL_VALUE_COMMAND_ID,
|
||||
type: CommandType.COMMAND,
|
||||
params: {
|
||||
unitId: 'focused-workbook',
|
||||
subUnitId: 'Recorded Sheet',
|
||||
value: '42',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('stops recording without exporting when the panel is closed', async () => {
|
||||
currentTestBed = createRecorderPanelTestBed();
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
|
||||
await renderRecorderPanel(root, currentTestBed.injector);
|
||||
|
||||
await act(async () => {
|
||||
await currentTestBed!.commandService.executeCommand(OpenRecordPanelOperation.id);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
getButton(container!, 'Start').dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await currentTestBed!.commandService.executeCommand(CloseRecordPanelOperation.id);
|
||||
});
|
||||
|
||||
expect(currentTestBed.recorderService.recording).toBe(false);
|
||||
expect(container.textContent).toBe('');
|
||||
expect(TestState.downloads).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('completes an active recording from the panel cancel action using the stop command', async () => {
|
||||
currentTestBed = createRecorderPanelTestBed();
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
|
||||
await renderRecorderPanel(root, currentTestBed.injector);
|
||||
|
||||
await act(async () => {
|
||||
await currentTestBed!.commandService.executeCommand(OpenRecordPanelOperation.id);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
getButton(container!, 'Start').dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await currentTestBed!.commandService.executeCommand(APPLY_CELL_VALUE_COMMAND_ID, {
|
||||
unitId: 'focused-workbook',
|
||||
subUnitId: 'sheet-1',
|
||||
value: 'cancelled',
|
||||
});
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain(`1: ${APPLY_CELL_VALUE_COMMAND_ID}`);
|
||||
|
||||
await act(async () => {
|
||||
getButton(container!, 'Cancel').dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(currentTestBed.recorderService.recording).toBe(false);
|
||||
expect(container.textContent).toContain('Start Recording');
|
||||
expect(TestState.downloads).toHaveLength(1);
|
||||
expect(JSON.parse(await TestState.downloads[0].data.text())).toEqual([
|
||||
{
|
||||
id: APPLY_CELL_VALUE_COMMAND_ID,
|
||||
type: CommandType.COMMAND,
|
||||
params: {
|
||||
unitId: 'focused-workbook',
|
||||
subUnitId: 'sheet-1',
|
||||
value: 'cancelled',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
27
packages/action-recorder/tailwind.config.ts
Normal file
27
packages/action-recorder/tailwind.config.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { Config } from 'tailwindcss';
|
||||
import preset from '@univerjs-infra/shared/tailwind';
|
||||
|
||||
const config: Config = {
|
||||
presets: [preset],
|
||||
content: [
|
||||
'./src/**/*.{js,ts,jsx,tsx}',
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
8
packages/action-recorder/tsconfig.json
Normal file
8
packages/action-recorder/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@univerjs-infra/shared/tsconfigs/base",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib/types"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
18
packages/action-recorder/tsconfig.node.json
Normal file
18
packages/action-recorder/tsconfig.node.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "@univerjs-infra/shared/tsconfigs/node",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"noEmit": false,
|
||||
"outDir": "lib/types"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/__tests__/**",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.ts"
|
||||
]
|
||||
}
|
||||
3
packages/action-recorder/vitest.config.ts
Normal file
3
packages/action-recorder/vitest.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import createConfig from '@univerjs-infra/shared/vitest';
|
||||
|
||||
export default createConfig();
|
||||
Reference in New Issue
Block a user