feat(history): preserve document action semantics (#7597)

This commit is contained in:
Univer
2026-08-27 17:33:47 +08:00
committed by GitHub
parent 41733ecf38
commit b6dce6c6d9
21 changed files with 401 additions and 30 deletions
@@ -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({
@@ -106,7 +106,7 @@ export interface IMultiCommand<P extends object = object, R = boolean> 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<string, any>).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<P extends object>(command: ICommand<P>, 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<P extends object, R = boolean>(command: ICommand<P, R>, params?: P, options?: IExecutionOptions): Promise<R> {
// If syncOnly is true, skip execution but return true to indicate success for sync purposes
if (options?.syncOnly) {
@@ -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<typeof createFacadeTestBed>;
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<IRemoveDocDrawingCommandParams>(
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] })
);
});
});
@@ -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<typeof createFacadeTestBed>;
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<IUpdateDrawingDocTransformCommandParams>(
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 })
);
});
});
@@ -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<IUpdateDocDrawingWrappingStyleParams>(
const commandService = testBed.injector.get(ICommandService);
const mutationSpy = vi.spyOn(commandService, 'syncExecuteCommand');
const result = commandService.syncExecuteCommand<IUpdateDocDrawingWrappingStyleParams>(
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,
@@ -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<IRichTextEditingMutationParams> = {
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
@@ -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<IRichTextEditingMutationParams, IRichTextEditingMutationParams>(RichTextEditingMutation.id, {
unitId,
historyAction,
actions: actions.reduce((acc, action) => JSONX.compose(acc, action as JSONXActions), null as JSONXActions),
textRanges: null,
debounce: true,
@@ -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
@@ -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<IAddDocHyperLinkCommandParams> = {
type: CommandType.COMMAND,
id: 'docs.command.add-hyper-link',
id: DocHyperLinkCommandId.Add,
async handler(accessor, params) {
if (!params) {
return false;
@@ -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<IDeleteDocHyperLinkMutationParams> = {
type: CommandType.COMMAND,
id: 'docs.command.delete-hyper-link',
id: DocHyperLinkCommandId.Delete,
async handler(accessor, params) {
if (!params) {
return false;
@@ -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<IUpdateDocHyperLinkCommandParams> = {
id: 'docs.command.update-hyper-link',
id: DocHyperLinkCommandId.Update,
type: CommandType.COMMAND,
handler(accessor, params) {
if (!params) {
@@ -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',
});
});
});
+6
View File
@@ -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';
@@ -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 () => {
@@ -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,
},
@@ -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<IDocPageSetupCommandParams> = {
id: RichTextEditingMutation.id,
params: {
unitId: docDataModel.getUnitId(),
historyAction: DocHistoryAction.UpdatePageLayout,
actions: [],
textRanges: undefined,
},
@@ -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<Pick<IParagraphStyle, 'hanging' | 'horizontalAlign' | 'spaceBelow' | 'spaceAbove' | 'indentEnd' | 'indentStart' | 'lineSpacing' | 'indentFirstLine' | 'snapToGrid' | 'spacingRule'>>;
@@ -52,6 +52,7 @@ export const DocParagraphSettingCommand: ICommand<IDocParagraphSettingCommandPar
id: RichTextEditingMutation.id,
params: {
unitId,
historyAction: DocHistoryAction.FormatParagraph,
actions: [],
textRanges: docRanges,
},
@@ -18,6 +18,7 @@ import type { ICommand } from '@univerjs/core';
import type { ITextRangeWithStyle } from '@univerjs/engine-render';
import type { IReplaceSelectionCommandParams } from './replace-content.command';
import { CommandType, CustomRangeType, generateRandomId, ICommandService } from '@univerjs/core';
import { DocHistoryAction } from '@univerjs/docs';
import { ReplaceSelectionCommand } from './replace-content.command';
export interface IInsertCustomRangeCommandParams {
@@ -38,6 +39,7 @@ export const InsertCustomRangeCommand: ICommand<IInsertCustomRangeCommandParams>
const { unitId, rangeId = generateRandomId(), textRanges, properties, text, wholeEntity } = params;
const replaceSelectionParams: IReplaceSelectionCommandParams = {
unitId,
historyAction: DocHistoryAction.InsertCustomRange,
textRanges,
body: {
dataStream: text,
@@ -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<IReplaceSelectionCommandParams> =
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<DocumentDataModel>(unitId);
const docSelectionManagerService = accessor.get(DocSelectionManagerService);
@@ -332,6 +333,7 @@ export const ReplaceSelectionCommand: ICommand<IReplaceSelectionCommandParams> =
id: RichTextEditingMutation.id,
params: {
unitId,
historyAction,
actions: [],
textRanges: textRanges ?? [{
startOffset: insertOffset + insertBody.dataStream.length,
@@ -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<ITextRangeWithStyle[]>;
segmentId?: string;
+1 -1
View File
@@ -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';