diff --git a/packages/core/src/services/command/__tests__/command.service.spec.ts b/packages/core/src/services/command/__tests__/command.service.spec.ts
index 99989d703f..34f56b8c31 100644
--- a/packages/core/src/services/command/__tests__/command.service.spec.ts
+++ b/packages/core/src/services/command/__tests__/command.service.spec.ts
@@ -335,6 +335,56 @@ describe('Test CommandService', () => {
expect(params.trigger).toBe(commandID);
});
+ it('Should attach the triggering operation id when no command wraps a synchronous mutation', () => {
+ const mutationID = 'nested-operation-trigger-mutation';
+ const operationID = 'nested-trigger-operation';
+ const params: { trigger?: string } = {};
+
+ commandService.registerCommand({
+ id: mutationID,
+ type: CommandType.MUTATION,
+ handler: (_accessor, mutationParams: { trigger?: string }) => {
+ expect(mutationParams.trigger).toBe(operationID);
+ return true;
+ },
+ });
+ commandService.registerCommand({
+ id: operationID,
+ type: CommandType.OPERATION,
+ handler: (accessor) => accessor.get(ICommandService).syncExecuteCommand(mutationID, params),
+ });
+
+ expect(commandService.syncExecuteCommand(operationID)).toBe(true);
+ expect(params.trigger).toBe(operationID);
+ });
+
+ it('Should attach operation triggers to asynchronous mutations without replacing explicit triggers', async () => {
+ const mutationID = 'nested-async-operation-trigger-mutation';
+ const operationID = 'nested-async-trigger-operation';
+ const inferredParams: { trigger?: string } = {};
+ const explicitParams = { trigger: 'semantic-trigger' };
+
+ commandService.registerCommand({
+ id: mutationID,
+ type: CommandType.MUTATION,
+ handler: () => true,
+ });
+ commandService.registerCommand({
+ id: operationID,
+ type: CommandType.OPERATION,
+ handler: async (accessor) => {
+ const service = accessor.get(ICommandService);
+ const inferred = await service.executeCommand(mutationID, inferredParams);
+ const explicit = await service.executeCommand(mutationID, explicitParams);
+ return inferred && explicit;
+ },
+ });
+
+ await expect(commandService.executeCommand(operationID)).resolves.toBe(true);
+ expect(inferredParams.trigger).toBe(operationID);
+ expect(explicitParams.trigger).toBe('semantic-trigger');
+ });
+
it('Should convert custom command execution errors into a false result', async () => {
const customErrorCommandID = 'custom-error-command';
commandService.registerCommand({
diff --git a/packages/core/src/services/command/command.service.ts b/packages/core/src/services/command/command.service.ts
index 1fc47585b9..e0b363179e 100644
--- a/packages/core/src/services/command/command.service.ts
+++ b/packages/core/src/services/command/command.service.ts
@@ -106,7 +106,7 @@ export interface IMultiCommand
extends I
export interface IMutationCommonParams {
/**
- * It is used to indicate which {@link CommandType.COMMAND} triggers the mutation.
+ * It is used to indicate which {@link CommandType.COMMAND} or {@link CommandType.OPERATION} triggers the mutation.
*/
trigger?: string;
@@ -433,6 +433,8 @@ export class CommandService extends Disposable implements ICommandService {
params,
};
+ this._attachMutationTrigger(command, params);
+
const stackItemDisposable = this._pushCommandExecutionStack(commandInfo);
const _options = options ?? {};
@@ -492,18 +494,7 @@ export class CommandService extends Disposable implements ICommandService {
params,
};
- // If the executed command is of type `Mutation`, we should add a trigger params,
- // whose value is the command's ID that triggers the mutation.
- if (command.type === CommandType.MUTATION) {
- const triggerCommand = findLast(
- this._commandExecutionStack,
- (item) => item.type === CommandType.COMMAND
- );
- if (triggerCommand) {
- commandInfo.params = commandInfo.params ?? {};
- (commandInfo.params as IMutationCommonParams).trigger = triggerCommand.id;
- }
- }
+ this._attachMutationTrigger(command, params);
const stackItemDisposable = this._pushCommandExecutionStack(commandInfo);
const _options = options ?? {};
@@ -566,11 +557,11 @@ export class CommandService extends Disposable implements ICommandService {
this._multiCommandDisposables.set(command.id, disposableCollection);
} else {
- if ((registry[0] as Record).multi !== true) {
- throw new Error('Command has registered as a single command.');
- } else {
- multiCommand = registry[0] as MultiCommand;
+ const registeredCommand = registry[0];
+ if (!(registeredCommand instanceof MultiCommand)) {
+ throw new TypeError('Command has registered as a single command.');
}
+ multiCommand = registeredCommand;
}
const implementationDisposable = multiCommand.registerImplementation(command as IMultiCommand);
@@ -582,6 +573,37 @@ export class CommandService extends Disposable implements ICommandService {
});
}
+ private _attachMutationTrigger(command: ICommand
, params?: P): void {
+ if (command.type !== CommandType.MUTATION || !params) {
+ return;
+ }
+
+ const triggerCommand = findLast(
+ this._commandExecutionStack,
+ (item) => item.type === CommandType.COMMAND
+ );
+ if (triggerCommand) {
+ this._setMutationTrigger(params, triggerCommand.id);
+ return;
+ }
+
+ if ('trigger' in params && params.trigger !== undefined) {
+ return;
+ }
+
+ const triggerOperation = findLast(
+ this._commandExecutionStack,
+ (item) => item.type === CommandType.OPERATION
+ );
+ if (triggerOperation) {
+ this._setMutationTrigger(params, triggerOperation.id);
+ }
+ }
+
+ private _setMutationTrigger(params: object, trigger: string): void {
+ Object.assign(params, { trigger } satisfies IMutationCommonParams);
+ }
+
private async _execute
(command: ICommand
, params?: P, options?: IExecutionOptions): Promise {
// If syncOnly is true, skip execution but return true to indicate success for sync purposes
if (options?.syncOnly) {
diff --git a/packages/docs-drawing/src/commands/commands/__tests__/remove-doc-drawing.command.spec.ts b/packages/docs-drawing/src/commands/commands/__tests__/remove-doc-drawing.command.spec.ts
new file mode 100644
index 0000000000..69e8d175ee
--- /dev/null
+++ b/packages/docs-drawing/src/commands/commands/__tests__/remove-doc-drawing.command.spec.ts
@@ -0,0 +1,91 @@
+/**
+ * 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 { IRemoveDocDrawingCommandParams } from '../remove-doc-drawing.command';
+import { DrawingTypeEnum, ICommandService, ImageSourceType } from '@univerjs/core';
+import { DocHistoryAction, RichTextEditingMutation } from '@univerjs/docs';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { createFacadeTestBed } from '../../../facade/__tests__/create-test-bed';
+import { RemoveDocDrawingCommand } from '../remove-doc-drawing.command';
+
+class MockImage {
+ width = 800;
+ height = 400;
+ onload: (() => void) | null = null;
+ onerror: (() => void) | null = null;
+
+ get src(): string {
+ return '';
+ }
+
+ set src(_value: string) {
+ queueMicrotask(() => this.onload?.());
+ }
+}
+
+describe('RemoveDocDrawingCommand', () => {
+ let testBed: ReturnType;
+
+ beforeEach(() => {
+ vi.stubGlobal('Image', MockImage);
+ testBed = createFacadeTestBed();
+ });
+
+ afterEach(() => {
+ testBed.univer.dispose();
+ vi.unstubAllGlobals();
+ });
+
+ it.each([
+ [DrawingTypeEnum.DRAWING_IMAGE, DocHistoryAction.DeleteImage],
+ [DrawingTypeEnum.DRAWING_SHAPE, DocHistoryAction.DeleteShape],
+ [DrawingTypeEnum.DRAWING_CHART, DocHistoryAction.DeleteChart],
+ ])('records the drawing type %s in history metadata', async (drawingType, historyAction) => {
+ const image = await testBed.document.insertImage({
+ source: 'data:image/png;base64,image',
+ imageSourceType: ImageSourceType.BASE64,
+ width: 160,
+ height: 90,
+ textRange: {
+ startOffset: 3,
+ endOffset: 3,
+ collapsed: true,
+ segmentId: '',
+ },
+ });
+ const commandService = testBed.injector.get(ICommandService);
+ const mutationSpy = vi.spyOn(commandService, 'syncExecuteCommand');
+
+ const result = commandService.syncExecuteCommand(
+ RemoveDocDrawingCommand.id,
+ {
+ unitId: 'test-doc',
+ drawings: [{
+ unitId: 'test-doc',
+ subUnitId: 'test-doc',
+ drawingId: image!.getId(),
+ drawingType,
+ }],
+ }
+ );
+
+ expect(result).toBe(true);
+ expect(mutationSpy).toHaveBeenCalledWith(
+ RichTextEditingMutation.id,
+ expect.objectContaining({ historyActions: [historyAction] })
+ );
+ });
+});
diff --git a/packages/docs-drawing/src/commands/commands/__tests__/update-doc-drawing-transform.command.spec.ts b/packages/docs-drawing/src/commands/commands/__tests__/update-doc-drawing-transform.command.spec.ts
new file mode 100644
index 0000000000..14b989f1d8
--- /dev/null
+++ b/packages/docs-drawing/src/commands/commands/__tests__/update-doc-drawing-transform.command.spec.ts
@@ -0,0 +1,83 @@
+/**
+ * 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 { IUpdateDrawingDocTransformCommandParams } from '@univerjs/docs-drawing';
+import { ICommandService, ImageSourceType } from '@univerjs/core';
+import { DocHistoryAction, RichTextEditingMutation } from '@univerjs/docs';
+import { UpdateDrawingDocTransformCommand } from '@univerjs/docs-drawing';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { createFacadeTestBed } from '../../../facade/__tests__/create-test-bed';
+
+class MockImage {
+ width = 800;
+ height = 400;
+ onload: (() => void) | null = null;
+ onerror: (() => void) | null = null;
+
+ get src(): string {
+ return '';
+ }
+
+ set src(_value: string) {
+ queueMicrotask(() => this.onload?.());
+ }
+}
+
+describe('UpdateDrawingDocTransformCommand', () => {
+ let testBed: ReturnType;
+
+ beforeEach(() => {
+ vi.stubGlobal('Image', MockImage);
+ testBed = createFacadeTestBed();
+ });
+
+ afterEach(() => {
+ testBed.univer.dispose();
+ vi.unstubAllGlobals();
+ });
+
+ it('marks image transforms for history action summaries', async () => {
+ const image = await testBed.document.insertImage({
+ source: 'data:image/png;base64,image',
+ imageSourceType: ImageSourceType.BASE64,
+ width: 160,
+ height: 90,
+ textRange: {
+ startOffset: 3,
+ endOffset: 3,
+ collapsed: true,
+ segmentId: '',
+ },
+ });
+ const commandService = testBed.injector.get(ICommandService);
+ const mutationSpy = vi.spyOn(commandService, 'syncExecuteCommand');
+
+ const result = commandService.syncExecuteCommand(
+ UpdateDrawingDocTransformCommand.id,
+ {
+ unitId: 'test-doc',
+ subUnitId: 'test-doc',
+ drawings: [{ drawingId: image!.getId(), key: 'angle', value: 15 }],
+ }
+ );
+
+ expect(result).toBe(true);
+ expect(mutationSpy).toHaveBeenCalledWith(
+ RichTextEditingMutation.id,
+ expect.objectContaining({ historyAction: DocHistoryAction.UpdateImage })
+ );
+ });
+});
diff --git a/packages/docs-drawing/src/commands/commands/__tests__/update-doc-drawing-wrapping-style.command.spec.ts b/packages/docs-drawing/src/commands/commands/__tests__/update-doc-drawing-wrapping-style.command.spec.ts
index a5b008699b..a2bd941f0f 100644
--- a/packages/docs-drawing/src/commands/commands/__tests__/update-doc-drawing-wrapping-style.command.spec.ts
+++ b/packages/docs-drawing/src/commands/commands/__tests__/update-doc-drawing-wrapping-style.command.spec.ts
@@ -26,6 +26,7 @@ import {
PositionedObjectLayoutType,
UniverInstanceType,
} from '@univerjs/core';
+import { DocHistoryAction, RichTextEditingMutation } from '@univerjs/docs';
import { TextWrappingStyle, UpdateDocDrawingWrappingStyleCommand } from '@univerjs/docs-drawing';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { createFacadeTestBed } from '../../../facade/__tests__/create-test-bed';
@@ -86,7 +87,9 @@ describe('UpdateDocDrawingWrappingStyleCommand', () => {
);
testBed.injector.get(IUniverInstanceService).focusUnit(otherDocument.getUnitId());
- const result = testBed.injector.get(ICommandService).syncExecuteCommand(
+ const commandService = testBed.injector.get(ICommandService);
+ const mutationSpy = vi.spyOn(commandService, 'syncExecuteCommand');
+ const result = commandService.syncExecuteCommand(
UpdateDocDrawingWrappingStyleCommand.id,
{
unitId: 'test-doc',
@@ -100,6 +103,10 @@ describe('UpdateDocDrawingWrappingStyleCommand', () => {
);
expect(result).toBe(true);
+ expect(mutationSpy).toHaveBeenCalledWith(
+ RichTextEditingMutation.id,
+ expect.objectContaining({ historyAction: DocHistoryAction.UpdateImage })
+ );
expect(testBed.document.getImage(image!.getId())?.getImageData()).toMatchObject({
layoutType: PositionedObjectLayoutType.WRAP_NONE,
behindDoc: BooleanNumber.TRUE,
diff --git a/packages/docs-drawing/src/commands/commands/remove-doc-drawing.command.ts b/packages/docs-drawing/src/commands/commands/remove-doc-drawing.command.ts
index bc4f55c353..405bf4642c 100644
--- a/packages/docs-drawing/src/commands/commands/remove-doc-drawing.command.ts
+++ b/packages/docs-drawing/src/commands/commands/remove-doc-drawing.command.ts
@@ -14,11 +14,12 @@
* limitations under the License.
*/
-import type { DocumentDataModel, DrawingTypeEnum, IAccessor, ICommand, IDisposable, IMutationInfo, ITextRangeParam, JSONXActions } from '@univerjs/core';
+import type { DocumentDataModel, IAccessor, ICommand, IDisposable, IMutationInfo, ITextRangeParam, JSONXActions } from '@univerjs/core';
import type { IRichTextEditingMutationParams } from '@univerjs/docs';
import type { IDocDrawing } from '../../services/doc-drawing.service';
import {
CommandType,
+ DrawingTypeEnum,
getRichTextEditPath,
ICommandService,
IUndoRedoService,
@@ -29,7 +30,7 @@ import {
TextXActionType,
UniverInstanceType,
} from '@univerjs/core';
-import { DocSelectionManagerService, getContentInsertRange, normalizeTextRange, RichTextEditingMutation } from '@univerjs/docs';
+import { DocHistoryAction, DocSelectionManagerService, getContentInsertRange, normalizeTextRange, RichTextEditingMutation } from '@univerjs/docs';
import { IDocDrawingAdapterService } from '../../services/doc-drawing-adapter.service';
export interface IRemoveDocDrawingCommandParam {
@@ -136,9 +137,10 @@ export const RemoveDocDrawingCommand: ICommand = {
const memoryCursor = new MemoryCursor();
const cursorIndex = removeCustomBlocks[0]!.startIndex;
const textRanges = [{ startOffset: cursorIndex, endOffset: cursorIndex }] as IRichTextEditingMutationParams['textRanges'];
+ const historyActions = getHistoryActions(removeDrawings);
const doMutation: IMutationInfo = {
id: RichTextEditingMutation.id,
- params: { unitId, actions: [], textRanges },
+ params: { unitId, actions: [], textRanges, historyActions },
};
const rawActions: JSONXActions = [];
@@ -174,6 +176,22 @@ export const RemoveDocDrawingCommand: ICommand = {
},
};
+function getHistoryActions(drawings: IRemoveDocDrawingCommandParam[]): DocHistoryAction[] {
+ const historyActions = drawings.flatMap((drawing): DocHistoryAction[] => {
+ switch (drawing.drawingType) {
+ case DrawingTypeEnum.DRAWING_IMAGE:
+ return [DocHistoryAction.DeleteImage];
+ case DrawingTypeEnum.DRAWING_SHAPE:
+ return [DocHistoryAction.DeleteShape];
+ case DrawingTypeEnum.DRAWING_CHART:
+ return [DocHistoryAction.DeleteChart];
+ default:
+ return [];
+ }
+ });
+ return [...new Set(historyActions)];
+}
+
function executeResourceMutationGroups(
mutationGroups: Array<{ redoMutations: IMutationInfo[]; undoMutations: IMutationInfo[] }>,
commandService: ICommandService
diff --git a/packages/docs-drawing/src/commands/commands/update-doc-drawing-transform.command.ts b/packages/docs-drawing/src/commands/commands/update-doc-drawing-transform.command.ts
index b4b9aa6e3a..06c00a2f1d 100644
--- a/packages/docs-drawing/src/commands/commands/update-doc-drawing-transform.command.ts
+++ b/packages/docs-drawing/src/commands/commands/update-doc-drawing-transform.command.ts
@@ -19,13 +19,14 @@ import type { IRichTextEditingMutationParams } from '@univerjs/docs';
import type { IDocImage } from '../../services/doc-drawing.service';
import {
CommandType,
+ DrawingTypeEnum,
ICommandService,
IUniverInstanceService,
JSONX,
Tools,
UniverInstanceType,
} from '@univerjs/core';
-import { RichTextEditingMutation } from '@univerjs/docs';
+import { DocHistoryAction, RichTextEditingMutation } from '@univerjs/docs';
export interface IDrawingDocTransform {
drawingId: string;
@@ -57,6 +58,11 @@ export const UpdateDrawingDocTransformCommand: ICommand = {
}
const oldDrawings = documentDataModel.getSnapshot().drawings ?? {};
+ const historyAction = drawings.length > 0 && drawings.every(({ drawingId }) =>
+ oldDrawings[drawingId]?.drawingType === DrawingTypeEnum.DRAWING_IMAGE
+ )
+ ? DocHistoryAction.UpdateImage
+ : undefined;
const jsonX = JSONX.getInstance();
const actions: JSONXActions = [];
@@ -78,6 +84,7 @@ export const UpdateDrawingDocTransformCommand: ICommand = {
return Boolean(commandService.syncExecuteCommand(RichTextEditingMutation.id, {
unitId,
+ historyAction,
actions: actions.reduce((acc, action) => JSONX.compose(acc, action as JSONXActions), null as JSONXActions),
textRanges: null,
debounce: true,
diff --git a/packages/docs-drawing/src/commands/commands/update-doc-drawing-wrapping-style.command.ts b/packages/docs-drawing/src/commands/commands/update-doc-drawing-wrapping-style.command.ts
index 830db9517b..a9126b17f7 100644
--- a/packages/docs-drawing/src/commands/commands/update-doc-drawing-wrapping-style.command.ts
+++ b/packages/docs-drawing/src/commands/commands/update-doc-drawing-wrapping-style.command.ts
@@ -20,6 +20,7 @@ import type { IDocDrawing } from '../../services/doc-drawing.service';
import {
BooleanNumber,
CommandType,
+ DrawingTypeEnum,
ICommandService,
IUniverInstanceService,
JSONX,
@@ -27,7 +28,7 @@ import {
Tools,
UniverInstanceType,
} from '@univerjs/core';
-import { RichTextEditingMutation } from '@univerjs/docs';
+import { DocHistoryAction, RichTextEditingMutation } from '@univerjs/docs';
/**
* Controls how a document drawing participates in text layout.
@@ -106,6 +107,11 @@ export const UpdateDocDrawingWrappingStyleCommand: ICommand = {
}
const oldDrawings = documentDataModel.getDrawings() ?? {};
+ const historyAction = drawings.length > 0 && drawings.every(({ drawingId }) =>
+ oldDrawings[drawingId]?.drawingType === DrawingTypeEnum.DRAWING_IMAGE
+ )
+ ? DocHistoryAction.UpdateImage
+ : undefined;
const jsonX = JSONX.getInstance();
const rawActions: JSONXActions = [];
@@ -142,6 +148,7 @@ export const UpdateDocDrawingWrappingStyleCommand: ICommand = {
id: RichTextEditingMutation.id,
params: {
unitId,
+ historyAction,
actions: rawActions.reduce(
(actions, action) => JSONX.compose(actions, action as JSONXActions),
null as JSONXActions
diff --git a/packages/docs-hyper-link-ui/src/commands/commands/add-link.command.ts b/packages/docs-hyper-link-ui/src/commands/commands/add-link.command.ts
index 753b2c3fb9..cb6c55c9dd 100644
--- a/packages/docs-hyper-link-ui/src/commands/commands/add-link.command.ts
+++ b/packages/docs-hyper-link-ui/src/commands/commands/add-link.command.ts
@@ -17,6 +17,7 @@
import type { ICommand, ITextRangeParam } from '@univerjs/core';
import { CommandType, CustomRangeType, generateRandomId, ICommandService } from '@univerjs/core';
import { addCustomRangeBySelectionFactory } from '@univerjs/docs';
+import { DocHyperLinkCommandId } from '@univerjs/docs-hyper-link';
export interface IAddDocHyperLinkCommandParams {
payload: string;
@@ -26,7 +27,7 @@ export interface IAddDocHyperLinkCommandParams {
export const AddDocHyperLinkCommand: ICommand = {
type: CommandType.COMMAND,
- id: 'docs.command.add-hyper-link',
+ id: DocHyperLinkCommandId.Add,
async handler(accessor, params) {
if (!params) {
return false;
diff --git a/packages/docs-hyper-link-ui/src/commands/commands/delete-link.command.ts b/packages/docs-hyper-link-ui/src/commands/commands/delete-link.command.ts
index 4bc236b2bd..9381c84381 100644
--- a/packages/docs-hyper-link-ui/src/commands/commands/delete-link.command.ts
+++ b/packages/docs-hyper-link-ui/src/commands/commands/delete-link.command.ts
@@ -17,6 +17,7 @@
import type { ICommand } from '@univerjs/core';
import { CommandType, ICommandService } from '@univerjs/core';
import { deleteCustomRangeFactory } from '@univerjs/docs';
+import { DocHyperLinkCommandId } from '@univerjs/docs-hyper-link';
export interface IDeleteDocHyperLinkMutationParams {
unitId: string;
@@ -26,7 +27,7 @@ export interface IDeleteDocHyperLinkMutationParams {
export const DeleteDocHyperLinkCommand: ICommand = {
type: CommandType.COMMAND,
- id: 'docs.command.delete-hyper-link',
+ id: DocHyperLinkCommandId.Delete,
async handler(accessor, params) {
if (!params) {
return false;
diff --git a/packages/docs-hyper-link-ui/src/commands/commands/update-link.command.ts b/packages/docs-hyper-link-ui/src/commands/commands/update-link.command.ts
index 37b8520863..c150a7b1de 100644
--- a/packages/docs-hyper-link-ui/src/commands/commands/update-link.command.ts
+++ b/packages/docs-hyper-link-ui/src/commands/commands/update-link.command.ts
@@ -17,6 +17,7 @@
import type { DocumentDataModel, ICommand } from '@univerjs/core';
import { CommandType, CustomRangeType, getBodySlice, ICommandService, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { DocSelectionManagerService, replaceSelectionFactory } from '@univerjs/docs';
+import { DocHyperLinkCommandId } from '@univerjs/docs-hyper-link';
export interface IUpdateDocHyperLinkCommandParams {
unitId: string;
@@ -27,7 +28,7 @@ export interface IUpdateDocHyperLinkCommandParams {
}
export const UpdateDocHyperLinkCommand: ICommand = {
- id: 'docs.command.update-hyper-link',
+ id: DocHyperLinkCommandId.Update,
type: CommandType.COMMAND,
handler(accessor, params) {
if (!params) {
diff --git a/packages/docs-hyper-link/src/__tests__/index.spec.ts b/packages/docs-hyper-link/src/__tests__/index.spec.ts
new file mode 100644
index 0000000000..1b28ef3645
--- /dev/null
+++ b/packages/docs-hyper-link/src/__tests__/index.spec.ts
@@ -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 { describe, expect, it } from 'vitest';
+import { DocHyperLinkCommandId } from '../index';
+
+describe('DocHyperLinkCommandId', () => {
+ it('keeps the public command identifiers stable', () => {
+ expect(DocHyperLinkCommandId).toEqual({
+ Add: 'docs.command.add-hyper-link',
+ Update: 'docs.command.update-hyper-link',
+ Delete: 'docs.command.delete-hyper-link',
+ });
+ });
+});
diff --git a/packages/docs-hyper-link/src/index.ts b/packages/docs-hyper-link/src/index.ts
index a3177058e9..5ebf0e9c1d 100644
--- a/packages/docs-hyper-link/src/index.ts
+++ b/packages/docs-hyper-link/src/index.ts
@@ -14,5 +14,11 @@
* limitations under the License.
*/
+export enum DocHyperLinkCommandId {
+ Add = 'docs.command.add-hyper-link',
+ Update = 'docs.command.update-hyper-link',
+ Delete = 'docs.command.delete-hyper-link',
+}
+
export type { IUniverDocsHyperLinkConfig } from './config/config';
export { UniverDocsHyperLinkPlugin } from './plugin';
diff --git a/packages/docs-ui/src/commands/commands/__tests__/misc.command.spec.ts b/packages/docs-ui/src/commands/commands/__tests__/misc.command.spec.ts
index c00b122ed5..57946849e2 100644
--- a/packages/docs-ui/src/commands/commands/__tests__/misc.command.spec.ts
+++ b/packages/docs-ui/src/commands/commands/__tests__/misc.command.spec.ts
@@ -47,6 +47,7 @@ import {
CreateHeaderFooterCommand,
DeleteTextCommand,
DocContentInsertService,
+ DocHistoryAction,
DocSelectionManagerService,
DocSkeletonManagerService,
HeaderFooterType,
@@ -634,6 +635,16 @@ describe('misc document commands', () => {
return getDoc()?.getBody();
}
+ function collectRichTextMutationParams() {
+ const params: unknown[] = [];
+ commandService.onCommandExecuted((commandInfo) => {
+ if (commandInfo.id === RichTextEditingMutation.id) {
+ params.push(commandInfo.params);
+ }
+ });
+ return params;
+ }
+
function setCollapsedSelection(startOffset: number, endOffset = startOffset) {
const selectionManager = get(DocSelectionManagerService);
selectionManager.__TEST_ONLY_setCurrentSelection({
@@ -679,6 +690,7 @@ describe('misc document commands', () => {
commandService.registerCommand(SetTextSelectionsOperation);
commandService.registerCommand(RichTextEditingMutation as unknown as ICommand);
setCollapsedSelection(5);
+ const mutationParams = collectRichTextMutationParams();
await commandService.executeCommand(InsertCustomRangeCommand.id, {
unitId: 'test-doc',
@@ -703,6 +715,9 @@ describe('misc document commands', () => {
source: 'test',
},
})]);
+ expect(mutationParams).toContainEqual(expect.objectContaining({
+ historyAction: DocHistoryAction.InsertCustomRange,
+ }));
});
it('updates paragraph styles across selected paragraphs', async () => {
@@ -712,6 +727,7 @@ describe('misc document commands', () => {
commandService.registerCommand(SetTextSelectionsOperation);
commandService.registerCommand(RichTextEditingMutation as unknown as ICommand);
setCollapsedSelection(0, 10);
+ const mutationParams = collectRichTextMutationParams();
await commandService.executeCommand(DocParagraphSettingCommand.id, {
paragraph: {
@@ -732,6 +748,9 @@ describe('misc document commands', () => {
spaceAbove: { v: 24 },
indentFirstLine: { v: 12 },
}));
+ expect(mutationParams).toContainEqual(expect.objectContaining({
+ historyAction: DocHistoryAction.FormatParagraph,
+ }));
});
it('selects the whole body when no tables are present', async () => {
@@ -1085,11 +1104,15 @@ describe('misc document commands', () => {
commandService.registerCommand(RemoveHorizontalLineCommand);
commandService.registerCommand(RichTextEditingMutation as unknown as ICommand);
setCollapsedSelection(6);
+ const mutationParams = collectRichTextMutationParams();
expect(await commandService.executeCommand(RemoveHorizontalLineCommand.id)).toBe(true);
await awaitTime(0);
expect(getBody()?.paragraphs?.[0].paragraphStyle?.borderBottom).toBeUndefined();
+ expect(mutationParams).toContainEqual(expect.objectContaining({
+ historyAction: DocHistoryAction.DeleteDivider,
+ }));
});
it('merges adjacent paragraphs through the delete merge command', async () => {
@@ -1490,6 +1513,7 @@ describe('misc document commands', () => {
commandService = get(ICommandService);
commandService.registerCommand(DocPageSetupCommand);
commandService.registerCommand(RichTextEditingMutation as unknown as ICommand);
+ const mutationParams = collectRichTextMutationParams();
const result = await commandService.executeCommand(DocPageSetupCommand.id, {
documentFlavor: DocumentFlavor.TRADITIONAL,
@@ -1512,6 +1536,9 @@ describe('misc document commands', () => {
marginLeft: 48,
marginRight: 54,
}));
+ expect(mutationParams).toContainEqual(expect.objectContaining({
+ historyAction: DocHistoryAction.UpdatePageLayout,
+ }));
});
it('inserts page setup values when the document has no explicit page style', async () => {
diff --git a/packages/docs-ui/src/commands/commands/doc-delete.command.ts b/packages/docs-ui/src/commands/commands/doc-delete.command.ts
index b44002239a..a211626f15 100644
--- a/packages/docs-ui/src/commands/commands/doc-delete.command.ts
+++ b/packages/docs-ui/src/commands/commands/doc-delete.command.ts
@@ -40,6 +40,7 @@ import {
} from '@univerjs/core';
import {
DeleteTextCommand,
+ DocHistoryAction,
DocSelectionManagerService,
RichTextEditingMutation,
UpdateTextCommand,
@@ -313,6 +314,7 @@ export const RemoveHorizontalLineCommand: ICommand = {
id: RichTextEditingMutation.id,
params: {
unitId,
+ historyAction: DocHistoryAction.DeleteDivider,
actions: [],
textRanges,
},
diff --git a/packages/docs-ui/src/commands/commands/doc-page-setup.command.ts b/packages/docs-ui/src/commands/commands/doc-page-setup.command.ts
index bff50f4ef3..e28f087514 100644
--- a/packages/docs-ui/src/commands/commands/doc-page-setup.command.ts
+++ b/packages/docs-ui/src/commands/commands/doc-page-setup.command.ts
@@ -35,7 +35,7 @@ import {
MODERN_DOCUMENT_DEFAULT_MARGIN,
UniverInstanceType,
} from '@univerjs/core';
-import { RichTextEditingMutation } from '@univerjs/docs';
+import { DocHistoryAction, RichTextEditingMutation } from '@univerjs/docs';
export interface IDocPageSetupCommandParams {
pageSize: ISize;
@@ -215,6 +215,7 @@ export const DocPageSetupCommand: ICommand = {
id: RichTextEditingMutation.id,
params: {
unitId: docDataModel.getUnitId(),
+ historyAction: DocHistoryAction.UpdatePageLayout,
actions: [],
textRanges: undefined,
},
diff --git a/packages/docs-ui/src/commands/commands/doc-paragraph-setting.command.ts b/packages/docs-ui/src/commands/commands/doc-paragraph-setting.command.ts
index a709e6915f..3220862e1b 100644
--- a/packages/docs-ui/src/commands/commands/doc-paragraph-setting.command.ts
+++ b/packages/docs-ui/src/commands/commands/doc-paragraph-setting.command.ts
@@ -17,7 +17,7 @@
import type { DocumentDataModel, IAccessor, ICommand, IMutationInfo, IParagraphStyle } from '@univerjs/core';
import type { IRichTextEditingMutationParams } from '@univerjs/docs';
import { BuildTextUtils, CommandType, getRichTextEditPath, ICommandService, IUniverInstanceService, JSONX, MemoryCursor, TextX, TextXActionType, UniverInstanceType, UpdateDocsAttributeType } from '@univerjs/core';
-import { DocSelectionManagerService, RichTextEditingMutation } from '@univerjs/docs';
+import { DocHistoryAction, DocSelectionManagerService, RichTextEditingMutation } from '@univerjs/docs';
export interface IDocParagraphSettingCommandParams {
paragraph?: Partial>;
@@ -52,6 +52,7 @@ export const DocParagraphSettingCommand: ICommand
const { unitId, rangeId = generateRandomId(), textRanges, properties, text, wholeEntity } = params;
const replaceSelectionParams: IReplaceSelectionCommandParams = {
unitId,
+ historyAction: DocHistoryAction.InsertCustomRange,
textRanges,
body: {
dataStream: text,
diff --git a/packages/docs-ui/src/commands/commands/replace-content.command.ts b/packages/docs-ui/src/commands/commands/replace-content.command.ts
index be1d525f61..026995b516 100644
--- a/packages/docs-ui/src/commands/commands/replace-content.command.ts
+++ b/packages/docs-ui/src/commands/commands/replace-content.command.ts
@@ -271,6 +271,7 @@ function getMutationParams(unitId: string, segmentId: string, docDatModel: Docum
export interface IReplaceSelectionCommandParams {
unitId: string;
+ historyAction?: string;
selection?: ITextRange;
body: IDocumentBody; // Do not contain `\r\n` at the end.
textRanges?: ITextRangeWithStyle[];
@@ -285,7 +286,7 @@ export const ReplaceSelectionCommand: ICommand =
return false;
}
const commandService = accessor.get(ICommandService);
- const { unitId, body: insertBody, textRanges, segmentId } = params;
+ const { unitId, body: insertBody, historyAction, textRanges, segmentId } = params;
const univerInstanceService = accessor.get(IUniverInstanceService);
const docDataModel = univerInstanceService.getUnit(unitId);
const docSelectionManagerService = accessor.get(DocSelectionManagerService);
@@ -332,6 +333,7 @@ export const ReplaceSelectionCommand: ICommand =
id: RichTextEditingMutation.id,
params: {
unitId,
+ historyAction,
actions: [],
textRanges: textRanges ?? [{
startOffset: insertOffset + insertBody.dataStream.length,
diff --git a/packages/docs/src/commands/mutations/core-editing.mutation.ts b/packages/docs/src/commands/mutations/core-editing.mutation.ts
index 9544b29a01..6d8c09f590 100644
--- a/packages/docs/src/commands/mutations/core-editing.mutation.ts
+++ b/packages/docs/src/commands/mutations/core-editing.mutation.ts
@@ -23,8 +23,22 @@ import { DocSelectionManagerService } from '../../services/doc-selection-manager
import { DocSkeletonManagerService } from '../../services/doc-skeleton-manager.service';
import { DocStateEmitService } from '../../services/doc-state-emit.service';
+export enum DocHistoryAction {
+ DeleteChart = 'delete-chart',
+ DeleteDivider = 'delete-divider',
+ DeleteImage = 'delete-image',
+ DeleteShape = 'delete-shape',
+ EditTableCell = 'edit-table-cell',
+ FormatParagraph = 'format-paragraph',
+ InsertCustomRange = 'insert-custom-range',
+ UpdateImage = 'update-image',
+ UpdatePageLayout = 'update-page-layout',
+}
+
export interface IRichTextEditingMutationParams extends IMutationCommonParams {
unitId: string;
+ historyAction?: string;
+ historyActions?: string[];
actions: JSONXActions;
textRanges: Nullable;
segmentId?: string;
diff --git a/packages/docs/src/index.ts b/packages/docs/src/index.ts
index d5dba204f1..9cfe0aef23 100644
--- a/packages/docs/src/index.ts
+++ b/packages/docs/src/index.ts
@@ -40,7 +40,7 @@ export type { ISetSectionHeaderFooterLinkCommandParams } from './commands/comman
export { UpdateDocumentParagraphStyleCommand } from './commands/commands/update-document-paragraph-style.command';
export { DeleteDocumentSectionBreakCommand, InsertDocumentColumnBreakCommand, InsertDocumentSectionBreakCommand, UpdateDocumentSectionCommand } from './commands/commands/update-document-section.command';
export type { IDeleteDocumentSectionBreakCommandParams, IDocumentSectionConfig, IDocumentSectionUpdate, IInsertDocumentColumnBreakCommandParams, IInsertDocumentSectionBreakCommandParams, IUpdateDocumentSectionCommandParams } from './commands/commands/update-document-section.command';
-export { RichTextEditingMutation } from './commands/mutations/core-editing.mutation';
+export { DocHistoryAction, RichTextEditingMutation } from './commands/mutations/core-editing.mutation';
export type { IRichTextEditingMutationParams } from './commands/mutations/core-editing.mutation';
export { SetTextSelectionsOperation } from './commands/operations/text-selection.operation';
export type { ISetTextSelectionsOperationParams } from './commands/operations/text-selection.operation';