fix: fix sheets-table render (#7263)

This commit is contained in:
wpxp123456
2026-07-14 20:51:45 +08:00
committed by GitHub
parent 2564f19a79
commit dc36b204a1
2 changed files with 158 additions and 4 deletions
@@ -0,0 +1,129 @@
/**
* 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 { CommandType } from '@univerjs/core';
import { SelectRangeCommand } from '@univerjs/sheets';
import { MoveSelectionCommand, SelectAllCommand } from '@univerjs/sheets-ui';
import { Subject } from 'rxjs';
import { describe, expect, it, vi } from 'vitest';
import { SheetTableControlsRenderController } from '../sheet-table-controls-render.controller';
function createController(hasSkeleton = true) {
const activeSheet$ = new Subject();
const currentSkeleton$ = new Subject();
const tableAdd$ = new Subject();
const selectionChanged$ = new Subject();
const sceneMakeDirty = vi.fn();
const skeleton = hasSkeleton ? { gapConfig: { rowGaps: {} } } : null;
let commandListener: ((command: { id: string; type: CommandType }) => void) | undefined;
const controller = new SheetTableControlsRenderController(
{
unit: {
activeSheet$,
getActiveSheet: () => ({ getSheetId: () => 'sheet-1' }),
getUnitId: () => 'unit-1',
},
scene: {
addObjects: vi.fn(),
removeObjects: vi.fn(),
makeDirty: sceneMakeDirty,
setCursor: vi.fn(),
resetCursor: vi.fn(),
},
components: { get: vi.fn() },
} as never,
{} as never,
{
currentSkeleton$,
getCurrentSkeleton: () => skeleton,
} as never,
{
onCommandExecuted: (listener: (command: { id: string; type: CommandType }) => void) => {
commandListener = listener;
return { dispose: vi.fn() };
},
} as never,
{
tableAdd$,
tableDelete$: new Subject(),
tableNameChanged$: new Subject(),
tableRangeChanged$: new Subject(),
tableThemeChanged$: new Subject(),
getTablesBySubunitId: () => [],
} as never,
{ getRangeThemeStyle: vi.fn() } as never,
{ unitPermissionInitStateChange$: new Subject() } as never,
{
permissionPointUpdate$: new Subject(),
getPermissionPoint: () => ({ value: true }),
} as never,
{
selectionChanged$,
getCurrentSelections: () => [],
} as never,
{ resetSelectionsByModelData: vi.fn() } as never,
{ refreshTable$: new Subject() } as never,
{ t: (key: string) => key } as never,
{} as never,
{} as never
);
sceneMakeDirty.mockClear();
return {
controller,
sceneMakeDirty,
selectionChanged$,
tableAdd$,
executeCommand: (command: { id: string; type: CommandType }) => commandListener?.(command),
};
}
describe('SheetTableControlsRenderController', () => {
it('keeps sheet caches for selection refreshes and invalidates them for table refreshes', () => {
const { controller, sceneMakeDirty, selectionChanged$, tableAdd$, executeCommand } = createController();
selectionChanged$.next({});
executeCommand({ id: MoveSelectionCommand.id, type: CommandType.COMMAND });
executeCommand({ id: SelectAllCommand.id, type: CommandType.COMMAND });
executeCommand({ id: SelectRangeCommand.id, type: CommandType.COMMAND });
executeCommand({ id: 'doc.mutation.rich-text-editing', type: CommandType.MUTATION });
expect(sceneMakeDirty).not.toHaveBeenCalled();
tableAdd$.next({});
expect(sceneMakeDirty).toHaveBeenCalledOnce();
controller.dispose();
});
it('clears unavailable controls without invalidating sheet caches on selection changes', () => {
const { controller, sceneMakeDirty, selectionChanged$ } = createController(false);
const shape = Reflect.get(controller, '_shape') as {
setItems: (items: unknown[]) => void;
refreshBounds: () => void;
};
const setItems = vi.spyOn(shape, 'setItems');
const refreshBounds = vi.spyOn(shape, 'refreshBounds');
selectionChanged$.next({});
expect(setItems).toHaveBeenCalledWith([]);
expect(refreshBounds).toHaveBeenCalledOnce();
expect(sceneMakeDirty).not.toHaveBeenCalled();
controller.dispose();
});
});
@@ -31,6 +31,7 @@ import {
} from '@univerjs/core';
import { CURSOR_TYPE } from '@univerjs/engine-render';
import {
SelectRangeCommand,
SheetRangeThemeModel,
SheetsSelectionsService,
WorkbookEditablePermission,
@@ -44,8 +45,12 @@ import {
TableManager,
} from '@univerjs/sheets-table';
import {
ExpandSelectionCommand,
getTransformCoord,
ISheetSelectionRenderService,
MoveSelectionCommand,
MoveSelectionEnterAndTabCommand,
SelectAllCommand,
SetScrollOperation,
SetZoomRatioOperation,
SHEET_VIEW_KEY,
@@ -71,6 +76,13 @@ const TABLE_RENDER_REFRESH_COMMANDS = new Set([
SetScrollOperation.id,
SetZoomRatioOperation.id,
]);
const SELECTION_ONLY_COMMANDS = new Set([
MoveSelectionCommand.id,
MoveSelectionEnterAndTabCommand.id,
ExpandSelectionCommand.id,
SelectAllCommand.id,
SelectRangeCommand.id,
]);
type TopGapSnapshot = { size: number; color?: string; stripeColor?: string } | null;
@@ -138,6 +150,10 @@ export class SheetTableControlsRenderController extends Disposable implements IR
private _initRefresh(): void {
const commandExecuted$ = fromCallback(this._commandService.onCommandExecuted.bind(this._commandService))
.pipe(filter(([command]) => {
if (command.id.startsWith('doc.') || SELECTION_ONLY_COMMANDS.has(command.id)) {
return false;
}
if (command.type === CommandType.OPERATION && TABLE_RENDER_REFRESH_COMMANDS.has(command.id)) {
this._closeFloatingControls();
return true;
@@ -157,22 +173,29 @@ export class SheetTableControlsRenderController extends Disposable implements IR
this._sheetTableThemeUIController.refreshTable$,
this._workbookPermissionService.unitPermissionInitStateChange$,
this._permissionService.permissionPointUpdate$,
this._sheetsSelectionsService.selectionChanged$,
commandExecuted$
).subscribe(() => {
this._closeFloatingControls();
this._refresh();
}));
this.disposeWithMe(this._sheetsSelectionsService.selectionChanged$.subscribe(() => {
this._closeFloatingControls();
this._refresh(false);
}));
}
private _refresh(): void {
private _refresh(invalidateScene = true): void {
const skeleton = this._sheetSkeletonManagerService.getCurrentSkeleton();
const worksheet = this._context.unit.getActiveSheet();
if (!skeleton || !worksheet || !this._canEditWorkbook()) {
this._shape.setItems([]);
this._shape.refreshBounds();
this._context.scene.makeDirty();
this._shape.makeDirty(true);
if (invalidateScene) {
this._context.scene.makeDirty();
}
return;
}
@@ -200,7 +223,9 @@ export class SheetTableControlsRenderController extends Disposable implements IR
this._shape.setItems(items);
this._shape.refreshBounds();
this._shape.makeDirty(true);
this._context.scene.makeDirty();
if (invalidateScene) {
this._context.scene.makeDirty();
}
}
private _canEditWorkbook(): boolean {