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';