mirror of
https://github.com/dream-num/univer.git
synced 2026-08-28 14:56:51 +08:00
feat(history): preserve document action semantics (#7597)
This commit is contained in:
@@ -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) {
|
||||
|
||||
+91
@@ -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] })
|
||||
);
|
||||
});
|
||||
});
|
||||
+83
@@ -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 })
|
||||
);
|
||||
});
|
||||
});
|
||||
+8
-1
@@ -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,
|
||||
|
||||
+8
-1
@@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user