snapshot of https://github.com/dream-num/univer.git (dev, 2026-09-16; blobs > 131072B omitted)
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-16 05:09:45 +05:00
commit bcf6ef63ad
7118 changed files with 1236854 additions and 0 deletions

View File

@@ -0,0 +1,222 @@
/**
* 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 { IWorkbookData } from '@univerjs/core';
import type { ISelectionWithStyle } from '@univerjs/sheets';
import type { ISheetNote } from '../../../models/sheets-note.model';
import {
ICommandService,
ILogService,
Inject,
Injector,
IUndoRedoService,
IUniverInstanceService,
LocaleType,
LogLevel,
Plugin,
RANGE_TYPE,
touchDependencies,
UndoCommand,
Univer,
UniverInstanceType,
} from '@univerjs/core';
import { SheetsSelectionsService } from '@univerjs/sheets';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { SheetsNoteModel } from '../../../models/sheets-note.model';
import { RemoveNoteMutation, ToggleNotePopupMutation, UpdateNoteMutation } from '../../mutations/note.mutation';
import { SheetDeleteNoteCommand, SheetToggleNotePopupCommand, SheetUpdateNoteCommand } from '../note.command';
const unitId = 'test';
const subUnitId = 'sheet1';
const TEST_WORKBOOK_DATA: IWorkbookData = {
id: unitId,
sheetOrder: [subUnitId],
name: '',
appVersion: '0.15.4',
locale: LocaleType.EN_US,
styles: {},
sheets: {
[subUnitId]: {
id: subUnitId,
name: 'Sheet1',
rowCount: 10,
columnCount: 10,
cellData: {},
},
},
};
function createCommandTestBed() {
const univer = new Univer();
const injector = univer.__getInjector();
const get = injector.get.bind(injector);
class TestPlugin extends Plugin {
static override pluginName = 'test-plugin';
static override type = UniverInstanceType.UNIVER_SHEET;
constructor(
_config: undefined,
@Inject(Injector) override readonly _injector: Injector
) {
super();
}
override onStarting(): void {
// Ensure undo/redo exists and registers commands.
this._injector.get(IUndoRedoService);
this._injector.add([SheetsSelectionsService]);
this._injector.add([SheetsNoteModel]);
touchDependencies(this._injector, [
[SheetsSelectionsService],
[SheetsNoteModel],
]);
}
}
univer.registerPlugin(TestPlugin);
univer.createUnit(UniverInstanceType.UNIVER_SHEET, TEST_WORKBOOK_DATA);
get(IUniverInstanceService).focusUnit(unitId);
const logService = get(ILogService);
logService.setLogLevel(LogLevel.SILENT);
const commandService = get(ICommandService);
commandService.registerCommand(UpdateNoteMutation);
commandService.registerCommand(RemoveNoteMutation);
commandService.registerCommand(ToggleNotePopupMutation);
commandService.registerCommand(SheetUpdateNoteCommand);
commandService.registerCommand(SheetDeleteNoteCommand);
commandService.registerCommand(SheetToggleNotePopupCommand);
return {
univer,
get,
injector,
commandService,
};
}
function setSingleCellSelection(get: Injector['get'], row: number, col: number) {
const selections = get(SheetsSelectionsService);
const selection: ISelectionWithStyle = {
range: {
startRow: row,
endRow: row + 1,
startColumn: col,
endColumn: col + 1,
rangeType: RANGE_TYPE.NORMAL,
},
primary: {
startRow: row,
endRow: row + 1,
startColumn: col,
endColumn: col + 1,
actualRow: row,
actualColumn: col,
isMerged: false,
isMergedMainCell: false,
},
style: null,
};
selections.setSelections([selection]);
}
describe('sheets-note commands', () => {
let univer: Univer;
let get: Injector['get'];
let commandService: ICommandService;
beforeEach(() => {
const testBed = createCommandTestBed();
univer = testBed.univer;
get = testBed.get;
commandService = testBed.commandService;
});
afterEach(() => univer.dispose());
it('SheetUpdateNoteCommand pushes undo correctly (new note -> undo removes)', async () => {
const note: ISheetNote = { id: 'n1', row: 1, col: 2, width: 160, height: 72, note: 'hello' };
const res = commandService.syncExecuteCommand(SheetUpdateNoteCommand.id, {
unitId,
sheetId: subUnitId,
row: 1,
col: 2,
note,
});
expect(res).toBeTruthy();
const model = get(SheetsNoteModel);
expect(model.getNote(unitId, subUnitId, { row: 1, col: 2 })?.note).toBe('hello');
// Undo should remove the note.
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(model.getNote(unitId, subUnitId, { row: 1, col: 2 })).toBeUndefined();
});
it('SheetUpdateNoteCommand pushes undo correctly (existing note -> undo restores)', async () => {
const model = get(SheetsNoteModel);
model.updateNote(unitId, subUnitId, 1, 2, { id: 'n2', row: 1, col: 2, width: 160, height: 72, note: 'old' });
const note: ISheetNote = { id: 'n2', row: 1, col: 2, width: 160, height: 72, note: 'new' };
const res = commandService.syncExecuteCommand(SheetUpdateNoteCommand.id, {
unitId,
sheetId: subUnitId,
row: 1,
col: 2,
note,
});
expect(res).toBeTruthy();
expect(model.getNote(unitId, subUnitId, { row: 1, col: 2 })?.note).toBe('new');
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(model.getNote(unitId, subUnitId, { row: 1, col: 2 })?.note).toBe('old');
});
it('SheetDeleteNoteCommand removes note at current selection and supports undo', async () => {
const model = get(SheetsNoteModel);
model.updateNote(unitId, subUnitId, 2, 3, { id: 'n3', row: 2, col: 3, width: 160, height: 72, note: 'to delete' });
setSingleCellSelection(get, 2, 3);
const res = commandService.syncExecuteCommand(SheetDeleteNoteCommand.id);
expect(res).toBeTruthy();
expect(model.getNote(unitId, subUnitId, { row: 2, col: 3 })).toBeUndefined();
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(model.getNote(unitId, subUnitId, { row: 2, col: 3 })?.note).toBe('to delete');
});
it('SheetToggleNotePopupCommand toggles show state at current selection and supports undo', async () => {
const model = get(SheetsNoteModel);
model.updateNote(unitId, subUnitId, 4, 4, { id: 'n4', row: 4, col: 4, width: 160, height: 72, note: 'toggle', show: false });
setSingleCellSelection(get, 4, 4);
const res = commandService.syncExecuteCommand(SheetToggleNotePopupCommand.id);
expect(res).toBeTruthy();
expect(model.getNote(unitId, subUnitId, { row: 4, col: 4 })?.show).toBe(true);
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(model.getNote(unitId, subUnitId, { row: 4, col: 4 })?.show).toBe(false);
});
// Avoid directly asserting pure exports without business value.
});

View File

@@ -0,0 +1,192 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ICommand, IMutationInfo } from '@univerjs/core';
import type { IRemoveNoteMutationParams, IToggleNotePopupMutationParams, IUpdateNoteMutationParams } from '../mutations/note.mutation';
import { CommandType, ICommandService, IUndoRedoService, IUniverInstanceService } from '@univerjs/core';
import { getSheetCommandTarget, SheetsSelectionsService } from '@univerjs/sheets';
import { SheetsNoteModel } from '../../models/sheets-note.model';
import { RemoveNoteMutation, ToggleNotePopupMutation, UpdateNoteMutation } from '../mutations/note.mutation';
export const SheetDeleteNoteCommand: ICommand = {
id: 'sheet.command.delete-note',
type: CommandType.COMMAND,
handler: (accessor) => {
const target = getSheetCommandTarget(accessor.get(IUniverInstanceService));
if (!target) return false;
const sheetsSelectionsService = accessor.get(SheetsSelectionsService);
const selection = sheetsSelectionsService.getCurrentLastSelection();
if (!selection?.primary) return false;
const sheetsNoteModel = accessor.get(SheetsNoteModel);
const { unitId, subUnitId } = target;
const { actualColumn, actualRow } = selection.primary;
const note = sheetsNoteModel.getNote(unitId, subUnitId, { row: actualRow, col: actualColumn });
if (!note) return false;
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const redoMutation: IMutationInfo<IRemoveNoteMutationParams> = {
id: RemoveNoteMutation.id,
params: {
unitId,
sheetId: subUnitId,
noteId: note.id,
},
};
const undoMutation: IMutationInfo<IUpdateNoteMutationParams> = {
id: UpdateNoteMutation.id,
params: {
unitId,
sheetId: subUnitId,
row: actualRow,
col: actualColumn,
note: { ...note },
},
};
const result = commandService.syncExecuteCommand(redoMutation.id, redoMutation.params);
if (result) {
undoRedoService.pushUndoRedo({
unitID: unitId,
redoMutations: [redoMutation],
undoMutations: [undoMutation],
});
return true;
}
return false;
},
};
export const SheetToggleNotePopupCommand: ICommand = {
id: 'sheet.command.toggle-note-popup',
type: CommandType.COMMAND,
handler: (accessor) => {
const target = getSheetCommandTarget(accessor.get(IUniverInstanceService));
if (!target) return false;
const sheetsSelectionsService = accessor.get(SheetsSelectionsService);
const selection = sheetsSelectionsService.getCurrentLastSelection();
if (!selection?.primary) return false;
const sheetsNoteModel = accessor.get(SheetsNoteModel);
const { unitId, subUnitId } = target;
const { actualColumn, actualRow } = selection.primary;
const note = sheetsNoteModel.getNote(unitId, subUnitId, { row: actualRow, col: actualColumn });
if (!note) return false;
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const redoMutation: IMutationInfo<IToggleNotePopupMutationParams> = {
id: ToggleNotePopupMutation.id,
params: {
unitId,
sheetId: subUnitId,
noteId: note.id,
},
};
const undoMutation: IMutationInfo<IToggleNotePopupMutationParams> = {
id: ToggleNotePopupMutation.id,
params: {
unitId,
sheetId: subUnitId,
noteId: note.id,
},
};
const result = commandService.syncExecuteCommand(redoMutation.id, redoMutation.params);
if (result) {
undoRedoService.pushUndoRedo({
unitID: unitId,
redoMutations: [redoMutation],
undoMutations: [undoMutation],
});
return true;
}
return false;
},
};
export const SheetUpdateNoteCommand: ICommand<IUpdateNoteMutationParams> = {
id: 'sheet.command.update-note',
type: CommandType.COMMAND,
handler: (accessor, params: IUpdateNoteMutationParams) => {
const target = getSheetCommandTarget(accessor.get(IUniverInstanceService), params);
if (!target) return false;
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const sheetsNoteModel = accessor.get(SheetsNoteModel);
const { unitId, subUnitId } = target;
const { row, col, note } = params;
const oldNote = sheetsNoteModel.getNote(unitId, subUnitId, { noteId: note.id, row, col });
const redoMutation: IMutationInfo<IUpdateNoteMutationParams> = {
id: UpdateNoteMutation.id,
params: {
unitId,
sheetId: subUnitId,
row,
col,
note,
},
};
const undoMutations: IMutationInfo[] = [];
if (oldNote) {
const undoMutation: IMutationInfo<IUpdateNoteMutationParams> = {
id: UpdateNoteMutation.id,
params: {
unitId,
sheetId: subUnitId,
row,
col,
note: { ...oldNote },
},
};
undoMutations.push(undoMutation);
} else {
const undoMutation: IMutationInfo<IRemoveNoteMutationParams> = {
id: RemoveNoteMutation.id,
params: {
unitId,
sheetId: subUnitId,
row,
col,
},
};
undoMutations.push(undoMutation);
}
const result = commandService.syncExecuteCommand(redoMutation.id, redoMutation.params);
if (result) {
undoRedoService.pushUndoRedo({
unitID: unitId,
redoMutations: [redoMutation],
undoMutations,
});
return true;
}
return false;
},
};

View File

@@ -0,0 +1,104 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IAccessor, IMutation } from '@univerjs/core';
import type { ISheetNote } from '../../models/sheets-note.model';
import { CommandType } from '@univerjs/core';
import { SheetsNoteModel } from '../../models/sheets-note.model';
export interface IUpdateNoteMutationParams {
unitId: string;
sheetId: string;
row: number;
col: number;
note: ISheetNote;
silent?: boolean;
}
export const UpdateNoteMutation: IMutation<IUpdateNoteMutationParams> = {
id: 'sheet.mutation.update-note',
type: CommandType.MUTATION,
handler: (accessor: IAccessor, params: IUpdateNoteMutationParams) => {
const { unitId, sheetId, row, col, note, silent } = params;
const sheetsNoteModel = accessor.get(SheetsNoteModel);
sheetsNoteModel.updateNote(unitId, sheetId, row, col, note, silent);
return true;
},
};
export interface IRemoveNoteMutationParams {
unitId: string;
sheetId: string;
noteId?: string;
row?: number;
col?: number;
silent?: boolean;
}
export const RemoveNoteMutation: IMutation<IRemoveNoteMutationParams> = {
id: 'sheet.mutation.remove-note',
type: CommandType.MUTATION,
handler: (accessor: IAccessor, params: IRemoveNoteMutationParams) => {
const { unitId, sheetId, noteId, row, col, silent } = params;
const sheetsNoteModel = accessor.get(SheetsNoteModel);
sheetsNoteModel.removeNote(unitId, sheetId, { noteId, row, col, silent });
return true;
},
};
export interface IToggleNotePopupMutationParams {
unitId: string;
sheetId: string;
noteId?: string;
row?: number;
col?: number;
silent?: boolean;
}
export const ToggleNotePopupMutation: IMutation<IToggleNotePopupMutationParams> = {
id: 'sheet.mutation.toggle-note-popup',
type: CommandType.MUTATION,
handler: (accessor: IAccessor, params: IToggleNotePopupMutationParams) => {
const { unitId, sheetId, noteId, row, col, silent } = params;
const sheetsNoteModel = accessor.get(SheetsNoteModel);
sheetsNoteModel.toggleNotePopup(unitId, sheetId, { noteId, row, col, silent });
return true;
},
};
export interface IUpdateNotePositionMutationParams {
unitId: string;
sheetId: string;
noteId?: string;
row?: number;
col?: number;
newPosition: {
row: number;
col: number;
};
silent?: boolean;
}
export const UpdateNotePositionMutation: IMutation<IUpdateNotePositionMutationParams> = {
id: 'sheet.mutation.update-note-position',
type: CommandType.MUTATION,
handler: (accessor: IAccessor, params: IUpdateNotePositionMutationParams) => {
const { unitId, sheetId, noteId, row, col, newPosition, silent } = params;
const sheetsNoteModel = accessor.get(SheetsNoteModel);
sheetsNoteModel.updateNotePosition(unitId, sheetId, { noteId, row, col, newRow: newPosition.row, newCol: newPosition.col, silent });
return true;
},
};

View File

@@ -0,0 +1,23 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const SHEETS_NOTE_PLUGIN_CONFIG_KEY = 'sheets-note.config';
export const configSymbol = Symbol(SHEETS_NOTE_PLUGIN_CONFIG_KEY);
export interface IUniverSheetsNoteConfig {}
export const defaultPluginConfig: IUniverSheetsNoteConfig = {};

View File

@@ -0,0 +1,17 @@
/**
* 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.
*/
export const PLUGIN_NAME = 'SHEET_NOTE_PLUGIN';

View File

@@ -0,0 +1,257 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Dependency, IWorkbookData, Nullable, Workbook } from '@univerjs/core';
import type { IInsertColCommandParams, IInsertRowCommandParams, IRemoveColByRangeCommandParams, IRemoveRowByRangeCommandParams } from '@univerjs/sheets';
import type { ISheetNote } from '../../models/sheets-note.model';
import { Direction, ICommandService, ILogService, Inject, Injector, IUndoRedoService, IUniverInstanceService, LocaleType, LogLevel, Plugin, touchDependencies, UndoCommand, Univer, UniverInstanceType } from '@univerjs/core';
import {
InsertColByRangeCommand,
InsertColMutation,
InsertRowByRangeCommand,
InsertRowMutation,
RefRangeService,
RemoveColByRangeCommand,
RemoveColMutation,
RemoveRowByRangeCommand,
RemoveRowMutation,
SetRangeValuesMutation,
SetSelectionsOperation,
SheetInterceptorService,
SheetsSelectionsService,
} from '@univerjs/sheets';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { SheetsNoteModel } from '../../models/sheets-note.model';
import { SheetsNoteRefRangeController } from '../sheets-note-ref-range.controller';
import { SheetsNoteResourceController } from '../sheets-note-resource.controller';
import { SheetsNoteController } from '../sheets.note.controller';
const unitId = 'test';
const subUnitId = 'sheet1';
const TEST_WORKBOOK_DATA: IWorkbookData = {
id: unitId,
sheetOrder: [subUnitId],
name: '',
appVersion: '0.15.4',
locale: LocaleType.EN_US,
styles: {},
sheets: {
[subUnitId]: {
id: subUnitId,
name: 'Sheet1',
rowCount: 10,
columnCount: 10,
cellData: {},
},
},
resources: [
{
name: 'SHEET_NOTE_PLUGIN',
data: '{"sheet1":{"1":{"1":{"width":160,"height":72,"note":"1","row":1,"col":1,"id":"cwvCsB"}},"2":{"2":{"width":160,"height":72,"note":"2","row":2,"col":2,"id":"URAqOO"}},"3":{"3":{"width":160,"height":72,"note":"3","row":3,"col":3,"id":"tCk4Qd"}},"4":{"4":{"width":160,"height":72,"note":"4","row":4,"col":4,"id":"nIasaz"}}}}',
},
],
};
export function createRefRangeTestBed() {
const univer = new Univer();
const injector = univer.__getInjector();
const get = injector.get.bind(injector);
class TestPlugin extends Plugin {
static override pluginName = 'test-plugin';
static override type = UniverInstanceType.UNIVER_SHEET;
constructor(
_config: undefined,
@Inject(Injector) override readonly _injector: Injector
) {
super();
}
override onStarting(): void {
this._injector.get(IUndoRedoService);
([
[SheetInterceptorService],
[RefRangeService],
[SheetsSelectionsService],
[SheetsNoteModel],
[SheetsNoteController],
[SheetsNoteResourceController],
[SheetsNoteRefRangeController],
] as Dependency[]).forEach((d) => this._injector.add(d));
touchDependencies(this._injector, [
[SheetsNoteModel],
[SheetsNoteController],
[SheetsNoteResourceController],
]);
}
override onReady(): void {
touchDependencies(this._injector, [
[SheetsNoteRefRangeController],
]);
}
}
univer.registerPlugin(TestPlugin);
const sheet = univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, TEST_WORKBOOK_DATA);
const univerInstanceService = get(IUniverInstanceService);
univerInstanceService.focusUnit('test');
const logService = get(ILogService);
logService.setLogLevel(LogLevel.SILENT);
return {
univer,
get,
sheet,
};
}
describe('test note ref range controller', () => {
let univer: Univer;
let get: Injector['get'];
let commandService: ICommandService;
let sheetsNoteModel: SheetsNoteModel;
let getNoteByPosition: (row: number, col: number) => Nullable<ISheetNote>;
beforeEach(() => {
const testBed = createRefRangeTestBed();
univer = testBed.univer;
get = testBed.get;
commandService = get(ICommandService);
commandService.registerCommand(InsertRowByRangeCommand);
commandService.registerCommand(InsertColByRangeCommand);
commandService.registerCommand(RemoveRowByRangeCommand);
commandService.registerCommand(RemoveColByRangeCommand);
commandService.registerCommand(InsertRowMutation);
commandService.registerCommand(InsertColMutation);
commandService.registerCommand(RemoveRowMutation);
commandService.registerCommand(RemoveColMutation);
commandService.registerCommand(SetRangeValuesMutation);
commandService.registerCommand(SetSelectionsOperation);
sheetsNoteModel = get(SheetsNoteModel);
getNoteByPosition = (row: number, col: number) => sheetsNoteModel.getNote(unitId, subUnitId, { row, col });
});
afterEach(() => univer.dispose());
describe('test note ref range change', () => {
it('test D4 cell note when insert row', async () => {
const D4CellNote = {
width: 160,
height: 72,
note: '3',
row: 3,
col: 3,
id: 'tCk4Qd',
};
const params: IInsertRowCommandParams = {
unitId,
subUnitId,
range: { startRow: 3, endRow: 3, startColumn: 0, endColumn: 9 },
direction: Direction.DOWN,
};
expect(commandService.syncExecuteCommand(InsertRowByRangeCommand.id, params)).toBeTruthy();
expect(getNoteByPosition(3, 3)).toBeUndefined();
expect(getNoteByPosition(4, 3)).toStrictEqual({ ...D4CellNote, row: 4, col: 3 });
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(getNoteByPosition(3, 3)).toStrictEqual(D4CellNote);
expect(getNoteByPosition(4, 3)).toBeUndefined();
});
it('test D4 cell note when insert column', async () => {
const D4CellNote = {
width: 160,
height: 72,
note: '3',
row: 3,
col: 3,
id: 'tCk4Qd',
};
const params: IInsertColCommandParams = {
unitId,
subUnitId,
range: { startRow: 0, endRow: 9, startColumn: 3, endColumn: 3 },
direction: Direction.RIGHT,
};
expect(commandService.syncExecuteCommand(InsertColByRangeCommand.id, params)).toBeTruthy();
expect(getNoteByPosition(3, 3)).toBeUndefined();
expect(getNoteByPosition(3, 4)).toStrictEqual({ ...D4CellNote, row: 3, col: 4 });
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(getNoteByPosition(3, 3)).toStrictEqual(D4CellNote);
expect(getNoteByPosition(3, 4)).toBeUndefined();
});
it('test D4 cell note when remove row', async () => {
const D4CellNote = {
width: 160,
height: 72,
note: '3',
row: 3,
col: 3,
id: 'tCk4Qd',
};
const params: IRemoveRowByRangeCommandParams = {
unitId,
subUnitId,
range: { startRow: 2, endRow: 2, startColumn: 0, endColumn: 9 },
};
expect(commandService.syncExecuteCommand(RemoveRowByRangeCommand.id, params)).toBeTruthy();
expect(getNoteByPosition(3, 3)).toBeUndefined();
expect(getNoteByPosition(2, 3)).toStrictEqual({ ...D4CellNote, row: 2, col: 3 });
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(getNoteByPosition(3, 3)).toStrictEqual(D4CellNote);
expect(getNoteByPosition(2, 3)).toBeUndefined();
});
it('test D4 cell note when remove column', async () => {
const D4CellNote = {
width: 160,
height: 72,
note: '3',
row: 3,
col: 3,
id: 'tCk4Qd',
};
const params: IRemoveColByRangeCommandParams = {
unitId,
subUnitId,
range: { startRow: 0, endRow: 9, startColumn: 2, endColumn: 2 },
};
expect(commandService.syncExecuteCommand(RemoveColByRangeCommand.id, params)).toBeTruthy();
expect(getNoteByPosition(3, 3)).toBeUndefined();
expect(getNoteByPosition(3, 2)).toStrictEqual({ ...D4CellNote, row: 3, col: 2 });
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(getNoteByPosition(3, 3)).toStrictEqual(D4CellNote);
expect(getNoteByPosition(3, 2)).toBeUndefined();
});
});
});

View File

@@ -0,0 +1,258 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Dependency, IWorkbookData, Workbook } from '@univerjs/core';
import type { ICopySheetCommandParams, IRemoveSheetCommandParams } from '@univerjs/sheets';
import type { ISheetNote } from '../../models/sheets-note.model';
import {
ICommandService,
ILogService,
Inject,
Injector,
IResourceManagerService,
IUndoRedoService,
IUniverInstanceService,
LocaleService,
LocaleType,
LogLevel,
Plugin,
touchDependencies,
UndoCommand,
Univer,
UniverInstanceType,
} from '@univerjs/core';
import {
CopySheetCommand,
CopyWorksheetEndMutation,
InsertColByRangeCommand,
InsertColMutation,
InsertRowByRangeCommand,
InsertRowMutation,
InsertSheetMutation,
RemoveColByRangeCommand,
RemoveColMutation,
RemoveRowByRangeCommand,
RemoveRowMutation,
RemoveSheetCommand,
RemoveSheetMutation,
SetRangeValuesMutation,
SetSelectionsOperation,
SetWorksheetActivateCommand,
SetWorksheetActiveOperation,
SheetInterceptorService,
SheetLazyExecuteScheduleService,
SheetsSelectionsService,
} from '@univerjs/sheets';
import enUS from '@univerjs/sheets/locale/en-US';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
// Note mutations are registered by SheetsNoteController.
import { PLUGIN_NAME } from '../../const';
import { SheetsNoteModel } from '../../models/sheets-note.model';
import { SheetsNoteResourceController } from '../sheets-note-resource.controller';
import { SheetsNoteController } from '../sheets.note.controller';
const unitId = 'test';
const subUnitId = 'sheet1';
const TEST_WORKBOOK_DATA: IWorkbookData = {
id: unitId,
sheetOrder: [subUnitId],
name: '',
appVersion: '0.15.4',
locale: LocaleType.EN_US,
styles: {},
sheets: {
[subUnitId]: {
id: subUnitId,
name: 'Sheet1',
rowCount: 10,
columnCount: 10,
cellData: {},
},
},
};
function createTestBed(resources?: Array<{ name: string; data: string }>) {
const univer = new Univer();
const injector = univer.__getInjector();
const get = injector.get.bind(injector);
class TestPlugin extends Plugin {
static override pluginName = 'test-plugin';
static override type = UniverInstanceType.UNIVER_SHEET;
constructor(
_config: undefined,
@Inject(Injector) override readonly _injector: Injector
) {
super();
}
override onStarting(): void {
// Ensure undo/redo exists and registers commands.
this._injector.get(IUndoRedoService);
([
[SheetInterceptorService],
[SheetsSelectionsService],
[SheetLazyExecuteScheduleService],
[SheetsNoteModel],
[SheetsNoteController],
[SheetsNoteResourceController],
] as Dependency[]).forEach((d) => this._injector.add(d));
touchDependencies(this._injector, [
[SheetsNoteModel],
[SheetsNoteController],
[SheetsNoteResourceController],
]);
}
}
univer.registerPlugin(TestPlugin);
univer.createUnit(UniverInstanceType.UNIVER_SHEET, {
...TEST_WORKBOOK_DATA,
resources: resources ?? [],
});
get(IUniverInstanceService).focusUnit(unitId);
const logService = get(ILogService);
logService.setLogLevel(LogLevel.SILENT);
const localeService = get(LocaleService);
localeService.load({ enUS });
localeService.setLocale(LocaleType.EN_US);
const commandService = get(ICommandService);
// Register sheet structure commands/mutations that resource controller intercepts.
commandService.registerCommand(RemoveSheetCommand);
commandService.registerCommand(CopySheetCommand);
commandService.registerCommand(RemoveSheetMutation);
commandService.registerCommand(InsertSheetMutation);
commandService.registerCommand(CopyWorksheetEndMutation);
commandService.registerCommand(SetRangeValuesMutation);
commandService.registerCommand(SetSelectionsOperation);
commandService.registerCommand(SetWorksheetActivateCommand);
commandService.registerCommand(SetWorksheetActiveOperation);
// Keep parity with existing ref-range test bed (some sheet commands expect these).
commandService.registerCommand(InsertRowByRangeCommand);
commandService.registerCommand(InsertColByRangeCommand);
commandService.registerCommand(RemoveRowByRangeCommand);
commandService.registerCommand(RemoveColByRangeCommand);
commandService.registerCommand(InsertRowMutation);
commandService.registerCommand(InsertColMutation);
commandService.registerCommand(RemoveRowMutation);
commandService.registerCommand(RemoveColMutation);
// Note mutations are registered by SheetsNoteController.
return {
univer,
get,
injector,
commandService,
};
}
describe('SheetsNoteResourceController', () => {
let univer: Univer;
let get: Injector['get'];
let commandService: ICommandService;
beforeEach(() => {
// Create empty bed by default.
const testBed = createTestBed();
univer = testBed.univer;
get = testBed.get;
commandService = testBed.commandService;
});
afterEach(() => univer.dispose());
it('loads notes from snapshot resources', () => {
univer.dispose();
const note: ISheetNote = { id: 'n1', row: 1, col: 1, width: 160, height: 72, note: 'from snapshot' };
const resources = [{
name: PLUGIN_NAME,
data: JSON.stringify({ [subUnitId]: { 1: { 1: note } } }),
}];
const testBed = createTestBed(resources);
univer = testBed.univer;
get = testBed.get;
const model = get(SheetsNoteModel);
expect(model.getNote(unitId, subUnitId, { row: 1, col: 1 })?.note).toBe('from snapshot');
});
it('clears unit notes on resource unload', () => {
const model = get(SheetsNoteModel);
model.updateNote(unitId, subUnitId, 2, 2, { id: 'n2', row: 2, col: 2, width: 160, height: 72, note: 'note' });
expect(model.getSheetNotes(unitId, subUnitId)?.size).toBe(1);
const resourceManager = get(IResourceManagerService);
resourceManager.unloadResources(unitId, UniverInstanceType.UNIVER_SHEET);
expect(model.getUnitNotes(unitId)).toBeUndefined();
});
it('intercepts RemoveSheetCommand to remove notes and supports undo', async () => {
const model = get(SheetsNoteModel);
model.updateNote(unitId, subUnitId, 3, 4, { id: 'n3', row: 3, col: 4, width: 160, height: 72, note: 'note' });
// Need at least 2 sheets for RemoveSheetCommand to pass.
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
workbook.addWorksheet('sheet2', 1, { id: 'sheet2', name: 'Sheet2', rowCount: 10, columnCount: 10, cellData: {} });
expect(await commandService.executeCommand(SetWorksheetActivateCommand.id, { unitId, subUnitId })).toBeTruthy();
const params: IRemoveSheetCommandParams = { unitId, subUnitId };
expect(commandService.syncExecuteCommand(RemoveSheetCommand.id, params)).toBeTruthy();
expect(model.getSheetNotes(unitId, subUnitId)?.size).toBe(0);
// Undo should restore the sheet and its notes via UpdateNoteMutation in undo list.
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(model.getNote(unitId, subUnitId, { row: 3, col: 4 })?.id).toBe('n3');
});
it('intercepts CopySheetCommand to copy notes to target sheet and supports undo', async () => {
const model = get(SheetsNoteModel);
model.updateNote(unitId, subUnitId, 1, 2, { id: 'n4', row: 1, col: 2, width: 160, height: 72, note: 'note' });
// Need at least 2 sheets for RemoveSheetCommand used by undo in CopySheetCommand to pass.
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
workbook.addWorksheet('sheet2', 1, { id: 'sheet2', name: 'Sheet2', rowCount: 10, columnCount: 10, cellData: {} });
expect(await commandService.executeCommand(SetWorksheetActivateCommand.id, { unitId, subUnitId })).toBeTruthy();
const params: ICopySheetCommandParams = { unitId, subUnitId };
expect(commandService.syncExecuteCommand(CopySheetCommand.id, params)).toBeTruthy();
const copiedSheetId = workbook.getSheets()[1].getSheetId();
expect(copiedSheetId).not.toBe(subUnitId);
expect(model.getSheetNotes(unitId, copiedSheetId)?.size).toBe(1);
const copiedNotes = Array.from(model.getSheetNotes(unitId, copiedSheetId)!.values());
expect(copiedNotes[0].row).toBe(1);
expect(copiedNotes[0].col).toBe(2);
expect(copiedNotes[0].note).toBe('note');
// Copied note should have a new id.
expect(copiedNotes[0].id).not.toBe('n4');
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(model.getSheetNotes(unitId, copiedSheetId)?.size).toBe(0);
});
});

View File

@@ -0,0 +1,204 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IDisposable, IRange, Nullable } from '@univerjs/core';
import type { EffectRefRangeParams } from '@univerjs/sheets';
import type { ISheetNote } from '../models/sheets-note.model';
import { Disposable, ICommandService, Inject, sequenceExecuteAsync } from '@univerjs/core';
import { handleCommonRangeChangeWithEffectRefCommandsSkipNoInterests, RefRangeService, SheetsSelectionsService } from '@univerjs/sheets';
import { RemoveNoteMutation, UpdateNoteMutation, UpdateNotePositionMutation } from '../commands/mutations/note.mutation';
import { SheetsNoteModel } from '../models/sheets-note.model';
export class SheetsNoteRefRangeController extends Disposable {
private _disposableMap = new Map<string, IDisposable>();
private _watcherMap = new Map<string, IDisposable>();
constructor(
@Inject(RefRangeService) private readonly _refRangeService: RefRangeService,
@Inject(SheetsNoteModel) private readonly _sheetsNoteModel: SheetsNoteModel,
@Inject(SheetsSelectionsService) private readonly _selectionManagerService: SheetsSelectionsService,
@ICommandService private readonly _commandService: ICommandService
) {
super();
this._initData();
this._initRefRange();
}
private _getIdWithUnitId(unitId: string, subUnitId: string, row: number, col: number) {
return `${unitId}-${subUnitId}-${row}-${col}`;
}
private _handleRangeChange = (unitId: string, subUnitId: string, note: ISheetNote, row: number, col: number, resultRange: Nullable<IRange>, silent?: boolean) => {
if (!resultRange) {
return {
redos: [{
id: RemoveNoteMutation.id,
params: {
unitId,
sheetId: subUnitId,
noteId: note.id,
},
}],
undos: [{
id: UpdateNoteMutation.id,
params: {
unitId,
sheetId: subUnitId,
row,
col,
note,
},
}],
};
}
return {
redos: [{
id: UpdateNotePositionMutation.id,
params: {
unitId,
sheetId: subUnitId,
noteId: note.id,
newPosition: {
row: resultRange.startRow,
col: resultRange.startColumn,
},
silent,
},
}],
undos: [{
id: UpdateNotePositionMutation.id,
params: {
unitId,
sheetId: subUnitId,
noteId: note.id,
newPosition: {
row,
col,
},
note,
silent,
},
}],
};
};
private _register(unitId: string, subUnitId: string, note: ISheetNote, row: number, col: number) {
const oldRange: IRange = {
startColumn: col,
endColumn: col,
startRow: row,
endRow: row,
};
this._disposableMap.set(
this._getIdWithUnitId(unitId, subUnitId, row, col),
this._refRangeService.registerRefRange(oldRange, (commandInfo: EffectRefRangeParams) => {
const resultRanges = handleCommonRangeChangeWithEffectRefCommandsSkipNoInterests(oldRange, commandInfo, { selectionManagerService: this._selectionManagerService });
const resultRange = Array.isArray(resultRanges) ? resultRanges[0] : resultRanges;
if (resultRange && resultRange.startColumn === oldRange.startColumn && resultRange.startRow === oldRange.startRow) {
return {
undos: [],
redos: [],
};
}
const res = this._handleRangeChange(unitId, subUnitId, note, row, col, resultRange, false);
return res;
}, unitId, subUnitId)
);
}
private _watch(unitId: string, subUnitId: string, note: ISheetNote, row: number, col: number) {
const oldRange: IRange = {
startColumn: col,
endColumn: col,
startRow: row,
endRow: row,
};
this._watcherMap.set(
this._getIdWithUnitId(unitId, subUnitId, row, col),
this._refRangeService.watchRange(unitId, subUnitId, oldRange, (before, after) => {
const { redos } = this._handleRangeChange(unitId, subUnitId, note, before.startRow, before.startColumn, after, true);
sequenceExecuteAsync(redos, this._commandService, { onlyLocal: true });
}, true)
);
}
private _unwatch(unitId: string, subUnitId: string, row: number, col: number) {
const id = this._getIdWithUnitId(unitId, subUnitId, row, col);
this._watcherMap.get(id)?.dispose();
this._watcherMap.delete(id);
}
private _unregister(unitId: string, subUnitId: string, row: number, col: number) {
const id = this._getIdWithUnitId(unitId, subUnitId, row, col);
this._disposableMap.get(id)?.dispose();
this._disposableMap.delete(id);
}
private _initData() {
const unitNotes = this._sheetsNoteModel.getNotes();
for (const [unitId, unitNote] of unitNotes) {
for (const [subUnitId, notes] of unitNote) {
notes.forEach((note) => {
this._register(unitId, subUnitId, note, note.row, note.col);
this._watch(unitId, subUnitId, note, note.row, note.col);
});
}
}
}
private _initRefRange() {
this.disposeWithMe(
this._sheetsNoteModel.change$.subscribe((option) => {
switch (option.type) {
case 'update': {
const { unitId, subUnitId, oldNote, newNote } = option;
const row = newNote ? newNote.row : oldNote!.row;
const col = newNote ? newNote.col : oldNote!.col;
const id = this._getIdWithUnitId(unitId, subUnitId, row, col);
if (newNote) {
if (!this._disposableMap.has(id)) {
this._register(unitId, subUnitId, newNote, row, col);
this._watch(unitId, subUnitId, newNote, row, col);
}
} else {
this._unregister(unitId, subUnitId, row, col);
this._unwatch(unitId, subUnitId, row, col);
}
break;
}
case 'ref': {
const { unitId, subUnitId, oldNote, newNote, silent } = option;
const { row: oldRow, col: oldCol } = oldNote!;
const { row: newRow, col: newCol } = newNote;
this._unregister(unitId, subUnitId, oldRow, oldCol);
if (!silent) {
this._unwatch(unitId, subUnitId, oldRow, oldCol);
this._watch(unitId, subUnitId, newNote, newRow, newCol);
}
this._register(unitId, subUnitId, newNote, newRow, newCol);
break;
}
}
})
);
}
}

View File

@@ -0,0 +1,212 @@
/**
* 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 { IMutationInfo, Workbook } from '@univerjs/core';
import type { ICopySheetCommandParams, IRemoveSheetCommandParams } from '@univerjs/sheets';
import type { ISheetNote } from '../models/sheets-note.model';
import { Disposable, generateRandomId, Inject, IResourceManagerService, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { CopySheetCommand, RemoveSheetCommand, SheetInterceptorService } from '@univerjs/sheets';
import { RemoveNoteMutation, UpdateNoteMutation } from '../commands/mutations/note.mutation';
import { PLUGIN_NAME } from '../const';
import { SheetsNoteModel } from '../models/sheets-note.model';
interface INoteData {
[sheetId: string]: {
[row: number]: {
[col: number]: ISheetNote;
};
};
}
export class SheetsNoteResourceController extends Disposable {
constructor(
@IResourceManagerService private readonly _resourceManagerService: IResourceManagerService,
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
@Inject(SheetInterceptorService) private _sheetInterceptorService: SheetInterceptorService,
@Inject(SheetsNoteModel) private readonly _sheetsNoteModel: SheetsNoteModel
) {
super();
this._initSnapshot();
this._initSheetChange();
}
private _initSnapshot() {
const toJson = (unitId: string) => {
const unitMap = this._sheetsNoteModel.getUnitNotes(unitId);
if (!unitMap) {
return '';
}
const result: INoteData = {};
unitMap.forEach((subUnitMap, subUnitId) => {
const sheetNotes: INoteData[string] = {};
subUnitMap.forEach((note) => {
const { row, col } = note;
if (!sheetNotes[row]) {
sheetNotes[row] = {};
}
sheetNotes[row][col] = note;
});
if (Object.keys(sheetNotes).length > 0) {
result[subUnitId] = sheetNotes;
}
});
return JSON.stringify(result);
};
const parseJson = (json: string): INoteData => {
if (!json) {
return {};
}
try {
return JSON.parse(json);
} catch {
return {};
}
};
this.disposeWithMe(
this._resourceManagerService.registerPluginResource<INoteData>({
pluginName: PLUGIN_NAME,
businesses: [UniverInstanceType.UNIVER_SHEET],
toJson: (unitId) => toJson(unitId),
parseJson: (json) => parseJson(json),
onUnLoad: (unitId) => {
// Clear all notes for this unit
this._sheetsNoteModel.deleteUnitNotes(unitId);
},
onLoad: (unitId, value) => {
// Load notes from the parsed data
Object.entries(value).forEach(([sheetId, sheetNotes]) => {
Object.entries(sheetNotes).forEach(([row, colNotes]) => {
Object.entries(colNotes).forEach(([col, note]) => {
this._sheetsNoteModel.updateNote(
unitId,
sheetId,
Number(row),
Number(col),
note
);
});
});
});
},
})
);
}
// eslint-disable-next-line max-lines-per-function
private _initSheetChange() {
this.disposeWithMe(
this._sheetInterceptorService.interceptCommand({
// eslint-disable-next-line max-lines-per-function
getMutations: (commandInfo) => {
if (commandInfo.id === RemoveSheetCommand.id) {
const params = commandInfo.params as IRemoveSheetCommandParams;
const unitId = params.unitId || this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getUnitId();
const subUnitId = params.subUnitId || this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet()?.getSheetId();
if (!unitId || !subUnitId) {
return { redos: [], undos: [] };
}
const sheetNotes = this._sheetsNoteModel.getSheetNotes(unitId, subUnitId);
if (!sheetNotes) {
return { redos: [], undos: [] };
}
const redos: IMutationInfo[] = [];
const undos: IMutationInfo[] = [];
sheetNotes.forEach((note) => {
redos.push({
id: RemoveNoteMutation.id,
params: {
unitId,
sheetId: subUnitId,
noteId: note.id,
row: note.row,
col: note.col,
},
});
undos.push({
id: UpdateNoteMutation.id,
params: {
unitId,
sheetId: subUnitId,
row: note.row,
col: note.col,
note,
},
});
});
return { redos, undos };
} else if (commandInfo.id === CopySheetCommand.id) {
const params = commandInfo.params as ICopySheetCommandParams & { targetSubUnitId: string };
const { unitId, subUnitId, targetSubUnitId } = params;
if (!unitId || !subUnitId || !targetSubUnitId) {
return { redos: [], undos: [] };
}
const sheetNotes = this._sheetsNoteModel.getSheetNotes(unitId, subUnitId);
if (!sheetNotes) {
return { redos: [], undos: [] };
}
const redos: IMutationInfo[] = [];
const undos: IMutationInfo[] = [];
sheetNotes.forEach((note) => {
const newNote = { ...note, id: generateRandomId(6) }; // Generate new ID for the copied note
redos.push({
id: UpdateNoteMutation.id,
params: {
unitId,
sheetId: targetSubUnitId,
row: newNote.row,
col: newNote.col,
note: newNote,
},
});
undos.push({
id: RemoveNoteMutation.id,
params: {
unitId,
sheetId: targetSubUnitId,
noteId: newNote.id,
row: newNote.row,
col: newNote.col,
},
});
});
return { redos, undos };
}
return { redos: [], undos: [] };
},
})
);
}
}

View File

@@ -0,0 +1,45 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Disposable, ICommandService } from '@univerjs/core';
import { SheetDeleteNoteCommand, SheetToggleNotePopupCommand, SheetUpdateNoteCommand } from '../commands/commands/note.command';
import { RemoveNoteMutation, ToggleNotePopupMutation, UpdateNoteMutation, UpdateNotePositionMutation } from '../commands/mutations/note.mutation';
export class SheetsNoteController extends Disposable {
constructor(
@ICommandService private readonly _commandService: ICommandService
) {
super();
this._initialize();
}
private _initialize(): void {
[
UpdateNotePositionMutation,
ToggleNotePopupMutation,
UpdateNoteMutation,
RemoveNoteMutation,
SheetDeleteNoteCommand,
SheetToggleNotePopupCommand,
SheetUpdateNoteCommand,
].forEach((command) => {
this.disposeWithMe(
this._commandService.registerCommand(command)
);
});
}
}

View File

@@ -0,0 +1,282 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Dependency, IWorkbookData, UnitModel } from '@univerjs/core';
import type { ISelectionWithStyle } from '@univerjs/sheets';
import {
ICommandService,
ILogService,
Inject,
Injector,
IUndoRedoService,
IUniverInstanceService,
LocaleType,
LogLevel,
Plugin,
RANGE_TYPE,
touchDependencies,
Univer,
UniverInstanceType,
} from '@univerjs/core';
import { FUniver } from '@univerjs/core/facade';
import {
DefinedNamesService,
FormulaDataModel,
FunctionService,
IDefinedNamesService,
IFunctionService,
LexerTreeBuilder,
} from '@univerjs/engine-formula';
import { IRenderManagerService, RenderManagerService } from '@univerjs/engine-render';
import { ISocketService, WebSocketService } from '@univerjs/network';
import {
RangeProtectionRuleModel,
RefRangeService,
SheetInterceptorService,
SheetLazyExecuteScheduleService,
SheetsSelectionsService,
WorkbookPermissionService,
WorksheetPermissionService,
WorksheetProtectionPointModel,
WorksheetProtectionRuleModel,
} from '@univerjs/sheets';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import {
SheetDeleteNoteCommand,
SheetToggleNotePopupCommand,
SheetUpdateNoteCommand,
} from '../../commands/commands/note.command';
import { SheetsNoteController } from '../../controllers/sheets.note.controller';
import { SheetsNoteModel } from '../../models/sheets-note.model';
import '@univerjs/sheets/facade';
import '../../facade';
const unitId = 'test';
const subUnitId = 'sheet1';
const TEST_WORKBOOK_DATA: IWorkbookData = {
id: unitId,
sheetOrder: [subUnitId],
name: '',
appVersion: '0.15.4',
locale: LocaleType.EN_US,
styles: {},
sheets: {
[subUnitId]: {
id: subUnitId,
name: 'Sheet1',
rowCount: 10,
columnCount: 10,
cellData: {},
},
},
};
function createFacadeTestBed(workbookData?: IWorkbookData, dependencies?: Dependency[]) {
const univer = new Univer();
const injector = univer.__getInjector();
class TestPlugin extends Plugin {
static override pluginName = 'test-plugin';
static override type = UniverInstanceType.UNIVER_SHEET;
constructor(
_config: undefined,
@Inject(Injector) override readonly _injector: Injector
) {
super();
}
override onStarting(): void {
// Ensure undo/redo exists.
this._injector.get(IUndoRedoService);
(dependencies ?? []).forEach((d) => this._injector.add(d));
// Minimal set of dependencies required by Sheets facade classes.
this._injector.add([SheetsSelectionsService]);
this._injector.add([SheetInterceptorService]);
this._injector.add([IFunctionService, { useClass: FunctionService }]);
this._injector.add([ISocketService, { useClass: WebSocketService }]);
this._injector.add([IRenderManagerService, { useClass: RenderManagerService }]);
this._injector.add([FormulaDataModel]);
this._injector.add([LexerTreeBuilder]);
this._injector.add([RefRangeService]);
this._injector.add([WorksheetPermissionService]);
this._injector.add([WorkbookPermissionService]);
this._injector.add([WorksheetProtectionPointModel]);
this._injector.add([RangeProtectionRuleModel]);
this._injector.add([WorksheetProtectionRuleModel]);
this._injector.add([IDefinedNamesService, { useClass: DefinedNamesService }]);
this._injector.add([SheetLazyExecuteScheduleService]);
([
[SheetsNoteModel],
[SheetsNoteController],
] as Dependency[]).forEach((d) => this._injector.add(d));
touchDependencies(this._injector, [
[SheetsSelectionsService],
[SheetInterceptorService],
[SheetsNoteModel],
[SheetsNoteController],
]);
}
}
univer.registerPlugin(TestPlugin);
const sheet = univer.createUnit<IWorkbookData, UnitModel<IWorkbookData>>(UniverInstanceType.UNIVER_SHEET, workbookData ?? TEST_WORKBOOK_DATA);
const univerInstanceService = injector.get(IUniverInstanceService);
univerInstanceService.focusUnit(unitId);
const logService = injector.get(ILogService);
logService.setLogLevel(LogLevel.SILENT);
const univerAPI = FUniver.newAPI(injector);
return {
univer,
injector,
sheet,
univerAPI,
};
}
function setSingleCellSelection(injector: Injector, row: number, col: number) {
const selection: ISelectionWithStyle = {
range: {
startRow: row,
endRow: row,
startColumn: col,
endColumn: col,
rangeType: RANGE_TYPE.NORMAL,
},
primary: {
startRow: row,
endRow: row,
startColumn: col,
endColumn: col,
actualRow: row,
actualColumn: col,
isMerged: false,
isMergedMainCell: false,
},
style: null,
};
injector.get(SheetsSelectionsService).setSelections([selection]);
}
describe('sheets-note facade mixins', () => {
let univer: Univer;
let injector: Injector;
let univerAPI: FUniver;
beforeEach(() => {
const testBed = createFacadeTestBed();
univer = testBed.univer;
injector = testBed.injector;
univerAPI = testBed.univerAPI;
});
afterEach(() => {
univer.dispose();
});
it('creates/reads/deletes note through FRange and reads all notes via FWorksheet', () => {
const workbook = univerAPI.getActiveWorkbook();
expect(workbook).toBeTruthy();
const sheet = workbook!.getActiveSheet();
const range = sheet.getRange(1, 1, 1, 1);
expect(range.getNote()).toBeUndefined();
range.createOrUpdateNote({ width: 160, height: 72, note: 'hello', show: true });
expect(range.getNote()).toMatchObject({
col: 1,
height: 72,
note: 'hello',
row: 1,
show: true,
width: 160,
});
expect(sheet.getNotes()).toHaveLength(1);
range.deleteNote();
expect(range.getNote()).toBeUndefined();
expect(sheet.getNotes()).toHaveLength(0);
});
it('emits business events while notes are added, updated, shown, hidden and deleted', () => {
const workbook = univerAPI.getActiveWorkbook()!;
const sheet = workbook.getActiveSheet();
const commandService = injector.get(ICommandService);
const events: string[] = [];
const disposables = [
univerAPI.addEvent(univerAPI.Event.BeforeSheetNoteAdd, (event) => events.push(`before-add:${event.row}:${event.col}:${event.note.note}`)),
univerAPI.addEvent(univerAPI.Event.SheetNoteAdd, (event) => events.push(`add:${event.row}:${event.col}:${event.note.note}`)),
univerAPI.addEvent(univerAPI.Event.BeforeSheetNoteUpdate, (event) => events.push(`before-update:${event.row}:${event.col}:${event.oldNote.note}->${event.note.note}`)),
univerAPI.addEvent(univerAPI.Event.SheetNoteUpdate, (event) => events.push(`update:${event.row}:${event.col}:${event.oldNote.note}->${event.note.note}`)),
univerAPI.addEvent(univerAPI.Event.BeforeSheetNoteShow, (event) => events.push(`before-show:${event.row}:${event.col}`)),
univerAPI.addEvent(univerAPI.Event.SheetNoteShow, (event) => events.push(`show:${event.row}:${event.col}`)),
univerAPI.addEvent(univerAPI.Event.BeforeSheetNoteHide, (event) => events.push(`before-hide:${event.row}:${event.col}`)),
univerAPI.addEvent(univerAPI.Event.SheetNoteHide, (event) => events.push(`hide:${event.row}:${event.col}`)),
univerAPI.addEvent(univerAPI.Event.BeforeSheetNoteDelete, (event) => events.push(`before-delete:${event.row}:${event.col}:${event.oldNote.note}`)),
univerAPI.addEvent(univerAPI.Event.SheetNoteDelete, (event) => events.push(`delete:${event.row}:${event.col}:${event.oldNote.note}`)),
];
expect(commandService.syncExecuteCommand(SheetUpdateNoteCommand.id, {
unitId,
sheetId: subUnitId,
row: 1,
col: 1,
note: { id: 'note-1', row: 1, col: 1, width: 160, height: 72, note: 'hello', show: false },
})).toBe(true);
expect(commandService.syncExecuteCommand(SheetUpdateNoteCommand.id, {
unitId,
sheetId: subUnitId,
row: 1,
col: 1,
note: { id: 'note-1', row: 1, col: 1, width: 160, height: 72, note: 'updated', show: false },
})).toBe(true);
setSingleCellSelection(injector, 1, 1);
expect(commandService.syncExecuteCommand(SheetToggleNotePopupCommand.id)).toBe(true);
expect(sheet.getRange(1, 1).getNote()?.show).toBe(true);
expect(commandService.syncExecuteCommand(SheetToggleNotePopupCommand.id)).toBe(true);
expect(sheet.getRange(1, 1).getNote()?.show).toBe(false);
expect(commandService.syncExecuteCommand(SheetDeleteNoteCommand.id)).toBe(true);
expect(sheet.getRange(1, 1).getNote()).toBeUndefined();
expect(events).toEqual(expect.arrayContaining([
'before-add:1:1:hello',
'add:1:1:hello',
'before-update:1:1:hello->updated',
'update:1:1:hello->updated',
'before-show:1:1',
'show:1:1',
'before-hide:1:1',
'hide:1:1',
'before-delete:1:1:updated',
'delete:1:1:updated',
]));
disposables.forEach((disposable) => disposable.dispose());
});
});

View File

@@ -0,0 +1,311 @@
/**
* 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 { ISheetNote } from '@univerjs/sheets-note';
import type { FWorkbook, FWorksheet } from '@univerjs/sheets/facade';
import { FEventName } from '@univerjs/core/facade';
export interface ISheetNoteAddEventParams {
workbook: FWorkbook;
worksheet: FWorksheet;
row: number;
col: number;
note: ISheetNote;
cancel?: boolean;
}
export interface ISheetNoteDeleteEventParams {
workbook: FWorkbook;
worksheet: FWorksheet;
row: number;
col: number;
oldNote: ISheetNote;
cancel?: boolean;
}
export interface ISheetNoteUpdateEventParams {
workbook: FWorkbook;
worksheet: FWorksheet;
row: number;
col: number;
note: ISheetNote;
oldNote: ISheetNote;
cancel?: boolean;
}
export interface ISheetNoteShowEventParams {
workbook: FWorkbook;
worksheet: FWorksheet;
row: number;
col: number;
cancel?: boolean;
}
export interface ISheetNoteHideEventParams {
workbook: FWorkbook;
worksheet: FWorksheet;
row: number;
col: number;
cancel?: boolean;
}
/**
* @ignore
*/
export interface ISheetsNoteEventParamConfig {
SheetNoteAdd: ISheetNoteAddEventParams;
SheetNoteDelete: ISheetNoteDeleteEventParams;
SheetNoteUpdate: ISheetNoteUpdateEventParams;
SheetNoteShow: ISheetNoteShowEventParams;
SheetNoteHide: ISheetNoteHideEventParams;
BeforeSheetNoteAdd: ISheetNoteAddEventParams;
BeforeSheetNoteDelete: ISheetNoteDeleteEventParams;
BeforeSheetNoteUpdate: ISheetNoteUpdateEventParams;
BeforeSheetNoteShow: ISheetNoteShowEventParams;
BeforeSheetNoteHide: ISheetNoteHideEventParams;
}
/**
* @ignore
*/
interface IFSheetsNoteEventNameMixin {
/**
* Event fired when a note is added
* @see {@link ISheetNoteAddEventParams}
* @example
* ```ts
* const disposable = univerAPI.addEvent(univerAPI.Event.SheetNoteAdd, (params) => {
* const { workbook, worksheet, row, col, note } = params;
* console.log(params);
* });
*
* // Remove the event listener, use `disposable.dispose()`
* ```
*/
readonly SheetNoteAdd: 'SheetNoteAdd';
/**
* Event fired when a note is deleted
* @see {@link ISheetNoteDeleteEventParams}
* @example
* ```ts
* const disposable = univerAPI.addEvent(univerAPI.Event.SheetNoteDelete, (params) => {
* const { workbook, worksheet, row, col, oldNote } = params;
* console.log(params);
* });
*
* // Remove the event listener, use `disposable.dispose()`
* ```
*/
readonly SheetNoteDelete: 'SheetNoteDelete';
/**
* Event fired when a note is updated
* @see {@link ISheetNoteUpdateEventParams}
* @example
* ```ts
* const disposable = univerAPI.addEvent(univerAPI.Event.SheetNoteUpdate, (params) => {
* const { workbook, worksheet, row, col, note, oldNote } = params;
* console.log(params);
* });
*
* // Remove the event listener, use `disposable.dispose()`
* ```
*/
readonly SheetNoteUpdate: 'SheetNoteUpdate';
/**
* Event fired when a note is shown
* @see {@link ISheetNoteShowEventParams}
* @example
* ```ts
* const disposable = univerAPI.addEvent(univerAPI.Event.SheetNoteShow, (params) => {
* const { workbook, worksheet, row, col } = params;
* console.log(params);
* });
*
* // Remove the event listener, use `disposable.dispose()`
* ```
*/
readonly SheetNoteShow: 'SheetNoteShow';
/**
* Event fired when a note is hidden
* @see {@link ISheetNoteHideEventParams}
* @example
* ```ts
* const disposable = univerAPI.addEvent(univerAPI.Event.SheetNoteHide, (params) => {
* const { workbook, worksheet, row, col } = params;
* console.log(params);
* });
*
* // Remove the event listener, use `disposable.dispose()`
* ```
*/
readonly SheetNoteHide: 'SheetNoteHide';
/**
* Event fired before a note is added
* @see {@link ISheetNoteAddEventParams}
* @example
* ```ts
* const disposable = univerAPI.addEvent(univerAPI.Event.BeforeSheetNoteAdd, (params) => {
* const { workbook, worksheet, row, col, note } = params;
* console.log(params);
*
* // Cancel the note addition operation
* params.cancel = true;
* });
*
* // Remove the event listener, use `disposable.dispose()`
* ```
*/
readonly BeforeSheetNoteAdd: 'BeforeSheetNoteAdd';
/**
* Event fired before a note is deleted
* @see {@link ISheetNoteDeleteEventParams}
* @example
* ```ts
* const disposable = univerAPI.addEvent(univerAPI.Event.BeforeSheetNoteDelete, (params) => {
* const { workbook, worksheet, row, col, oldNote } = params;
* console.log(params);
*
* // Cancel the note deletion operation
* params.cancel = true;
* });
*
* // Remove the event listener, use `disposable.dispose()`
* ```
*/
readonly BeforeSheetNoteDelete: 'BeforeSheetNoteDelete';
/**
* Event fired before a note is updated
* @see {@link ISheetNoteUpdateEventParams}
* @example
* ```ts
* const disposable = univerAPI.addEvent(univerAPI.Event.BeforeSheetNoteUpdate, (params) => {
* const { workbook, worksheet, row, col, note, oldNote } = params;
* console.log(params);
*
* // Cancel the note update operation
* params.cancel = true;
* });
*
* // Remove the event listener, use `disposable.dispose()`
* ```
*/
readonly BeforeSheetNoteUpdate: 'BeforeSheetNoteUpdate';
/**
* Event fired before a note is shown
* @see {@link ISheetNoteShowEventParams}
* @example
* ```ts
* const disposable = univerAPI.addEvent(univerAPI.Event.BeforeSheetNoteShow, (params) => {
* const { workbook, worksheet, row, col } = params;
* console.log(params);
*
* // Cancel the note show operation
* params.cancel = true;
* });
*
* // Remove the event listener, use `disposable.dispose()`
* ```
*/
readonly BeforeSheetNoteShow: 'BeforeSheetNoteShow';
/**
* Event fired before a note is hidden
* @see {@link ISheetNoteHideEventParams}
* @example
* ```ts
* const disposable = univerAPI.addEvent(univerAPI.Event.BeforeSheetNoteHide, (params) => {
* const { workbook, worksheet, row, col } = params;
* console.log(params);
*
* // Cancel the note hide operation
* params.cancel = true;
* });
*
* // Remove the event listener, use `disposable.dispose()`
* ```
*/
readonly BeforeSheetNoteHide: 'BeforeSheetNoteHide';
}
/**
* @ignore
*/
export class FSheetsNoteEventNameMixin extends FEventName implements IFSheetsNoteEventNameMixin {
override get SheetNoteAdd(): 'SheetNoteAdd' {
return 'SheetNoteAdd' as const;
}
override get SheetNoteDelete(): 'SheetNoteDelete' {
return 'SheetNoteDelete' as const;
}
override get SheetNoteUpdate(): 'SheetNoteUpdate' {
return 'SheetNoteUpdate' as const;
}
override get SheetNoteShow(): 'SheetNoteShow' {
return 'SheetNoteShow' as const;
}
override get SheetNoteHide(): 'SheetNoteHide' {
return 'SheetNoteHide' as const;
}
override get BeforeSheetNoteAdd(): 'BeforeSheetNoteAdd' {
return 'BeforeSheetNoteAdd' as const;
}
override get BeforeSheetNoteDelete(): 'BeforeSheetNoteDelete' {
return 'BeforeSheetNoteDelete' as const;
}
override get BeforeSheetNoteUpdate(): 'BeforeSheetNoteUpdate' {
return 'BeforeSheetNoteUpdate' as const;
}
override get BeforeSheetNoteShow(): 'BeforeSheetNoteShow' {
return 'BeforeSheetNoteShow' as const;
}
override get BeforeSheetNoteHide(): 'BeforeSheetNoteHide' {
return 'BeforeSheetNoteHide' as const;
}
}
/**
* @ignore
*/
export interface ISheetsNoteEventParamConfig {
SheetNoteAdd: ISheetNoteAddEventParams;
SheetNoteDelete: ISheetNoteDeleteEventParams;
SheetNoteUpdate: ISheetNoteUpdateEventParams;
SheetNoteShow: ISheetNoteShowEventParams;
SheetNoteHide: ISheetNoteHideEventParams;
BeforeSheetNoteAdd: ISheetNoteAddEventParams;
BeforeSheetNoteDelete: ISheetNoteDeleteEventParams;
BeforeSheetNoteUpdate: ISheetNoteUpdateEventParams;
BeforeSheetNoteShow: ISheetNoteShowEventParams;
BeforeSheetNoteHide: ISheetNoteHideEventParams;
}
FEventName.extend(FSheetsNoteEventNameMixin);
declare module '@univerjs/core/facade' {
// eslint-disable-next-line ts/naming-convention
interface FEventName extends IFSheetsNoteEventNameMixin {}
interface IEventParamConfig extends ISheetsNoteEventParamConfig {}
}

View File

@@ -0,0 +1,128 @@
/**
* 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 { Nullable } from '@univerjs/core';
import type { ISheetNote } from '@univerjs/sheets-note';
import { RemoveNoteMutation, SheetsNoteModel, UpdateNoteMutation } from '@univerjs/sheets-note';
import { FRange } from '@univerjs/sheets/facade';
export interface ICreateOrUpdateNoteOptions {
id?: string;
width: number;
height: number;
note: string;
show?: boolean;
}
/**
* @ignore
*/
export interface IFRangeSheetsNoteMixin {
/**
* Get the annotation of the top-left cell in the range
* @returns {Nullable<ISheetNote>} The annotation of the top-left cell in the range
* @example
* ```ts
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
* const fRange = fWorksheet.getRange('A1:D10');
* const note = fRange.getNote();
* console.log(note);
* ```
*/
getNote(): Nullable<ISheetNote>;
/**
* Create or update the annotation of the top-left cell in the range
* @param {ICreateOrUpdateNoteOptions} note The annotation to create or update
* @returns {FRange} This range for method chaining
* @example
* ```ts
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
* const fRange = fWorksheet.getRange('A1');
* fRange.createOrUpdateNote({
* note: 'This is a note',
* width: 160,
* height: 100,
* show: true,
* });
* ```
*/
createOrUpdateNote(note: ICreateOrUpdateNoteOptions): FRange;
/**
* Delete the annotation of the top-left cell in the range
* @returns {FRange} This range for method chaining
* @example
* ```ts
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
* const notes = fWorksheet.getNotes();
* console.log(notes);
*
* if (notes.length > 0) {
* // Delete the first note
* const { row, col } = notes[0];
* fWorksheet.getRange(row, col).deleteNote();
* }
* ```
*/
deleteNote(): FRange;
}
export class FRangeSheetsNoteMixin extends FRange implements IFRangeSheetsNoteMixin {
override createOrUpdateNote(note: ICreateOrUpdateNoteOptions): FRange {
this._commandService.syncExecuteCommand(
UpdateNoteMutation.id,
{
unitId: this.getUnitId(),
sheetId: this.getSheetId(),
row: this.getRow(),
col: this.getColumn(),
note,
}
);
return this;
}
override deleteNote(): FRange {
this._commandService.syncExecuteCommand(
RemoveNoteMutation.id,
{
unitId: this.getUnitId(),
sheetId: this.getSheetId(),
row: this.getRow(),
col: this.getColumn(),
}
);
return this;
}
override getNote(): Nullable<ISheetNote> {
const model = this._injector.get(SheetsNoteModel);
return model.getNote(this.getUnitId(), this.getSheetId(), { row: this.getRow(), col: this.getColumn() });
}
}
FRange.extend(FRangeSheetsNoteMixin);
declare module '@univerjs/sheets/facade' {
// eslint-disable-next-line ts/naming-convention
interface FRange extends IFRangeSheetsNoteMixin { }
}

View File

@@ -0,0 +1,331 @@
/**
* 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 { Injector } from '@univerjs/core';
import type { IUpdateNoteMutationParams } from '@univerjs/sheets-note';
import type { ISheetNoteAddEventParams, ISheetNoteDeleteEventParams, ISheetNoteHideEventParams, ISheetNoteShowEventParams, ISheetNoteUpdateEventParams } from './f-event';
import { CanceledError, ICommandService } from '@univerjs/core';
import { FUniver } from '@univerjs/core/facade';
import { SheetsSelectionsService } from '@univerjs/sheets';
import { SheetDeleteNoteCommand, SheetsNoteModel, SheetToggleNotePopupCommand, SheetUpdateNoteCommand } from '@univerjs/sheets-note';
export class FUniverSheetsNoteMixin extends FUniver {
// eslint-disable-next-line max-lines-per-function
override _initialize(injector: Injector): void {
const commandService = injector.get(ICommandService);
this.disposeWithMe(
this.registerEventHandler(
this.Event.SheetNoteAdd,
() => {
const model = injector.get(SheetsNoteModel);
return model.change$.subscribe((change) => {
if (change.type === 'update' && !change.oldNote && change.newNote) {
const { unitId, subUnitId, newNote } = change;
const target = this.getSheetCommandTarget({ unitId, subUnitId });
if (!target) return;
const { workbook, worksheet } = target;
const eventParams: ISheetNoteAddEventParams = {
workbook,
worksheet,
row: newNote.row,
col: newNote.col,
note: newNote,
};
this.fireEvent(this.Event.SheetNoteAdd, eventParams);
}
});
}
)
);
this.disposeWithMe(
this.registerEventHandler(
this.Event.SheetNoteDelete,
() => {
const model = injector.get(SheetsNoteModel);
return model.change$.subscribe((change) => {
if (change.type === 'update' && change.oldNote && !change.newNote) {
const { unitId, subUnitId, oldNote } = change;
const target = this.getSheetCommandTarget({ unitId, subUnitId });
if (!target) return;
const { workbook, worksheet } = target;
const eventParams: ISheetNoteDeleteEventParams = {
workbook,
worksheet,
row: oldNote.row,
col: oldNote.col,
oldNote,
};
this.fireEvent(this.Event.SheetNoteDelete, eventParams);
}
});
}
)
);
this.disposeWithMe(
this.registerEventHandler(
this.Event.SheetNoteUpdate,
() => {
const model = injector.get(SheetsNoteModel);
return model.change$.subscribe((change) => {
if (change.type === 'update' && change.oldNote && change.newNote) {
const { unitId, subUnitId, oldNote, newNote } = change;
const target = this.getSheetCommandTarget({ unitId, subUnitId });
if (!target) return;
const { workbook, worksheet } = target;
const eventParams: ISheetNoteUpdateEventParams = {
workbook,
worksheet,
row: newNote.row,
col: newNote.col,
note: newNote,
oldNote,
};
this.fireEvent(this.Event.SheetNoteUpdate, eventParams);
}
});
}
)
);
this.disposeWithMe(
this.registerEventHandler(
this.Event.SheetNoteShow,
() => {
const model = injector.get(SheetsNoteModel);
return model.change$.subscribe((change) => {
if (change.type === 'update' && change.oldNote && change.newNote && !change.oldNote.show && change.newNote.show) {
const { unitId, subUnitId, newNote } = change;
const target = this.getSheetCommandTarget({ unitId, subUnitId });
if (!target) return;
const { workbook, worksheet } = target;
const eventParams: ISheetNoteShowEventParams = {
workbook,
worksheet,
row: newNote.row,
col: newNote.col,
};
this.fireEvent(this.Event.SheetNoteShow, eventParams);
}
});
}
)
);
this.disposeWithMe(
this.registerEventHandler(
this.Event.SheetNoteHide,
() => {
const model = injector.get(SheetsNoteModel);
return model.change$.subscribe((change) => {
if (change.type === 'update' && change.oldNote && change.newNote && change.oldNote.show && !change.newNote.show) {
const { unitId, subUnitId, newNote } = change;
const target = this.getSheetCommandTarget({ unitId, subUnitId });
if (!target) return;
const { workbook, worksheet } = target;
const eventParams: ISheetNoteHideEventParams = {
workbook,
worksheet,
row: newNote.row,
col: newNote.col,
};
this.fireEvent(this.Event.SheetNoteHide, eventParams);
}
});
}
)
);
this.disposeWithMe(
this.registerEventHandler(
this.Event.BeforeSheetNoteAdd,
() => commandService.beforeCommandExecuted((command) => {
if (command.id === SheetUpdateNoteCommand.id) {
const params = command.params as IUpdateNoteMutationParams;
const target = this.getSheetCommandTarget(params);
if (!target) return;
const { workbook, worksheet, unitId, subUnitId } = target;
const { row, col, note } = params;
const oldNote = injector.get(SheetsNoteModel).getNote(unitId, subUnitId, { noteId: note.id, row, col });
if (oldNote) return;
const eventParams: ISheetNoteAddEventParams = {
workbook,
worksheet,
row,
col,
note,
};
const cancel = this.fireEvent(this.Event.BeforeSheetNoteAdd, eventParams);
if (cancel) {
throw new CanceledError();
}
}
})
)
);
this.disposeWithMe(
this.registerEventHandler(
this.Event.BeforeSheetNoteDelete,
() => commandService.beforeCommandExecuted((command) => {
if (command.id === SheetDeleteNoteCommand.id) {
const target = this.getSheetCommandTarget();
if (!target) return;
const selection = injector.get(SheetsSelectionsService).getCurrentLastSelection();
if (!selection?.primary) return;
const { workbook, worksheet, unitId, subUnitId } = target;
const { actualRow: row, actualColumn: col } = selection.primary;
const oldNote = injector.get(SheetsNoteModel).getNote(unitId, subUnitId, { row, col });
if (!oldNote) return;
const eventParams: ISheetNoteDeleteEventParams = {
workbook,
worksheet,
row,
col,
oldNote,
};
const cancel = this.fireEvent(this.Event.BeforeSheetNoteDelete, eventParams);
if (cancel) {
throw new CanceledError();
}
}
})
)
);
this.disposeWithMe(
this.registerEventHandler(
this.Event.BeforeSheetNoteUpdate,
() => commandService.beforeCommandExecuted((command) => {
if (command.id === SheetUpdateNoteCommand.id) {
const params = command.params as IUpdateNoteMutationParams;
const target = this.getSheetCommandTarget(params);
if (!target) return;
const { workbook, worksheet, unitId, subUnitId } = target;
const { row, col, note } = params;
const oldNote = injector.get(SheetsNoteModel).getNote(unitId, subUnitId, { row, col });
if (!oldNote) return;
const eventParams: ISheetNoteUpdateEventParams = {
workbook,
worksheet,
row,
col,
note,
oldNote,
};
const cancel = this.fireEvent(this.Event.BeforeSheetNoteUpdate, eventParams);
if (cancel) {
throw new CanceledError();
}
}
})
)
);
this.disposeWithMe(
this.registerEventHandler(
this.Event.BeforeSheetNoteShow,
() => commandService.beforeCommandExecuted((command) => {
if (command.id === SheetToggleNotePopupCommand.id) {
const target = this.getSheetCommandTarget();
if (!target) return;
const selection = injector.get(SheetsSelectionsService).getCurrentLastSelection();
if (!selection?.primary) return;
const { workbook, worksheet, unitId, subUnitId } = target;
const { actualRow: row, actualColumn: col } = selection.primary;
const note = injector.get(SheetsNoteModel).getNote(unitId, subUnitId, { row, col });
if (!note || note.show) return;
const eventParams: ISheetNoteShowEventParams = {
workbook,
worksheet,
row,
col,
};
const cancel = this.fireEvent(this.Event.BeforeSheetNoteShow, eventParams);
if (cancel) {
throw new CanceledError();
}
}
})
)
);
this.disposeWithMe(
this.registerEventHandler(
this.Event.BeforeSheetNoteHide,
() => commandService.beforeCommandExecuted((command) => {
if (command.id === SheetToggleNotePopupCommand.id) {
const target = this.getSheetCommandTarget();
if (!target) return;
const selection = injector.get(SheetsSelectionsService).getCurrentLastSelection();
if (!selection?.primary) return;
const { workbook, worksheet, unitId, subUnitId } = target;
const { actualRow: row, actualColumn: col } = selection.primary;
const note = injector.get(SheetsNoteModel).getNote(unitId, subUnitId, { row, col });
if (!note || !note.show) return;
const eventParams: ISheetNoteHideEventParams = {
workbook,
worksheet,
row,
col,
};
const cancel = this.fireEvent(this.Event.BeforeSheetNoteHide, eventParams);
if (cancel) {
throw new CanceledError();
}
}
})
)
);
}
}
FUniver.extend(FUniverSheetsNoteMixin);

View File

@@ -0,0 +1,60 @@
/**
* 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 { ISheetNote } from '@univerjs/sheets-note';
import { SheetsNoteModel } from '@univerjs/sheets-note';
import { FWorksheet } from '@univerjs/sheets/facade';
/**
* @ignore
*/
export interface IFWorksheetNoteMixin {
/**
* Get all annotations in the worksheet
* @returns {ISheetNote[]} An array of all annotations in the worksheet
* @example
* ```ts
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
* const notes = fWorksheet.getNotes();
* console.log(notes);
*
* notes.forEach((item) => {
* const { row, col, note } = item;
* console.log(`Cell ${fWorksheet.getRange(row, col).getA1Notation()} has a note: ${note}`);
* });
* ```
*/
getNotes(): ISheetNote[];
}
export class FWorksheetNoteMixin extends FWorksheet implements IFWorksheetNoteMixin {
override getNotes(): ISheetNote[] {
const model = this._injector.get(SheetsNoteModel);
const notes = model.getSheetNotes(this.getWorkbook().getUnitId(), this.getSheetId());
if (!notes) {
return [];
}
return Array.from(notes.values()).map((note) => ({ ...note }));
}
}
FWorksheet.extend(FWorksheetNoteMixin);
declare module '@univerjs/sheets/facade' {
// eslint-disable-next-line ts/naming-convention
interface FWorksheet extends IFWorksheetNoteMixin { }
}

View File

@@ -0,0 +1,25 @@
/**
* 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 './f-event';
import './f-range';
import './f-univer';
import './f-worksheet';
export * from './f-event';
export * from './f-range';
export * from './f-univer';
export * from './f-worksheet';

View File

@@ -0,0 +1,22 @@
/**
* 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.
*/
export * from './commands/commands/note.command';
export * from './commands/mutations/note.mutation';
export type { IUniverSheetsNoteConfig } from './config/config';
export * from './controllers/sheets-note-resource.controller';
export * from './models/sheets-note.model';
export { UniverSheetsNotePlugin } from './plugin';

View File

@@ -0,0 +1,160 @@
/**
* 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 { ISheetNote } from '../sheets-note.model';
import { Injector } from '@univerjs/core';
import { filter, firstValueFrom, take, toArray } from 'rxjs';
import { beforeEach, describe, expect, it } from 'vitest';
import { SheetsNoteModel } from '../sheets-note.model';
describe('SheetsNoteModel', () => {
let injector: Injector;
let model: SheetsNoteModel;
const unitId = 'unit-1';
const subUnitId = 'sheet-1';
beforeEach(() => {
injector = new Injector([
[SheetsNoteModel],
]);
model = injector.get(SheetsNoteModel);
});
it('updates a note and emits change events', async () => {
const noteInput: ISheetNote = {
id: 'note-1',
row: 0,
col: 0,
width: 160,
height: 72,
note: 'hello',
show: false,
};
const firstChangePromise = firstValueFrom(model.change$.pipe(take(1)));
model.updateNote(unitId, subUnitId, 0, 0, noteInput);
const firstChange = await firstChangePromise;
expect(firstChange.type).toBe('update');
expect(firstChange.oldNote == null).toBe(true);
expect(firstChange.newNote?.note).toBe('hello');
const secondChangePromise = firstValueFrom(model.change$.pipe(take(1)));
model.updateNote(unitId, subUnitId, 0, 0, { ...noteInput, note: 'updated' });
const secondChange = await secondChangePromise;
expect(secondChange.type).toBe('update');
expect(secondChange.oldNote?.note).toBe('hello');
expect(secondChange.newNote?.note).toBe('updated');
const stored = model.getNote(unitId, subUnitId, { row: 0, col: 0 });
expect(stored?.note).toBe('updated');
});
it('toggles note popup state and exposes show notes stream', async () => {
const noteInput: ISheetNote = {
id: 'note-2',
row: 2,
col: 3,
width: 160,
height: 72,
note: 'note',
show: false,
};
model.updateNote(unitId, subUnitId, 2, 3, noteInput);
const showNotesPromise = firstValueFrom(
model.getSheetShowNotes$(unitId, subUnitId).pipe(
take(2),
toArray()
)
);
model.toggleNotePopup(unitId, subUnitId, { row: 2, col: 3 });
model.toggleNotePopup(unitId, subUnitId, { row: 2, col: 3 });
const [afterShow, afterHide] = await showNotesPromise;
expect(afterShow).toHaveLength(1);
expect(afterShow[0].note.show).toBe(true);
expect(afterShow[0].loc.row).toBe(2);
expect(afterShow[0].loc.col).toBe(3);
expect(afterHide).toHaveLength(0);
});
it('updates note position and emits ref change', async () => {
const noteInput: ISheetNote = {
id: 'note-3',
row: 1,
col: 1,
width: 160,
height: 72,
note: 'note',
};
model.updateNote(unitId, subUnitId, 1, 1, noteInput);
const refChangePromise = firstValueFrom(
model.change$.pipe(
filter((c) => c.type === 'ref'),
take(1)
)
);
model.updateNotePosition(unitId, subUnitId, { row: 1, col: 1, newRow: 4, newCol: 5 });
const refChange = await refChangePromise;
expect(refChange.oldNote?.row).toBe(1);
expect(refChange.oldNote?.col).toBe(1);
expect(refChange.newNote.row).toBe(4);
expect(refChange.newNote.col).toBe(5);
expect(model.getNote(unitId, subUnitId, { row: 1, col: 1 })).toBeUndefined();
expect(model.getNote(unitId, subUnitId, { row: 4, col: 5 })?.id).toBe('note-3');
});
it('removes notes and does nothing if note is missing', async () => {
const noteInput: ISheetNote = {
id: 'note-4',
row: 6,
col: 7,
width: 160,
height: 72,
note: 'note',
};
model.updateNote(unitId, subUnitId, 6, 7, noteInput);
const removeChangePromise = firstValueFrom(model.change$.pipe(take(1)));
model.removeNote(unitId, subUnitId, { row: 6, col: 7 });
const removeChange = await removeChangePromise;
expect(removeChange.type).toBe('update');
expect(removeChange.oldNote?.id).toBe('note-4');
expect(removeChange.newNote).toBeNull();
expect(model.getNote(unitId, subUnitId, { row: 6, col: 7 })).toBeUndefined();
let emitted = false;
const subscription = model.change$.subscribe(() => {
emitted = true;
});
model.removeNote(unitId, subUnitId, { row: 99, col: 99 });
expect(emitted).toBe(false);
subscription.unsubscribe();
});
});

View File

@@ -0,0 +1,206 @@
/**
* 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 { Nullable } from '@univerjs/core';
import type { ISheetLocationBase } from '@univerjs/sheets';
import { Disposable, generateRandomId } from '@univerjs/core';
import { filter, map, Subject } from 'rxjs';
export interface ISheetNote {
id: string;
row: number;
col: number;
width: number;
height: number;
note: string;
show?: boolean;
}
export type ISheetNoteChange = {
unitId: string;
subUnitId: string;
oldNote: Nullable<ISheetNote>;
silent?: boolean;
} & ({
type: 'update';
newNote: Nullable<ISheetNote>;
} | {
type: 'ref';
newNote: ISheetNote;
});
export class SheetsNoteModel extends Disposable {
private _notesMap: Map<string, Map<string, Map<string, ISheetNote>>> = new Map();
private readonly _change$ = new Subject<ISheetNoteChange>();
readonly change$ = this._change$.asObservable();
private _ensureNotesMap(unitId: string, subUnitId: string): Map<string, ISheetNote> {
let unitMap = this._notesMap.get(unitId);
if (!unitMap) {
unitMap = new Map();
this._notesMap.set(unitId, unitMap);
}
let subUnitMap = unitMap.get(subUnitId);
if (!subUnitMap) {
subUnitMap = new Map();
unitMap.set(subUnitId, subUnitMap);
}
return subUnitMap;
}
private _getNoteByPosition(unitId: string, subUnitId: string, row: number, col: number): Nullable<ISheetNote> {
const subUnitMap = this._ensureNotesMap(unitId, subUnitId);
for (const [_, note] of subUnitMap) {
if (note.row === row && note.col === col) {
return note;
}
}
}
private _getNoteById(unitId: string, subUnitId: string, id: string): Nullable<ISheetNote> {
const subUnitMap = this._ensureNotesMap(unitId, subUnitId);
return subUnitMap.get(id);
}
private _getNoteByParams(unitId: string, subUnitId: string, params: { noteId?: string; row?: number; col?: number }): Nullable<ISheetNote> {
const { noteId, row, col } = params;
if (noteId) {
return this._getNoteById(unitId, subUnitId, noteId);
}
if (row !== undefined && col !== undefined) {
return this._getNoteByPosition(unitId, subUnitId, row, col);
}
return null;
}
getSheetShowNotes$(unitId: string, subUnitId: string) {
return this._change$.pipe(
filter(({ unitId: u, subUnitId: s }) => u === unitId && s === subUnitId),
map(() => {
const subUnitMap = this._ensureNotesMap(unitId, subUnitId);
const notes: { loc: ISheetLocationBase; note: ISheetNote }[] = [];
for (const [_, note] of subUnitMap) {
if (note.show) {
notes.push({ loc: { row: note.row, col: note.col, unitId, subUnitId }, note });
}
}
return notes;
})
);
}
getCellNoteChange$(unitId: string, subUnitId: string, row: number, col: number) {
return this._change$.pipe(
filter(({ unitId: u, subUnitId: s, oldNote }) => {
if (u !== unitId || s !== subUnitId || !oldNote) {
return false;
}
return oldNote.row === row && oldNote.col === col;
}),
map((newNote) => newNote)
);
}
updateNote(unitId: string, subUnitId: string, row: number, col: number, note: Partial<ISheetNote>, silent?: boolean): void {
const oldNote = this._getNoteByParams(unitId, subUnitId, { noteId: note?.id, row, col });
const subUnitMap = this._ensureNotesMap(unitId, subUnitId);
const newNote = {
...note,
id: oldNote?.id || note.id || generateRandomId(6),
row,
col,
} as ISheetNote;
subUnitMap.set(newNote.id, newNote);
this._change$.next({ unitId, subUnitId, oldNote, type: 'update', newNote, silent });
}
removeNote(unitId: string, subUnitId: string, params: { noteId?: string; row?: number; col?: number; silent?: boolean }): void {
const { noteId, row, col, silent } = params;
const oldNote = this._getNoteByParams(unitId, subUnitId, { noteId, row, col });
if (!oldNote) {
return;
}
const subUnitMap = this._ensureNotesMap(unitId, subUnitId);
subUnitMap.delete(oldNote.id);
this._change$.next({ unitId, subUnitId, oldNote, type: 'update', newNote: null, silent });
}
toggleNotePopup(unitId: string, subUnitId: string, params: { noteId?: string; row?: number; col?: number; silent?: boolean }): void {
const { noteId, row, col, silent } = params;
const oldNote = this._getNoteByParams(unitId, subUnitId, { noteId, row, col });
if (!oldNote) {
return;
}
const subUnitMap = this._ensureNotesMap(unitId, subUnitId);
const newNote = { ...oldNote, show: !oldNote.show };
subUnitMap.set(newNote.id, newNote);
this._change$.next({ unitId, subUnitId, oldNote, type: 'update', newNote, silent });
}
updateNotePosition(unitId: string, subUnitId: string, params: { noteId?: string; row?: number; col?: number; newRow: number; newCol: number; silent?: boolean }): void {
const { noteId, row, col, newRow, newCol, silent } = params;
const oldNote = this._getNoteByParams(unitId, subUnitId, { noteId, row, col });
if (!oldNote) {
return;
}
const subUnitMap = this._ensureNotesMap(unitId, subUnitId);
const newNote = { ...oldNote, row: newRow, col: newCol };
subUnitMap.set(newNote.id, newNote);
this._change$.next({ unitId, subUnitId, oldNote, type: 'ref', newNote, silent });
}
getNote(unitId: string, subUnitId: string, params: { noteId?: string; row?: number; col?: number }): Nullable<ISheetNote> {
return this._getNoteByParams(unitId, subUnitId, params);
}
getNotes() {
return this._notesMap;
}
getUnitNotes(unitId: string): Map<string, Map<string, ISheetNote>> | undefined {
return this._notesMap.get(unitId);
}
getSheetNotes(unitId: string, subUnitId: string): Map<string, ISheetNote> | undefined {
const unitMap = this._notesMap.get(unitId);
if (!unitMap) {
return undefined;
}
return unitMap.get(subUnitId);
}
deleteUnitNotes(unitId: string): void {
this._notesMap.delete(unitId);
}
}

View File

@@ -0,0 +1,74 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Dependency } from '@univerjs/core';
import type { IUniverSheetsNoteConfig } from './config/config';
import { DependentOn, IConfigService, Inject, Injector, merge, Plugin, touchDependencies, UniverInstanceType } from '@univerjs/core';
import { UniverSheetsPlugin } from '@univerjs/sheets';
import pkg from '../package.json';
import { defaultPluginConfig, SHEETS_NOTE_PLUGIN_CONFIG_KEY } from './config/config';
import { PLUGIN_NAME } from './const';
import { SheetsNoteRefRangeController } from './controllers/sheets-note-ref-range.controller';
import { SheetsNoteResourceController } from './controllers/sheets-note-resource.controller';
import { SheetsNoteController } from './controllers/sheets.note.controller';
import { SheetsNoteModel } from './models/sheets-note.model';
@DependentOn(UniverSheetsPlugin)
export class UniverSheetsNotePlugin extends Plugin {
static override pluginName = PLUGIN_NAME;
static override packageName = pkg.name;
static override version = pkg.version;
static override type = UniverInstanceType.UNIVER_SHEET;
constructor(
private readonly _config: IUniverSheetsNoteConfig = defaultPluginConfig,
@IConfigService private readonly _configService: IConfigService,
@Inject(Injector) protected override readonly _injector: Injector
) {
super();
// Manage the plugin configuration.
const { ...rest } = merge(
{},
defaultPluginConfig,
this._config
);
this._configService.setConfig(SHEETS_NOTE_PLUGIN_CONFIG_KEY, rest);
}
override onStarting(): void {
([
[SheetsNoteModel],
[SheetsNoteController],
[SheetsNoteResourceController],
[SheetsNoteRefRangeController],
] as Dependency[]).forEach((dependency) => {
this._injector.add(dependency);
});
touchDependencies(this._injector, [
[SheetsNoteModel],
[SheetsNoteController],
[SheetsNoteResourceController],
]);
}
override onReady(): void {
touchDependencies(this._injector, [
[SheetsNoteRefRangeController],
]);
}
}