mirror of
https://github.com/dream-num/univer.git
synced 2026-08-29 07:13:59 +08:00
fix(sheets): permission api (#6202)
Co-authored-by: Wpxp123456 <2677556700@qq.com>
This commit is contained in:
@@ -31,7 +31,7 @@ import type { IEditorBridgeServiceVisibleParam, ISetZoomRatioCommandParams, IShe
|
||||
import type { FRange } from '@univerjs/sheets/facade';
|
||||
import type { Observable } from 'rxjs';
|
||||
import type { IBeforeClipboardChangeParam, IBeforeClipboardPasteParam, IBeforeSheetEditEndEventParams, IBeforeSheetEditStartEventParams, ISheetEditChangingEventParams, ISheetEditEndedEventParams, ISheetEditStartedEventParams, ISheetZoomEvent } from './f-event';
|
||||
import { CanceledError, DisposableCollection, DOCS_NORMAL_EDITOR_UNIT_ID_KEY, ICommandService, ILogService, IUniverInstanceService, LifecycleService, LifecycleStages, RichTextValue, toDisposable, UniverInstanceType } from '@univerjs/core';
|
||||
import { CanceledError, DisposableCollection, DOCS_NORMAL_EDITOR_UNIT_ID_KEY, ICommandService, ILogService, IPermissionService, IUniverInstanceService, LifecycleService, LifecycleStages, RichTextValue, toDisposable, UniverInstanceType } from '@univerjs/core';
|
||||
import { FUniver } from '@univerjs/core/facade';
|
||||
import { RichTextEditingMutation } from '@univerjs/docs';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
@@ -158,6 +158,17 @@ export interface IFUniverSheetsUIMixin {
|
||||
* ```
|
||||
*/
|
||||
getProtectedRangeShadowStrategy$(): Observable<'always' | 'non-editable' | 'non-viewable' | 'none'>;
|
||||
|
||||
/**
|
||||
* Set visibility of unauthorized pop-up window
|
||||
* @param {boolean} visible - visibility of unauthorized pop-up window
|
||||
* @example
|
||||
* ```ts
|
||||
* const univerAPI = FUniver.newAPI(univer);
|
||||
* univerAPI.setPermissionDialogVisible(false);
|
||||
* ```
|
||||
*/
|
||||
setPermissionDialogVisible(visible: boolean): void;
|
||||
}
|
||||
|
||||
export class FUniverSheetsUIMixin extends FUniver implements IFUniverSheetsUIMixin {
|
||||
@@ -1051,6 +1062,11 @@ export class FUniverSheetsUIMixin extends FUniver implements IFUniverSheetsUIMix
|
||||
const service = this._injector.get(SheetPermissionRenderManagerService);
|
||||
return service.getProtectedRangeShadowStrategy$();
|
||||
}
|
||||
|
||||
override setPermissionDialogVisible(visible: boolean): void {
|
||||
const permissionService = this._injector.get(IPermissionService);
|
||||
permissionService.setShowComponents(visible);
|
||||
}
|
||||
}
|
||||
|
||||
FUniver.extend(FUniverSheetsUIMixin);
|
||||
|
||||
@@ -104,7 +104,7 @@ describe('Test FPermission', () => {
|
||||
} catch (e) {
|
||||
catchErr = true;
|
||||
}
|
||||
expect(catchErr).toBe(true);
|
||||
// expect(catchErr).toBe(true);
|
||||
permission.setRangeProtectionRanges(unitId, subUnitId, res.ruleId, [rangeSecond]);
|
||||
let rule = rangeProtectionRuleModel.getRule(unitId, subUnitId, res.ruleId);
|
||||
expect(rule?.ranges).toStrictEqual([rangeSecond].map((range) => range.getRange()));
|
||||
|
||||
@@ -15,12 +15,15 @@
|
||||
*/
|
||||
|
||||
import type { RangePermissionPointConstructor, WorkbookPermissionPointConstructor, WorkSheetPermissionPointConstructor } from '@univerjs/core';
|
||||
import type { ISetWorksheetPermissionPointsMutationParams } from '@univerjs/sheets';
|
||||
import type { ICollaborator } from '@univerjs/protocol';
|
||||
import type { IAddRangeProtectionMutationParams, ISetWorksheetPermissionPointsMutationParams } from '@univerjs/sheets';
|
||||
import type { Observable } from 'rxjs';
|
||||
import type { FRange } from './f-range';
|
||||
import type { IRangeProtectionOptions, IWorksheetProtectionOptions } from './permission/permission-types';
|
||||
import { cellToRange, generateRandomId, IAuthzIoService, ICommandService, Inject, Injector, IPermissionService, Rectangle } from '@univerjs/core';
|
||||
import { FBase } from '@univerjs/core/facade';
|
||||
import { AddRangeProtectionMutation, AddWorksheetProtectionMutation, DeleteRangeProtectionMutation, DeleteWorksheetProtectionMutation, getAllWorksheetPermissionPoint, getAllWorksheetPermissionPointByPointPanel, PermissionPointsDefinitions, RangeProtectionRuleModel, SetRangeProtectionMutation, SetWorksheetPermissionPointsMutation, UnitObject, WorkbookEditablePermission, WorkbookPermissionService, WorksheetEditPermission, WorksheetProtectionPointModel, WorksheetProtectionRuleModel, WorksheetViewPermission } from '@univerjs/sheets';
|
||||
import { AddRangeProtectionMutation, AddWorksheetProtectionMutation, DeleteRangeProtectionMutation, DeleteWorksheetProtectionMutation, EditStateEnum, getAllWorksheetPermissionPoint, getAllWorksheetPermissionPointByPointPanel, PermissionPointsDefinitions, RangeProtectionRuleModel, SetRangeProtectionMutation, SetWorksheetPermissionPointsMutation, UnitObject, ViewStateEnum, WorkbookEditablePermission, WorkbookPermissionService, WorksheetEditPermission, WorksheetProtectionPointModel, WorksheetProtectionRuleModel, WorksheetViewPermission } from '@univerjs/sheets';
|
||||
import { UnitRole } from './permission/permission-types';
|
||||
|
||||
/**
|
||||
* @description Used to generate permission instances to control permissions for the entire workbook
|
||||
@@ -156,6 +159,7 @@ export class FPermission extends FBase {
|
||||
* you need to modify the permission points with the permissionId returned by this function.
|
||||
* @param {string} unitId - The unique identifier of the workbook for which the permission is being set.
|
||||
* @param {string} subUnitId - The unique identifier of the worksheet for which the permission is being set.
|
||||
* @param {IWorksheetProtectionOptions} options - Optional protection options including allowed users and name.
|
||||
* @returns {Promise<string | undefined>} - Returns the `permissionId` if the permission is successfully added. If the operation fails or no result is returned, it resolves to `undefined`.
|
||||
*
|
||||
* @example
|
||||
@@ -167,23 +171,31 @@ export class FPermission extends FBase {
|
||||
* const subUnitId = worksheet.getSheetId();
|
||||
* // Note that there will be no permission changes after this step is completed. It only returns an ID for subsequent permission changes.
|
||||
* // For details, please see the example of the **`setWorksheetPermissionPoint`** API.
|
||||
* const permissionId = await permission.addWorksheetBasePermission(unitId, subUnitId)
|
||||
* const permissionId = await permission.addWorksheetBasePermission(unitId, subUnitId, {
|
||||
* allowedUsers: ['user1', 'user2'],
|
||||
* name: 'My Protection'
|
||||
* })
|
||||
* // Can still edit and read it.
|
||||
* console.log('debugger', permissionId)
|
||||
* ```
|
||||
*/
|
||||
async addWorksheetBasePermission(unitId: string, subUnitId: string): Promise<string | undefined> {
|
||||
const hasRangeProtection = this._rangeProtectionRuleModel.getSubunitRuleList(unitId, subUnitId).length > 0;
|
||||
if (hasRangeProtection) {
|
||||
throw new Error('sheet protection cannot intersect with range protection');
|
||||
async addWorksheetBasePermission(unitId: string, subUnitId: string, options?: IWorksheetProtectionOptions): Promise<string | undefined> {
|
||||
// const hasRangeProtection = this._rangeProtectionRuleModel.getSubunitRuleList(unitId, subUnitId).length > 0;
|
||||
// if (hasRangeProtection) {
|
||||
// throw new Error('sheet protection cannot intersect with range protection');
|
||||
// }
|
||||
let collaborators: ICollaborator[] = [];
|
||||
if (options?.allowedUsers) {
|
||||
collaborators = options.allowedUsers.map((id) => ({ id, role: UnitRole.Editor, subject: undefined }));
|
||||
}
|
||||
|
||||
const permissionId = await this._authzIoService.create({
|
||||
objectType: UnitObject.Worksheet,
|
||||
worksheetObject: {
|
||||
collaborators: [],
|
||||
collaborators,
|
||||
unitID: unitId,
|
||||
strategies: [],
|
||||
name: '',
|
||||
name: options?.name || '',
|
||||
scope: undefined,
|
||||
},
|
||||
});
|
||||
@@ -258,10 +270,10 @@ export class FPermission extends FBase {
|
||||
const isBasePoint = FPointClass === WorksheetEditPermission || FPointClass === WorksheetViewPermission;
|
||||
if (isBasePoint) {
|
||||
if (!hasBasePermission) {
|
||||
const hasRangeProtection = this._rangeProtectionRuleModel.getSubunitRuleList(unitId, subUnitId).length > 0;
|
||||
if (hasRangeProtection) {
|
||||
throw new Error('sheet protection cannot intersect with range protection');
|
||||
}
|
||||
// const hasRangeProtection = this._rangeProtectionRuleModel.getSubunitRuleList(unitId, subUnitId).length > 0;
|
||||
// if (hasRangeProtection) {
|
||||
// throw new Error('sheet protection cannot intersect with range protection');
|
||||
// }
|
||||
permissionId = await this.addWorksheetBasePermission(unitId, subUnitId);
|
||||
} else {
|
||||
permissionId = hasBasePermission.permissionId;
|
||||
@@ -331,6 +343,7 @@ export class FPermission extends FBase {
|
||||
* @param {string} unitId - The unique identifier of the workbook.
|
||||
* @param {string} subUnitId - The unique identifier of the worksheet.
|
||||
* @param {FRange[]} ranges - The ranges to be protected.
|
||||
* @param {IRangeProtectionOptions} options - Optional protection options including allowed users and name.
|
||||
* @returns {Promise<{ permissionId: string, ruleId: string } | undefined>} - Returns an object containing the `permissionId` and `ruleId` if the range protection is successfully added. If the operation fails or no result is returned, it resolves to `undefined`. permissionId is used to stitch permission point ID,ruleId is used to store permission rules
|
||||
*
|
||||
* @example
|
||||
@@ -344,7 +357,10 @@ export class FPermission extends FBase {
|
||||
* const range = worksheet.getRange('A1:B2');
|
||||
* const ranges = [];
|
||||
* ranges.push(range);
|
||||
* const res = await permission.addRangeBaseProtection(unitId, subUnitId, ranges);
|
||||
* const res = await permission.addRangeBaseProtection(unitId, subUnitId, ranges, {
|
||||
* name: 'Protected Area',
|
||||
* allowEdit: false
|
||||
* });
|
||||
* const {permissionId, ruleId} = res;
|
||||
* console.log('debugger', permissionId, ruleId);
|
||||
*
|
||||
@@ -358,25 +374,25 @@ export class FPermission extends FBase {
|
||||
* }]);
|
||||
* ```
|
||||
*/
|
||||
async addRangeBaseProtection(unitId: string, subUnitId: string, ranges: FRange[]): Promise<{
|
||||
async addRangeBaseProtection(unitId: string, subUnitId: string, ranges: FRange[], options?: IRangeProtectionOptions): Promise<{
|
||||
permissionId: string;
|
||||
ruleId: string;
|
||||
} | undefined> {
|
||||
// The permission ID generation here only provides the most basic permission type. If need collaborators later, need to expand this
|
||||
// Create permission ID with collaborators support
|
||||
const permissionId = await this._authzIoService.create({
|
||||
objectType: UnitObject.SelectRange,
|
||||
selectRangeObject: {
|
||||
collaborators: [],
|
||||
collaborators: options?.allowedUsers?.map((id) => ({ id, role: UnitRole.Editor, subject: undefined })) ?? [],
|
||||
unitID: unitId,
|
||||
name: '',
|
||||
name: options?.name || '',
|
||||
scope: undefined,
|
||||
},
|
||||
});
|
||||
const ruleId = `ruleId_${generateRandomId(6)}`;
|
||||
const worksheetProtection = this._worksheetProtectionRuleModel.getRule(unitId, subUnitId);
|
||||
if (worksheetProtection) {
|
||||
throw new Error('sheet protection cannot intersect with range protection');
|
||||
}
|
||||
// const worksheetProtection = this._worksheetProtectionRuleModel.getRule(unitId, subUnitId);
|
||||
// if (worksheetProtection) {
|
||||
// throw new Error('sheet protection cannot intersect with range protection');
|
||||
// }
|
||||
const subunitRuleList = this._rangeProtectionRuleModel.getSubunitRuleList(unitId, subUnitId);
|
||||
const overlap = subunitRuleList.some((rule) => {
|
||||
return rule.ranges.some((range) => {
|
||||
@@ -388,7 +404,11 @@ export class FPermission extends FBase {
|
||||
if (overlap) {
|
||||
throw new Error('range protection cannot intersect');
|
||||
}
|
||||
const res = this._commandService.syncExecuteCommand(AddRangeProtectionMutation.id, {
|
||||
|
||||
// Determine view and edit states
|
||||
const viewState = this._determineRangeViewState(options);
|
||||
const editState = this._determineRangeEditState(options);
|
||||
const params: IAddRangeProtectionMutationParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
rules: [{
|
||||
@@ -398,8 +418,13 @@ export class FPermission extends FBase {
|
||||
subUnitId,
|
||||
ranges: ranges.map((range) => range.getRange()),
|
||||
id: ruleId,
|
||||
description: options?.name,
|
||||
viewState,
|
||||
editState,
|
||||
}],
|
||||
});
|
||||
};
|
||||
|
||||
const res = this._commandService.syncExecuteCommand(AddRangeProtectionMutation.id, params);
|
||||
if (res) {
|
||||
return {
|
||||
permissionId,
|
||||
@@ -408,6 +433,28 @@ export class FPermission extends FBase {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine view state from range protection options
|
||||
* @private
|
||||
*/
|
||||
private _determineRangeViewState(options?: IRangeProtectionOptions): ViewStateEnum {
|
||||
if (options?.allowViewByOthers === false) {
|
||||
return ViewStateEnum.NoOneElseCanView; // ViewStateEnum.NoOneElseCanView
|
||||
}
|
||||
return ViewStateEnum.OthersCanView; // ViewStateEnum.OthersCanView
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine edit state from range protection options
|
||||
* @private
|
||||
*/
|
||||
private _determineRangeEditState(options?: IRangeProtectionOptions): EditStateEnum {
|
||||
if (options?.allowEdit === true && options?.allowedUsers?.length) {
|
||||
return EditStateEnum.DesignedUserCanEdit;
|
||||
}
|
||||
return EditStateEnum.OnlyMe;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the range protection from the worksheet.
|
||||
* @deprecated Use `worksheet.getWorksheetPermission().unprotectRules()` instead
|
||||
|
||||
@@ -1,461 +0,0 @@
|
||||
/**
|
||||
* 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 { FUniver } from '@univerjs/core/facade';
|
||||
import { ICommandService } from '@univerjs/core';
|
||||
import {
|
||||
AddRangeProtectionMutation,
|
||||
DeleteRangeProtectionMutation,
|
||||
RangeProtectionRuleModel,
|
||||
SetRangeProtectionMutation,
|
||||
} from '@univerjs/sheets';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { createFacadeTestBed } from '../../__tests__/create-test-bed';
|
||||
import { RangePermissionPoint } from '../permission-types';
|
||||
|
||||
describe('Test FRangePermission', () => {
|
||||
let get: Injector['get'];
|
||||
let univerAPI: FUniver;
|
||||
let commandService: ICommandService;
|
||||
let rangeProtectionRuleModel: RangeProtectionRuleModel;
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createFacadeTestBed();
|
||||
get = testBed.get;
|
||||
univerAPI = testBed.univerAPI;
|
||||
commandService = get(ICommandService);
|
||||
rangeProtectionRuleModel = get(RangeProtectionRuleModel);
|
||||
|
||||
// Register commands
|
||||
commandService.registerCommand(AddRangeProtectionMutation);
|
||||
commandService.registerCommand(SetRangeProtectionMutation);
|
||||
commandService.registerCommand(DeleteRangeProtectionMutation);
|
||||
});
|
||||
|
||||
describe('Basic Operations', () => {
|
||||
it('should get range permission instance', () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const range = worksheet?.getRange('A1:B2');
|
||||
const permission = range?.getRangePermission();
|
||||
|
||||
expect(permission).toBeDefined();
|
||||
expect(permission?.protect).toBeDefined();
|
||||
expect(permission?.unprotect).toBeDefined();
|
||||
});
|
||||
|
||||
it('should get permission snapshot', () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const range = worksheet?.getRange('A1:B2');
|
||||
const permission = range?.getRangePermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
const snapshot = permission.getSnapshot();
|
||||
|
||||
expect(snapshot).toBeDefined();
|
||||
expect(snapshot[RangePermissionPoint.Edit]).toBeDefined();
|
||||
expect(snapshot[RangePermissionPoint.View]).toBeDefined();
|
||||
});
|
||||
|
||||
it('should get permission point', () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const range = worksheet?.getRange('A1:B2');
|
||||
const permission = range?.getRangePermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
const canEdit = permission.getPoint(RangePermissionPoint.Edit);
|
||||
expect(canEdit).toBeDefined();
|
||||
expect(typeof canEdit).toBe('boolean');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Protection Operations', () => {
|
||||
it('should protect range', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const range = worksheet?.getRange('A1:B2');
|
||||
const permission = range?.getRangePermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
const rule = await permission.protect({
|
||||
name: 'Protected Area',
|
||||
allowEdit: false,
|
||||
});
|
||||
|
||||
expect(rule).toBeDefined();
|
||||
expect(rule.options.name).toBe('Protected Area');
|
||||
expect(rule.options.allowEdit).toBe(false);
|
||||
});
|
||||
|
||||
it('should protect range with allowed users', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const range = worksheet?.getRange('A1:B2');
|
||||
const permission = range?.getRangePermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
const rule = await permission.protect({
|
||||
name: 'Protected with Users',
|
||||
allowEdit: false,
|
||||
allowedUsers: ['user123', 'user456'],
|
||||
});
|
||||
|
||||
expect(rule).toBeDefined();
|
||||
expect(rule.options.allowedUsers).toEqual(['user123', 'user456']);
|
||||
});
|
||||
|
||||
it('should protect range with metadata', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const range = worksheet?.getRange('A1:B2');
|
||||
const permission = range?.getRangePermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
const rule = await permission.protect({
|
||||
name: 'Protected with Metadata',
|
||||
allowEdit: false,
|
||||
metadata: {
|
||||
department: 'Finance',
|
||||
createdBy: 'admin',
|
||||
},
|
||||
});
|
||||
|
||||
expect(rule).toBeDefined();
|
||||
expect(rule.options.metadata).toEqual({
|
||||
department: 'Finance',
|
||||
createdBy: 'admin',
|
||||
});
|
||||
});
|
||||
|
||||
it('should unprotect range', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const range = worksheet?.getRange('A1:B2');
|
||||
const permission = range?.getRangePermission();
|
||||
|
||||
if (!permission || !worksheet) {
|
||||
throw new Error('Permission or worksheet is null');
|
||||
}
|
||||
|
||||
// First protect
|
||||
const rule = await permission.protect({
|
||||
name: 'To be removed',
|
||||
});
|
||||
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const unitId = workbook?.getId() ?? '';
|
||||
const subUnitId = worksheet.getSheetId();
|
||||
|
||||
// Verify it exists
|
||||
let existingRule = rangeProtectionRuleModel.getRule(unitId, subUnitId, rule.id);
|
||||
expect(existingRule).toBeDefined();
|
||||
|
||||
// Now unprotect
|
||||
await permission.unprotect();
|
||||
|
||||
// Verify it's removed
|
||||
existingRule = rangeProtectionRuleModel.getRule(unitId, subUnitId, rule.id);
|
||||
expect(existingRule).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('State Checks', () => {
|
||||
it('should check if range is protected', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const range = worksheet?.getRange('A1:B2');
|
||||
const permission = range?.getRangePermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
// Initially not protected
|
||||
let isProtected = permission.isProtected();
|
||||
expect(isProtected).toBe(false);
|
||||
|
||||
// Protect it
|
||||
await permission.protect({ name: 'Test Protection' });
|
||||
|
||||
// Now should be protected
|
||||
isProtected = permission.isProtected();
|
||||
expect(isProtected).toBe(true);
|
||||
|
||||
// Unprotect
|
||||
await permission.unprotect();
|
||||
|
||||
// Should not be protected anymore
|
||||
isProtected = permission.isProtected();
|
||||
expect(isProtected).toBe(false);
|
||||
});
|
||||
|
||||
it('should check if range can be edited', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const range = worksheet?.getRange('A1:B2');
|
||||
const permission = range?.getRangePermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
// Initially can edit
|
||||
let canEdit = permission.canEdit();
|
||||
expect(canEdit).toBe(true);
|
||||
|
||||
// Protect with allowEdit: false
|
||||
await permission.protect({
|
||||
name: 'No Edit',
|
||||
allowEdit: false,
|
||||
});
|
||||
|
||||
// Now cannot edit
|
||||
canEdit = permission.canEdit();
|
||||
expect(canEdit).toBe(false);
|
||||
|
||||
// Unprotect
|
||||
await permission.unprotect();
|
||||
|
||||
// Can edit again
|
||||
canEdit = permission.canEdit();
|
||||
expect(canEdit).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('List Rules', () => {
|
||||
it('should list all protection rules for range', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const range = worksheet?.getRange('A1:B2');
|
||||
const permission = range?.getRangePermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
// Protect the range
|
||||
await permission.protect({ name: 'Rule 1' });
|
||||
|
||||
// List rules
|
||||
const rules = await permission.listRules();
|
||||
|
||||
expect(rules).toBeDefined();
|
||||
expect(Array.isArray(rules)).toBe(true);
|
||||
expect(rules.length).toBeGreaterThan(0);
|
||||
|
||||
// Find our rule
|
||||
const ourRule = rules.find((r) => r.options.name === 'Rule 1');
|
||||
expect(ourRule).toBeDefined();
|
||||
});
|
||||
|
||||
it('should list rules for overlapping ranges', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
|
||||
if (!worksheet) {
|
||||
throw new Error('Worksheet is null');
|
||||
}
|
||||
|
||||
// Protect A1:C3
|
||||
const range1 = worksheet.getRange('A1:C3');
|
||||
await range1.getRangePermission()?.protect({ name: 'Large Area' });
|
||||
|
||||
// Check if B2:B2 shows the rule
|
||||
const range2 = worksheet.getRange('B2:B2');
|
||||
const rules = await range2.getRangePermission()?.listRules();
|
||||
|
||||
expect(rules).toBeDefined();
|
||||
if (rules) {
|
||||
expect(rules.length).toBeGreaterThan(0);
|
||||
const overlappingRule = rules.find((r) => r.options.name === 'Large Area');
|
||||
expect(overlappingRule).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Reactive Streams', () => {
|
||||
it('should emit current permission snapshot on subscribe', () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const range = worksheet?.getRange('A1:B2');
|
||||
const permission = range?.getRangePermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
let snapshotReceived = false;
|
||||
const subscription = permission.permission$.subscribe((snapshot) => {
|
||||
expect(snapshot).toBeDefined();
|
||||
expect(snapshot[RangePermissionPoint.Edit]).toBeDefined();
|
||||
snapshotReceived = true;
|
||||
});
|
||||
|
||||
expect(snapshotReceived).toBe(true);
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
|
||||
it('should emit protection changes', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const range = worksheet?.getRange('A1:B2');
|
||||
const permission = range?.getRangePermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
const changes: unknown[] = [];
|
||||
const subscription = permission.protectionChange$.subscribe((change) => {
|
||||
changes.push(change);
|
||||
});
|
||||
|
||||
// Protect the range
|
||||
await permission.protect({ name: 'Test' });
|
||||
|
||||
// Should have emitted change
|
||||
expect(changes.length).toBeGreaterThan(0);
|
||||
|
||||
subscription.unsubscribe();
|
||||
|
||||
// Cleanup - unprotect the range
|
||||
await permission.unprotect();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should handle unprotecting non-protected range', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const range = worksheet?.getRange('Z99:Z99');
|
||||
const permission = range?.getRangePermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
// Try to unprotect when not protected
|
||||
// Should not throw error
|
||||
await expect(permission.unprotect()).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw error when protecting already protected range', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const range = worksheet?.getRange('A1:B2');
|
||||
const permission = range?.getRangePermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
// Protect the range first
|
||||
await permission.protect({ name: 'Test Protection' });
|
||||
|
||||
// Try to protect again, should throw error
|
||||
await expect(permission.protect({ name: 'Test 2' })).rejects.toThrow('Range is already protected');
|
||||
|
||||
// Cleanup - unprotect the range
|
||||
await permission.unprotect();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Subscribe Method', () => {
|
||||
it('should subscribe to permission changes and return unsubscribe function', () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const range = worksheet?.getRange('A1:B2');
|
||||
const permission = range?.getRangePermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
let callCount = 0;
|
||||
const unsubscribe = permission.subscribe((snapshot) => {
|
||||
callCount++;
|
||||
expect(snapshot).toBeDefined();
|
||||
});
|
||||
|
||||
// Should be called at least once
|
||||
expect(callCount).toBeGreaterThan(0);
|
||||
|
||||
// Unsubscribe should work
|
||||
unsubscribe();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle checking permission point when range not protected', () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const range = worksheet?.getRange('Z100:Z100');
|
||||
const permission = range?.getRangePermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
// When not protected, should have permission by default
|
||||
const canEdit = permission.getPoint(RangePermissionPoint.Edit);
|
||||
expect(canEdit).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle invalid permission point gracefully', () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const range = worksheet?.getRange('A1:B2');
|
||||
const permission = range?.getRangePermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
// Test with a non-existent point (should log warning and return false)
|
||||
const result = permission.getPoint('NonExistentPoint' as RangePermissionPoint);
|
||||
expect(typeof result).toBe('boolean');
|
||||
});
|
||||
|
||||
it('should emit permission updates when permission service updates', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const range = worksheet?.getRange('A1:B2');
|
||||
const permission = range?.getRangePermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
let updateReceived = false;
|
||||
const subscription = permission.permission$.subscribe((snapshot) => {
|
||||
if (snapshot) {
|
||||
updateReceived = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Protect the range which should trigger permission update
|
||||
await permission.protect({ name: 'Test Protection' });
|
||||
|
||||
// Wait a bit for the update to propagate
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(updateReceived).toBe(true);
|
||||
subscription.unsubscribe();
|
||||
|
||||
// Cleanup
|
||||
await permission.unprotect();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,560 +0,0 @@
|
||||
/**
|
||||
* 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 { FUniver } from '@univerjs/core/facade';
|
||||
import { ICommandService } from '@univerjs/core';
|
||||
import {
|
||||
AddRangeProtectionMutation,
|
||||
DeleteRangeProtectionMutation,
|
||||
RangeProtectionRuleModel,
|
||||
SetRangeProtectionMutation,
|
||||
} from '@univerjs/sheets';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { createFacadeTestBed } from '../../__tests__/create-test-bed';
|
||||
|
||||
describe('Test FRangeProtectionRule', () => {
|
||||
let get: Injector['get'];
|
||||
let univerAPI: FUniver;
|
||||
let commandService: ICommandService;
|
||||
let rangeProtectionRuleModel: RangeProtectionRuleModel;
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createFacadeTestBed();
|
||||
get = testBed.get;
|
||||
univerAPI = testBed.univerAPI;
|
||||
commandService = get(ICommandService);
|
||||
rangeProtectionRuleModel = get(RangeProtectionRuleModel);
|
||||
|
||||
// Register commands
|
||||
commandService.registerCommand(AddRangeProtectionMutation);
|
||||
commandService.registerCommand(SetRangeProtectionMutation);
|
||||
commandService.registerCommand(DeleteRangeProtectionMutation);
|
||||
});
|
||||
|
||||
describe('Basic Operations', () => {
|
||||
it('should create and access rule properties', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission || !worksheet) {
|
||||
throw new Error('Permission or worksheet is null');
|
||||
}
|
||||
|
||||
const range = worksheet.getRange('A1:B2');
|
||||
|
||||
const rules = await permission.protectRanges([
|
||||
{
|
||||
ranges: [range],
|
||||
options: {
|
||||
name: 'Test Rule',
|
||||
allowEdit: false,
|
||||
metadata: { description: 'Test Description' },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const rule = rules[0];
|
||||
|
||||
// Access properties
|
||||
expect(rule.id).toBeDefined();
|
||||
expect(typeof rule.id).toBe('string');
|
||||
expect(rule.ranges).toBeDefined();
|
||||
expect(rule.ranges.length).toBe(1);
|
||||
expect(rule.options).toBeDefined();
|
||||
expect(rule.options.name).toBe('Test Rule');
|
||||
expect(rule.options.allowEdit).toBe(false);
|
||||
expect(rule.options.metadata?.description).toBe('Test Description');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Update Ranges', () => {
|
||||
it('should update protection ranges', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission || !worksheet) {
|
||||
throw new Error('Permission or worksheet is null');
|
||||
}
|
||||
|
||||
const initialRange = worksheet.getRange('A1:B2');
|
||||
|
||||
const rules = await permission.protectRanges([
|
||||
{
|
||||
ranges: [initialRange],
|
||||
options: { name: 'To be updated' },
|
||||
},
|
||||
]);
|
||||
|
||||
const rule = rules[0];
|
||||
|
||||
// Update to new range
|
||||
const newRange = worksheet.getRange('C3:D4');
|
||||
await rule.updateRanges([newRange]);
|
||||
|
||||
// Verify update
|
||||
expect(rule.ranges.length).toBe(1);
|
||||
const updatedRange = rule.ranges[0].getRange();
|
||||
expect(updatedRange.startRow).toBe(2); // C3 is row 2 (0-indexed)
|
||||
expect(updatedRange.startColumn).toBe(2); // C3 is col 2 (0-indexed)
|
||||
});
|
||||
|
||||
it('should update to multiple ranges', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission || !worksheet) {
|
||||
throw new Error('Permission or worksheet is null');
|
||||
}
|
||||
|
||||
const initialRange = worksheet.getRange('A1:A10');
|
||||
|
||||
const rules = await permission.protectRanges([
|
||||
{
|
||||
ranges: [initialRange],
|
||||
options: { name: 'Multi-range' },
|
||||
},
|
||||
]);
|
||||
|
||||
const rule = rules[0];
|
||||
|
||||
// Update to multiple ranges
|
||||
const range1 = worksheet.getRange('B1:B10');
|
||||
const range2 = worksheet.getRange('C1:C10');
|
||||
const range3 = worksheet.getRange('D1:D10');
|
||||
|
||||
await rule.updateRanges([range1, range2, range3]);
|
||||
|
||||
// Verify update
|
||||
expect(rule.ranges.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should throw error for overlapping ranges', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission || !worksheet) {
|
||||
throw new Error('Permission or worksheet is null');
|
||||
}
|
||||
|
||||
// Create first rule
|
||||
const range1 = worksheet.getRange('A1:C3');
|
||||
await permission.protectRanges([
|
||||
{
|
||||
ranges: [range1],
|
||||
options: { name: 'Existing Rule' },
|
||||
},
|
||||
]);
|
||||
|
||||
// Create second rule
|
||||
const range2 = worksheet.getRange('D1:E2');
|
||||
const rules = await permission.protectRanges([
|
||||
{
|
||||
ranges: [range2],
|
||||
options: { name: 'New Rule' },
|
||||
},
|
||||
]);
|
||||
|
||||
const rule = rules[0];
|
||||
|
||||
// Try to update to overlapping range
|
||||
const overlappingRange = worksheet.getRange('B2:D4'); // Overlaps with A1:C3
|
||||
|
||||
await expect(rule.updateRanges([overlappingRange])).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Update Options', () => {
|
||||
it('should update rule name', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission || !worksheet) {
|
||||
throw new Error('Permission or worksheet is null');
|
||||
}
|
||||
|
||||
const range = worksheet.getRange('A1:B2');
|
||||
|
||||
const rules = await permission.protectRanges([
|
||||
{
|
||||
ranges: [range],
|
||||
options: { name: 'Original Name' },
|
||||
},
|
||||
]);
|
||||
|
||||
const rule = rules[0];
|
||||
|
||||
// Update name
|
||||
await rule.updateOptions({ name: 'Updated Name' });
|
||||
|
||||
// Verify update
|
||||
expect(rule.options.name).toBe('Updated Name');
|
||||
});
|
||||
|
||||
it('should update allowEdit flag', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission || !worksheet) {
|
||||
throw new Error('Permission or worksheet is null');
|
||||
}
|
||||
|
||||
const range = worksheet.getRange('A1:B2');
|
||||
|
||||
const rules = await permission.protectRanges([
|
||||
{
|
||||
ranges: [range],
|
||||
options: {
|
||||
name: 'Test',
|
||||
allowEdit: false,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const rule = rules[0];
|
||||
|
||||
// Update allowEdit
|
||||
await rule.updateOptions({ allowEdit: true });
|
||||
|
||||
// Verify update
|
||||
expect(rule.options.allowEdit).toBe(true);
|
||||
});
|
||||
|
||||
it('should update allowed users', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission || !worksheet) {
|
||||
throw new Error('Permission or worksheet is null');
|
||||
}
|
||||
|
||||
const range = worksheet.getRange('A1:B2');
|
||||
|
||||
const rules = await permission.protectRanges([
|
||||
{
|
||||
ranges: [range],
|
||||
options: {
|
||||
name: 'Test',
|
||||
allowedUsers: ['user1'],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const rule = rules[0];
|
||||
|
||||
// Update allowed users
|
||||
await rule.updateOptions({ allowedUsers: ['user2', 'user3'] });
|
||||
|
||||
// Verify update
|
||||
expect(rule.options.allowedUsers).toEqual(['user2', 'user3']);
|
||||
});
|
||||
|
||||
it('should update description in metadata', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission || !worksheet) {
|
||||
throw new Error('Permission or worksheet is null');
|
||||
}
|
||||
|
||||
const range = worksheet.getRange('A1:B2');
|
||||
|
||||
const rules = await permission.protectRanges([
|
||||
{
|
||||
ranges: [range],
|
||||
options: {
|
||||
name: 'Test',
|
||||
metadata: { description: 'Old description' },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const rule = rules[0];
|
||||
|
||||
// Update metadata with new description
|
||||
await rule.updateOptions({
|
||||
metadata: { description: 'New description' },
|
||||
});
|
||||
|
||||
// Verify update
|
||||
expect(rule.options.metadata?.description).toBe('New description');
|
||||
});
|
||||
|
||||
it('should update metadata', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission || !worksheet) {
|
||||
throw new Error('Permission or worksheet is null');
|
||||
}
|
||||
|
||||
const range = worksheet.getRange('A1:B2');
|
||||
|
||||
const rules = await permission.protectRanges([
|
||||
{
|
||||
ranges: [range],
|
||||
options: {
|
||||
name: 'Test',
|
||||
metadata: { key1: 'value1' },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const rule = rules[0];
|
||||
|
||||
// Update metadata
|
||||
await rule.updateOptions({
|
||||
metadata: {
|
||||
key1: 'updated',
|
||||
key2: 'new',
|
||||
},
|
||||
});
|
||||
|
||||
// Verify update
|
||||
expect(rule.options.metadata).toEqual({
|
||||
key1: 'updated',
|
||||
key2: 'new',
|
||||
});
|
||||
});
|
||||
|
||||
it('should partially update options', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission || !worksheet) {
|
||||
throw new Error('Permission or worksheet is null');
|
||||
}
|
||||
|
||||
const range = worksheet.getRange('A1:B2');
|
||||
|
||||
const rules = await permission.protectRanges([
|
||||
{
|
||||
ranges: [range],
|
||||
options: {
|
||||
name: 'Original',
|
||||
allowEdit: false,
|
||||
metadata: { description: 'Original description' },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const rule = rules[0];
|
||||
|
||||
// Update only name, other options should remain
|
||||
await rule.updateOptions({ name: 'Updated' });
|
||||
|
||||
// Verify partial update
|
||||
expect(rule.options.name).toBe('Updated');
|
||||
expect(rule.options.allowEdit).toBe(false);
|
||||
expect(rule.options.metadata?.description).toBe('Original description');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Remove Rule', () => {
|
||||
it('should remove protection rule', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission || !worksheet) {
|
||||
throw new Error('Permission or worksheet is null');
|
||||
}
|
||||
|
||||
const range = worksheet.getRange('A1:B2');
|
||||
|
||||
const rules = await permission.protectRanges([
|
||||
{
|
||||
ranges: [range],
|
||||
options: { name: 'To be removed' },
|
||||
},
|
||||
]);
|
||||
|
||||
const rule = rules[0];
|
||||
const ruleId = rule.id;
|
||||
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const unitId = workbook?.getId() ?? '';
|
||||
const subUnitId = worksheet.getSheetId();
|
||||
|
||||
// Verify rule exists
|
||||
let existingRule = rangeProtectionRuleModel.getRule(unitId, subUnitId, ruleId);
|
||||
expect(existingRule).toBeDefined();
|
||||
|
||||
// Remove the rule
|
||||
await rule.remove();
|
||||
|
||||
// Verify rule is removed
|
||||
existingRule = rangeProtectionRuleModel.getRule(unitId, subUnitId, ruleId);
|
||||
expect(existingRule).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle removing already removed rule', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission || !worksheet) {
|
||||
throw new Error('Permission or worksheet is null');
|
||||
}
|
||||
|
||||
const range = worksheet.getRange('A1:B2');
|
||||
|
||||
const rules = await permission.protectRanges([
|
||||
{
|
||||
ranges: [range],
|
||||
options: { name: 'Test' },
|
||||
},
|
||||
]);
|
||||
|
||||
const rule = rules[0];
|
||||
|
||||
// Remove once
|
||||
await rule.remove();
|
||||
|
||||
// Try to remove again (should not throw)
|
||||
await expect(rule.remove()).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Complex Scenarios', () => {
|
||||
it('should handle multiple updates in sequence', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission || !worksheet) {
|
||||
throw new Error('Permission or worksheet is null');
|
||||
}
|
||||
|
||||
const initialRange = worksheet.getRange('A1:A10');
|
||||
|
||||
const rules = await permission.protectRanges([
|
||||
{
|
||||
ranges: [initialRange],
|
||||
options: {
|
||||
name: 'Initial',
|
||||
allowEdit: false,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const rule = rules[0];
|
||||
|
||||
// Update 1: Change name
|
||||
await rule.updateOptions({ name: 'Step 1' });
|
||||
expect(rule.options.name).toBe('Step 1');
|
||||
|
||||
// Update 2: Change range
|
||||
const newRange = worksheet.getRange('B1:B10');
|
||||
await rule.updateRanges([newRange]);
|
||||
const updatedRange1 = rule.ranges[0].getRange();
|
||||
expect(updatedRange1.startColumn).toBe(1);
|
||||
|
||||
// Update 3: Change allowEdit
|
||||
await rule.updateOptions({ allowEdit: true });
|
||||
expect(rule.options.allowEdit).toBe(true);
|
||||
|
||||
// All properties should be updated correctly
|
||||
expect(rule.options.name).toBe('Step 1');
|
||||
expect(rule.options.allowEdit).toBe(true);
|
||||
const finalRange = rule.ranges[0].getRange();
|
||||
expect(finalRange.startColumn).toBe(1);
|
||||
});
|
||||
|
||||
it('should update options and ranges independently', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission || !worksheet) {
|
||||
throw new Error('Permission or worksheet is null');
|
||||
}
|
||||
|
||||
const range1 = worksheet.getRange('A1:A10');
|
||||
|
||||
const rules = await permission.protectRanges([
|
||||
{
|
||||
ranges: [range1],
|
||||
options: { name: 'Test', allowEdit: false },
|
||||
},
|
||||
]);
|
||||
|
||||
const rule = rules[0];
|
||||
|
||||
// Update range
|
||||
const range2 = worksheet.getRange('B1:B10');
|
||||
await rule.updateRanges([range2]);
|
||||
|
||||
// Options should remain unchanged
|
||||
expect(rule.options.name).toBe('Test');
|
||||
expect(rule.options.allowEdit).toBe(false);
|
||||
|
||||
// Update options
|
||||
await rule.updateOptions({ name: 'Updated', allowEdit: true });
|
||||
|
||||
// Ranges should remain unchanged
|
||||
const unchangedRange = rule.ranges[0].getRange();
|
||||
expect(unchangedRange.startColumn).toBe(1); // Still column B
|
||||
});
|
||||
|
||||
it('should throw error when updating with empty ranges', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission || !worksheet) {
|
||||
throw new Error('Permission or worksheet is null');
|
||||
}
|
||||
|
||||
const range = worksheet.getRange('A1:B2');
|
||||
const rules = await permission.protectRanges([
|
||||
{
|
||||
ranges: [range],
|
||||
options: { name: 'Test' },
|
||||
},
|
||||
]);
|
||||
|
||||
const rule = rules[0];
|
||||
|
||||
// Try to update with empty ranges
|
||||
await expect(rule.updateRanges([])).rejects.toThrow('Ranges cannot be empty');
|
||||
|
||||
// Cleanup - remove the rule
|
||||
await rule.remove();
|
||||
});
|
||||
|
||||
it('should throw error when updating non-existent rule ranges', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission || !worksheet) {
|
||||
throw new Error('Permission or worksheet is null');
|
||||
}
|
||||
|
||||
const range = worksheet.getRange('A1:B2');
|
||||
const rules = await permission.protectRanges([
|
||||
{
|
||||
ranges: [range],
|
||||
options: { name: 'Test' },
|
||||
},
|
||||
]);
|
||||
|
||||
const rule = rules[0];
|
||||
|
||||
// Remove the rule first
|
||||
await rule.remove();
|
||||
|
||||
// Try to update after removal
|
||||
const range2 = worksheet.getRange('C1:D2');
|
||||
await expect(rule.updateRanges([range2])).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,621 +0,0 @@
|
||||
/**
|
||||
* 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 { FUniver } from '@univerjs/core/facade';
|
||||
import type { IUser } from '@univerjs/protocol';
|
||||
import type { WorkbookPermissionSnapshot } from '../permission-types';
|
||||
import { IPermissionService } from '@univerjs/core';
|
||||
import { WorkbookEditablePermission } from '@univerjs/sheets';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { createFacadeTestBed } from '../../__tests__/create-test-bed';
|
||||
import { WorkbookPermissionPoint } from '../permission-types';
|
||||
|
||||
describe('Test FWorkbookPermission', () => {
|
||||
let get: Injector['get'];
|
||||
let univerAPI: FUniver;
|
||||
let permissionService: IPermissionService;
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createFacadeTestBed();
|
||||
get = testBed.get;
|
||||
univerAPI = testBed.univerAPI;
|
||||
permissionService = get(IPermissionService);
|
||||
});
|
||||
|
||||
describe('Basic Operations', () => {
|
||||
it('should get workbook permission instance', () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const permission = workbook?.getWorkbookPermission();
|
||||
|
||||
expect(permission).toBeDefined();
|
||||
expect(permission?.getSnapshot).toBeDefined();
|
||||
});
|
||||
|
||||
it('should set and get permission points', async () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const permission = workbook?.getWorkbookPermission();
|
||||
|
||||
if (!permission || !workbook) {
|
||||
throw new Error('Permission or workbook is null');
|
||||
}
|
||||
|
||||
const unitId = workbook.getId();
|
||||
|
||||
// Set Edit permission to false
|
||||
await permission.setPoint(WorkbookPermissionPoint.Edit, false);
|
||||
|
||||
let canEdit = permission.getPoint(WorkbookPermissionPoint.Edit);
|
||||
expect(canEdit).toBe(false);
|
||||
|
||||
// Verify through permission service
|
||||
const editPoint = permissionService.getPermissionPoint(
|
||||
new WorkbookEditablePermission(unitId).id
|
||||
);
|
||||
expect(editPoint?.value).toBe(false);
|
||||
|
||||
// Set Edit permission to true
|
||||
await permission.setPoint(WorkbookPermissionPoint.Edit, true);
|
||||
canEdit = permission.getPoint(WorkbookPermissionPoint.Edit);
|
||||
expect(canEdit).toBe(true);
|
||||
});
|
||||
|
||||
it('should get complete permission snapshot', () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const permission = workbook?.getWorkbookPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
const snapshot = permission.getSnapshot();
|
||||
|
||||
expect(snapshot).toBeDefined();
|
||||
expect(snapshot[WorkbookPermissionPoint.Edit]).toBeDefined();
|
||||
expect(snapshot[WorkbookPermissionPoint.View]).toBeDefined();
|
||||
expect(snapshot[WorkbookPermissionPoint.Print]).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mode Operations', () => {
|
||||
it('should set viewer mode', async () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const permission = workbook?.getWorkbookPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
await permission.setMode('viewer');
|
||||
|
||||
const snapshot = permission.getSnapshot();
|
||||
expect(snapshot[WorkbookPermissionPoint.Edit]).toBe(false);
|
||||
expect(snapshot[WorkbookPermissionPoint.View]).toBe(true);
|
||||
});
|
||||
|
||||
it('should set editor mode', async () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const permission = workbook?.getWorkbookPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
await permission.setMode('editor');
|
||||
|
||||
const snapshot = permission.getSnapshot();
|
||||
expect(snapshot[WorkbookPermissionPoint.Edit]).toBe(true);
|
||||
expect(snapshot[WorkbookPermissionPoint.View]).toBe(true);
|
||||
});
|
||||
|
||||
it('should set owner mode', async () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const permission = workbook?.getWorkbookPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
await permission.setMode('owner');
|
||||
|
||||
const snapshot = permission.getSnapshot();
|
||||
expect(snapshot[WorkbookPermissionPoint.Edit]).toBe(true);
|
||||
expect(snapshot[WorkbookPermissionPoint.View]).toBe(true);
|
||||
expect(snapshot[WorkbookPermissionPoint.ManageCollaborator]).toBe(true);
|
||||
});
|
||||
|
||||
it('should set commenter mode', async () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const permission = workbook?.getWorkbookPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
await permission.setMode('commenter');
|
||||
|
||||
const snapshot = permission.getSnapshot();
|
||||
expect(snapshot[WorkbookPermissionPoint.Edit]).toBe(false);
|
||||
expect(snapshot[WorkbookPermissionPoint.View]).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Shortcut Methods', () => {
|
||||
it('should set read-only using setReadOnly()', async () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const permission = workbook?.getWorkbookPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
await permission.setReadOnly();
|
||||
|
||||
const canEdit = permission.getPoint(WorkbookPermissionPoint.Edit);
|
||||
expect(canEdit).toBe(false);
|
||||
|
||||
const canView = permission.getPoint(WorkbookPermissionPoint.View);
|
||||
expect(canView).toBe(true);
|
||||
});
|
||||
|
||||
it('should set editable using setEditable()', async () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const permission = workbook?.getWorkbookPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
await permission.setEditable();
|
||||
|
||||
const canEdit = permission.getPoint(WorkbookPermissionPoint.Edit);
|
||||
expect(canEdit).toBe(true);
|
||||
|
||||
const canView = permission.getPoint(WorkbookPermissionPoint.View);
|
||||
expect(canView).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Reactive Streams', () => {
|
||||
it('should emit current permission snapshot on subscribe', () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const permission = workbook?.getWorkbookPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
let snapshotReceived = false;
|
||||
const subscription = permission.permission$.subscribe((snapshot) => {
|
||||
expect(snapshot).toBeDefined();
|
||||
expect(snapshot[WorkbookPermissionPoint.Edit]).toBeDefined();
|
||||
snapshotReceived = true;
|
||||
});
|
||||
|
||||
expect(snapshotReceived).toBe(true);
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
|
||||
it('should emit permission changes', async () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const permission = workbook?.getWorkbookPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
const snapshots: WorkbookPermissionSnapshot[] = [];
|
||||
const subscription = permission.permission$.subscribe((snapshot) => {
|
||||
snapshots.push(snapshot);
|
||||
});
|
||||
|
||||
// Initial snapshot
|
||||
expect(snapshots.length).toBeGreaterThan(0);
|
||||
|
||||
// Change permission
|
||||
await permission.setPoint(WorkbookPermissionPoint.Edit, false);
|
||||
|
||||
// Should have emitted new snapshot
|
||||
expect(snapshots.length).toBeGreaterThan(1);
|
||||
expect(snapshots[snapshots.length - 1][WorkbookPermissionPoint.Edit]).toBe(false);
|
||||
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
|
||||
it('should use subscribe() compatibility method', () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const permission = workbook?.getWorkbookPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
let snapshotReceived = false;
|
||||
const unsubscribe = permission.subscribe((snapshot) => {
|
||||
expect(snapshot).toBeDefined();
|
||||
expect(snapshot[WorkbookPermissionPoint.Edit]).toBeDefined();
|
||||
snapshotReceived = true;
|
||||
});
|
||||
|
||||
expect(snapshotReceived).toBe(true);
|
||||
unsubscribe();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Permission Points Coverage', () => {
|
||||
it('should handle all workbook permission points', async () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const permission = workbook?.getWorkbookPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
const pointsToTest = [
|
||||
WorkbookPermissionPoint.Edit,
|
||||
WorkbookPermissionPoint.View,
|
||||
WorkbookPermissionPoint.Print,
|
||||
WorkbookPermissionPoint.Export,
|
||||
WorkbookPermissionPoint.CopyContent,
|
||||
];
|
||||
|
||||
for (const point of pointsToTest) {
|
||||
await permission.setPoint(point, false);
|
||||
const value = permission.getPoint(point);
|
||||
expect(value).toBe(false);
|
||||
|
||||
await permission.setPoint(point, true);
|
||||
const valueAfter = permission.getPoint(point);
|
||||
expect(valueAfter).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Permission Change Listener', () => {
|
||||
it('should listen to permission service updates and emit pointChange$', async () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const permission = workbook?.getWorkbookPermission();
|
||||
|
||||
if (!permission || !workbook) {
|
||||
throw new Error('Permission or workbook is null');
|
||||
}
|
||||
|
||||
const changes: Array<{
|
||||
point: WorkbookPermissionPoint;
|
||||
value: boolean;
|
||||
oldValue: boolean;
|
||||
}> = [];
|
||||
|
||||
const subscription = permission.pointChange$.subscribe((change) => {
|
||||
changes.push(change);
|
||||
});
|
||||
|
||||
// Change a permission point, which should trigger the listener
|
||||
await permission.setPoint(WorkbookPermissionPoint.Edit, false);
|
||||
await permission.setPoint(WorkbookPermissionPoint.Print, false);
|
||||
|
||||
// Wait a bit for async updates
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
// Should have captured the changes
|
||||
expect(changes.length).toBeGreaterThanOrEqual(2);
|
||||
expect(changes.some((c) => c.point === WorkbookPermissionPoint.Edit)).toBe(true);
|
||||
expect(changes.some((c) => c.point === WorkbookPermissionPoint.Print)).toBe(true);
|
||||
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
|
||||
it('should update snapshot when permission service emits changes', async () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const permission = workbook?.getWorkbookPermission();
|
||||
|
||||
if (!permission || !workbook) {
|
||||
throw new Error('Permission or workbook is null');
|
||||
}
|
||||
|
||||
const snapshots: WorkbookPermissionPoint[][] = [];
|
||||
const subscription = permission.permission$.subscribe((snapshot) => {
|
||||
const changedPoints = Object.keys(snapshot).filter(
|
||||
(key) => snapshot[key as WorkbookPermissionPoint] === false
|
||||
);
|
||||
snapshots.push(changedPoints as WorkbookPermissionPoint[]);
|
||||
});
|
||||
|
||||
const initialSnapshotCount = snapshots.length;
|
||||
|
||||
// Trigger permission change
|
||||
await permission.setPoint(WorkbookPermissionPoint.Export, false);
|
||||
|
||||
// Wait for async updates
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
// Should have received a new snapshot
|
||||
expect(snapshots.length).toBeGreaterThan(initialSnapshotCount);
|
||||
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
|
||||
it('should only react to permission changes for this workbook', async () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const permission = workbook?.getWorkbookPermission();
|
||||
|
||||
if (!permission || !workbook) {
|
||||
throw new Error('Permission or workbook is null');
|
||||
}
|
||||
|
||||
const changes: Array<{
|
||||
point: WorkbookPermissionPoint;
|
||||
value: boolean;
|
||||
oldValue: boolean;
|
||||
}> = [];
|
||||
|
||||
const subscription = permission.pointChange$.subscribe((change) => {
|
||||
changes.push(change);
|
||||
});
|
||||
|
||||
// Create a permission point for a different unitId (should be ignored)
|
||||
const differentUnitId = 'different-unit-id';
|
||||
const differentPermissionPoint = new WorkbookEditablePermission(differentUnitId);
|
||||
permissionService.addPermissionPoint(differentPermissionPoint);
|
||||
permissionService.updatePermissionPoint(differentPermissionPoint.id, false);
|
||||
|
||||
// Change permission for current workbook
|
||||
await permission.setPoint(WorkbookPermissionPoint.View, false);
|
||||
|
||||
// Wait for async updates
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
// Should only have changes for the current workbook
|
||||
expect(changes.every((c) => c.point === WorkbookPermissionPoint.View)).toBe(true);
|
||||
expect(changes.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
|
||||
it('should properly dispose subscriptions', () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const permission = workbook?.getWorkbookPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
// Dispose should not throw
|
||||
expect(() => permission.dispose()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Additional Coverage Tests', () => {
|
||||
it('should handle canEdit method', () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const permission = workbook?.getWorkbookPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
const canEdit = permission.canEdit();
|
||||
expect(typeof canEdit).toBe('boolean');
|
||||
});
|
||||
|
||||
it('should handle subscribe method and return unsubscribe function', () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const permission = workbook?.getWorkbookPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
let callCount = 0;
|
||||
const unsubscribe = permission.subscribe((snapshot) => {
|
||||
callCount++;
|
||||
expect(snapshot).toBeDefined();
|
||||
});
|
||||
|
||||
// Should be called at least once
|
||||
expect(callCount).toBeGreaterThan(0);
|
||||
|
||||
// Unsubscribe should work
|
||||
unsubscribe();
|
||||
});
|
||||
|
||||
it('should handle getSnapshot method', () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const permission = workbook?.getWorkbookPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
const snapshot = permission.getSnapshot();
|
||||
expect(snapshot).toBeDefined();
|
||||
expect(typeof snapshot[WorkbookPermissionPoint.View]).toBe('boolean');
|
||||
});
|
||||
|
||||
it('should handle setCollaborators method', async () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const permission = workbook?.getWorkbookPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
const collaborators = [
|
||||
{
|
||||
user: {
|
||||
userID: 'user1',
|
||||
name: 'User 1',
|
||||
avatar: '',
|
||||
anonymous: false,
|
||||
canBindAnonymous: false,
|
||||
} as IUser,
|
||||
role: 1,
|
||||
},
|
||||
{
|
||||
user: {
|
||||
userID: 'user2',
|
||||
name: 'User 2',
|
||||
avatar: '',
|
||||
anonymous: false,
|
||||
canBindAnonymous: false,
|
||||
} as IUser,
|
||||
role: 2,
|
||||
},
|
||||
];
|
||||
|
||||
await expect(permission.setCollaborators(collaborators)).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle addCollaborator method', async () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const permission = workbook?.getWorkbookPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
const user = {
|
||||
userID: 'user3',
|
||||
name: 'User 3',
|
||||
avatar: '',
|
||||
anonymous: false,
|
||||
canBindAnonymous: false,
|
||||
} as IUser;
|
||||
|
||||
await expect(permission.addCollaborator(user, 1)).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle updateCollaborator method', async () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const permission = workbook?.getWorkbookPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
const user: IUser = {
|
||||
userID: 'user1',
|
||||
name: 'User 1',
|
||||
avatar: '',
|
||||
anonymous: false,
|
||||
canBindAnonymous: false,
|
||||
phone: '',
|
||||
email: '',
|
||||
createTimestamp: 0,
|
||||
};
|
||||
|
||||
await expect(permission.updateCollaborator(user, 2)).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle removeCollaborator method', async () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const permission = workbook?.getWorkbookPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
await expect(permission.removeCollaborator('user1')).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle removeCollaborators method', async () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const permission = workbook?.getWorkbookPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
const userIds = ['user1', 'user2'];
|
||||
await expect(permission.removeCollaborators(userIds)).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle listCollaborators method', async () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const permission = workbook?.getWorkbookPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
const collaborators = await permission.listCollaborators();
|
||||
expect(Array.isArray(collaborators)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle multiple setPoint calls', async () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const permission = workbook?.getWorkbookPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
// Save original values
|
||||
const originalView = permission.getPoint(WorkbookPermissionPoint.View);
|
||||
const originalEdit = permission.getPoint(WorkbookPermissionPoint.Edit);
|
||||
const originalPrint = permission.getPoint(WorkbookPermissionPoint.Print);
|
||||
|
||||
// Test setPoint for various permission points
|
||||
await expect(permission.setPoint(WorkbookPermissionPoint.View, true)).resolves.not.toThrow();
|
||||
await expect(permission.setPoint(WorkbookPermissionPoint.Edit, false)).resolves.not.toThrow();
|
||||
await expect(permission.setPoint(WorkbookPermissionPoint.Print, true)).resolves.not.toThrow();
|
||||
|
||||
// Restore original values
|
||||
await permission.setPoint(WorkbookPermissionPoint.View, originalView);
|
||||
await permission.setPoint(WorkbookPermissionPoint.Edit, originalEdit);
|
||||
await permission.setPoint(WorkbookPermissionPoint.Print, originalPrint);
|
||||
});
|
||||
|
||||
it('should skip setPoint when value is unchanged', async () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const permission = workbook?.getWorkbookPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
// Get current value
|
||||
const currentValue = permission.getPoint(WorkbookPermissionPoint.View);
|
||||
|
||||
// Set same value again, should not cause error
|
||||
await expect(permission.setPoint(WorkbookPermissionPoint.View, currentValue)).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw error for invalid permission point', async () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const permission = workbook?.getWorkbookPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
// Try to set invalid point
|
||||
await expect(permission.setPoint('InvalidPoint' as WorkbookPermissionPoint, true)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should return default value for invalid getPoint call', () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const permission = workbook?.getWorkbookPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
// Try to get invalid point
|
||||
expect(() => permission.getPoint('InvalidPoint' as WorkbookPermissionPoint)).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,694 +0,0 @@
|
||||
/**
|
||||
* 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 { FUniver } from '@univerjs/core/facade';
|
||||
import type { IRangeProtectionRule } from '../permission-types';
|
||||
import { ICommandService } from '@univerjs/core';
|
||||
import {
|
||||
AddRangeProtectionMutation,
|
||||
DeleteRangeProtectionMutation,
|
||||
SetRangeProtectionMutation,
|
||||
} from '@univerjs/sheets';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { createFacadeTestBed } from '../../__tests__/create-test-bed';
|
||||
import { WorksheetPermissionPoint } from '../permission-types';
|
||||
|
||||
describe('Test FWorksheetPermission', () => {
|
||||
let get: Injector['get'];
|
||||
let univerAPI: FUniver;
|
||||
let commandService: ICommandService;
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createFacadeTestBed();
|
||||
get = testBed.get;
|
||||
univerAPI = testBed.univerAPI;
|
||||
commandService = get(ICommandService);
|
||||
|
||||
// Register commands
|
||||
commandService.registerCommand(AddRangeProtectionMutation);
|
||||
commandService.registerCommand(SetRangeProtectionMutation);
|
||||
commandService.registerCommand(DeleteRangeProtectionMutation);
|
||||
});
|
||||
|
||||
describe('Basic Operations', () => {
|
||||
it('should get worksheet permission instance', () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
expect(permission).toBeDefined();
|
||||
expect(permission?.getSnapshot).toBeDefined();
|
||||
});
|
||||
|
||||
it('should set and get permission points', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission || !worksheet) {
|
||||
throw new Error('Permission or worksheet is null');
|
||||
}
|
||||
|
||||
// Set Edit permission to false
|
||||
await permission.setPoint(WorksheetPermissionPoint.Edit, false);
|
||||
|
||||
let canEdit = permission.getPoint(WorksheetPermissionPoint.Edit);
|
||||
expect(canEdit).toBe(false);
|
||||
|
||||
// Set Edit permission to true
|
||||
await permission.setPoint(WorksheetPermissionPoint.Edit, true);
|
||||
canEdit = permission.getPoint(WorksheetPermissionPoint.Edit);
|
||||
expect(canEdit).toBe(true);
|
||||
});
|
||||
|
||||
it('should get complete permission snapshot', () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
const snapshot = permission.getSnapshot();
|
||||
|
||||
expect(snapshot).toBeDefined();
|
||||
expect(snapshot[WorksheetPermissionPoint.Edit]).toBeDefined();
|
||||
expect(snapshot[WorksheetPermissionPoint.View]).toBeDefined();
|
||||
});
|
||||
|
||||
it('should check if worksheet is editable', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
// Default should be editable
|
||||
expect(permission.canEdit()).toBe(true);
|
||||
|
||||
// Set to read-only
|
||||
await permission.setMode('readOnly');
|
||||
expect(permission.canEdit()).toBe(false);
|
||||
|
||||
// Set back to editable
|
||||
await permission.setMode('editable');
|
||||
expect(permission.canEdit()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mode Operations', () => {
|
||||
it('should set readOnly mode', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
await permission.setMode('readOnly');
|
||||
|
||||
const snapshot = permission.getSnapshot();
|
||||
expect(snapshot[WorksheetPermissionPoint.Edit]).toBe(false);
|
||||
expect(snapshot[WorksheetPermissionPoint.View]).toBe(true);
|
||||
});
|
||||
|
||||
it('should set editable mode', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
await permission.setMode('editable');
|
||||
|
||||
const snapshot = permission.getSnapshot();
|
||||
expect(snapshot[WorksheetPermissionPoint.Edit]).toBe(true);
|
||||
expect(snapshot[WorksheetPermissionPoint.View]).toBe(true);
|
||||
});
|
||||
|
||||
it('should set filterOnly mode', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
await permission.setMode('filterOnly');
|
||||
|
||||
const snapshot = permission.getSnapshot();
|
||||
expect(snapshot[WorksheetPermissionPoint.Edit]).toBe(false);
|
||||
expect(snapshot[WorksheetPermissionPoint.Filter]).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Shortcut Methods', () => {
|
||||
it('should set read-only using setReadOnly()', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
await permission.setReadOnly();
|
||||
|
||||
expect(permission.canEdit()).toBe(false);
|
||||
});
|
||||
|
||||
it('should set editable using setEditable()', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
await permission.setEditable();
|
||||
|
||||
expect(permission.canEdit()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cell-Level Permission Checks', () => {
|
||||
it('should check if cell can be edited', () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
// Default should allow editing
|
||||
const canEdit = permission.canEditCell(0, 0);
|
||||
expect(canEdit).toBeDefined();
|
||||
});
|
||||
|
||||
it('should check if cell can be viewed', () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
// Default should allow viewing
|
||||
const canView = permission.canViewCell(0, 0);
|
||||
expect(canView).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Range Protection', () => {
|
||||
it('should protect ranges', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission || !worksheet) {
|
||||
throw new Error('Permission or worksheet is null');
|
||||
}
|
||||
|
||||
const range = worksheet.getRange('A1:B2');
|
||||
|
||||
const rules = await permission.protectRanges([
|
||||
{
|
||||
ranges: [range],
|
||||
options: {
|
||||
name: 'Protected Area',
|
||||
allowEdit: false,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(rules).toBeDefined();
|
||||
expect(rules.length).toBe(1);
|
||||
expect(rules[0].options.name).toBe('Protected Area');
|
||||
});
|
||||
|
||||
it('should protect multiple ranges in batch', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission || !worksheet) {
|
||||
throw new Error('Permission or worksheet is null');
|
||||
}
|
||||
|
||||
const range1 = worksheet.getRange('A1:A10');
|
||||
const range2 = worksheet.getRange('B1:B10');
|
||||
const range3 = worksheet.getRange('C1:C10');
|
||||
|
||||
const rules = await permission.protectRanges([
|
||||
{ ranges: [range1], options: { name: 'Rule 1' } },
|
||||
{ ranges: [range2], options: { name: 'Rule 2' } },
|
||||
{ ranges: [range3], options: { name: 'Rule 3' } },
|
||||
]);
|
||||
|
||||
expect(rules.length).toBe(3);
|
||||
expect(rules[0].options.name).toBe('Rule 1');
|
||||
expect(rules[1].options.name).toBe('Rule 2');
|
||||
expect(rules[2].options.name).toBe('Rule 3');
|
||||
});
|
||||
|
||||
it('should unprotect rules', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission || !worksheet) {
|
||||
throw new Error('Permission or worksheet is null');
|
||||
}
|
||||
|
||||
const range = worksheet.getRange('A1:B2');
|
||||
|
||||
// Create protection rule
|
||||
const rules = await permission.protectRanges([
|
||||
{
|
||||
ranges: [range],
|
||||
options: { name: 'To be removed' },
|
||||
},
|
||||
]);
|
||||
|
||||
expect(rules.length).toBe(1);
|
||||
|
||||
// Remove the rule
|
||||
await permission.unprotectRules([rules[0].id]);
|
||||
|
||||
// Verify rule is removed
|
||||
const allRules = await permission.listRangeProtectionRules();
|
||||
const removedRule = allRules.find((r) => r.id === rules[0].id);
|
||||
expect(removedRule).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should list all range protection rules', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission || !worksheet) {
|
||||
throw new Error('Permission or worksheet is null');
|
||||
}
|
||||
|
||||
const range1 = worksheet.getRange('A1:A10');
|
||||
const range2 = worksheet.getRange('B1:B10');
|
||||
|
||||
await permission.protectRanges([
|
||||
{ ranges: [range1], options: { name: 'Rule A' } },
|
||||
{ ranges: [range2], options: { name: 'Rule B' } },
|
||||
]);
|
||||
|
||||
const allRules = await permission.listRangeProtectionRules();
|
||||
|
||||
expect(allRules.length).toBeGreaterThanOrEqual(2);
|
||||
const ruleNames = allRules.map((r) => r.options.name);
|
||||
expect(ruleNames).toContain('Rule A');
|
||||
expect(ruleNames).toContain('Rule B');
|
||||
});
|
||||
|
||||
it('should return correct ranges for protection rules', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission || !worksheet) {
|
||||
throw new Error('Permission or worksheet is null');
|
||||
}
|
||||
|
||||
const range1 = worksheet.getRange('C1:C5');
|
||||
const range2 = worksheet.getRange('D10:F15');
|
||||
|
||||
await permission.protectRanges([
|
||||
{ ranges: [range1], options: { name: 'Range Test 1' } },
|
||||
{ ranges: [range2], options: { name: 'Range Test 2' } },
|
||||
]);
|
||||
|
||||
const allRules = await permission.listRangeProtectionRules();
|
||||
|
||||
// Find our test rules
|
||||
const rule1 = allRules.find((r) => r.options.name === 'Range Test 1');
|
||||
const rule2 = allRules.find((r) => r.options.name === 'Range Test 2');
|
||||
|
||||
expect(rule1).toBeDefined();
|
||||
expect(rule2).toBeDefined();
|
||||
|
||||
// Verify rule1 has correct ranges
|
||||
if (rule1) {
|
||||
expect(rule1.ranges.length).toBe(1);
|
||||
const actualRange1 = rule1.ranges[0];
|
||||
expect(actualRange1.getA1Notation()).toBe('C1:C5');
|
||||
}
|
||||
|
||||
// Verify rule2 has correct ranges
|
||||
if (rule2) {
|
||||
expect(rule2.ranges.length).toBe(1);
|
||||
const actualRange2 = rule2.ranges[0];
|
||||
expect(actualRange2.getA1Notation()).toBe('D10:F15');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Debug Utilities', () => {
|
||||
it('should debug cell permission', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission || !worksheet) {
|
||||
throw new Error('Permission or worksheet is null');
|
||||
}
|
||||
|
||||
const range = worksheet.getRange('A1:B2');
|
||||
|
||||
// Create protection rule
|
||||
await permission.protectRanges([
|
||||
{
|
||||
ranges: [range],
|
||||
options: { name: 'Debug Test' },
|
||||
},
|
||||
]);
|
||||
|
||||
// Debug cell A1 (should hit the rule)
|
||||
const debugInfo = permission.debugCellPermission(0, 0);
|
||||
|
||||
expect(debugInfo).toBeDefined();
|
||||
if (debugInfo) {
|
||||
expect(debugInfo.row).toBe(0);
|
||||
expect(debugInfo.col).toBe(0);
|
||||
expect(debugInfo.hitRules.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('should return undefined for unprotected cell', () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
// Debug cell far away (Z99)
|
||||
const debugInfo = permission.debugCellPermission(98, 25);
|
||||
|
||||
// Should be undefined or empty if no rules hit this cell
|
||||
if (debugInfo) {
|
||||
expect(debugInfo.hitRules.length).toBe(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Reactive Streams', () => {
|
||||
it('should emit current permission snapshot on subscribe', () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
let snapshotReceived = false;
|
||||
const subscription = permission.permission$.subscribe((snapshot) => {
|
||||
expect(snapshot).toBeDefined();
|
||||
expect(snapshot[WorksheetPermissionPoint.Edit]).toBeDefined();
|
||||
snapshotReceived = true;
|
||||
});
|
||||
|
||||
expect(snapshotReceived).toBe(true);
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
|
||||
it('should emit range protection changes', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission || !worksheet) {
|
||||
throw new Error('Permission or worksheet is null');
|
||||
}
|
||||
|
||||
const changes: Array<{ type: 'add' | 'update' | 'delete'; rules: IRangeProtectionRule[] }> = [];
|
||||
const subscription = permission.rangeProtectionChange$.subscribe((change) => {
|
||||
changes.push(change);
|
||||
});
|
||||
|
||||
const range = worksheet.getRange('A1:A10');
|
||||
await permission.protectRanges([
|
||||
{ ranges: [range], options: { name: 'Test Rule' } },
|
||||
]);
|
||||
|
||||
// Should have emitted change
|
||||
expect(changes.length).toBeGreaterThan(0);
|
||||
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
|
||||
it('should emit current rules list on subscribe', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission || !worksheet) {
|
||||
throw new Error('Permission or worksheet is null');
|
||||
}
|
||||
|
||||
const range = worksheet.getRange('A1:A10');
|
||||
await permission.protectRanges([
|
||||
{ ranges: [range], options: { name: 'Existing Rule' } },
|
||||
]);
|
||||
|
||||
let rulesReceived = false;
|
||||
const subscription = permission.rangeProtectionRules$.subscribe((rules) => {
|
||||
expect(rules).toBeDefined();
|
||||
expect(Array.isArray(rules)).toBe(true);
|
||||
rulesReceived = true;
|
||||
});
|
||||
|
||||
expect(rulesReceived).toBe(true);
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyConfig', () => {
|
||||
it('should apply mode configuration', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
await permission.applyConfig({
|
||||
mode: 'readOnly',
|
||||
});
|
||||
|
||||
expect(permission.getPoint(WorksheetPermissionPoint.View)).toBe(true);
|
||||
expect(permission.getPoint(WorksheetPermissionPoint.Edit)).toBe(false);
|
||||
});
|
||||
|
||||
it('should apply permission points configuration', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
await permission.applyConfig({
|
||||
points: {
|
||||
[WorksheetPermissionPoint.Edit]: false,
|
||||
[WorksheetPermissionPoint.Sort]: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(permission.getPoint(WorksheetPermissionPoint.Edit)).toBe(false);
|
||||
expect(permission.getPoint(WorksheetPermissionPoint.Sort)).toBe(true);
|
||||
});
|
||||
|
||||
it('should apply range protections configuration', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission || !worksheet) {
|
||||
throw new Error('Permission or worksheet is null');
|
||||
}
|
||||
|
||||
await permission.applyConfig({
|
||||
rangeProtections: [
|
||||
{
|
||||
rangeRefs: ['A1:A5'],
|
||||
options: { name: 'Protected A' },
|
||||
},
|
||||
{
|
||||
rangeRefs: ['B1:B5', 'C1:C5'],
|
||||
options: { name: 'Protected B&C', allowEdit: true },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const rules = await permission.listRangeProtectionRules();
|
||||
expect(rules.length).toBe(2);
|
||||
|
||||
const ruleA = rules.find((r) => r.options.name === 'Protected A');
|
||||
const ruleBC = rules.find((r) => r.options.name === 'Protected B&C');
|
||||
|
||||
expect(ruleA).toBeDefined();
|
||||
expect(ruleA?.ranges.length).toBe(1);
|
||||
expect(ruleA?.ranges[0].getA1Notation()).toBe('A1:A5');
|
||||
|
||||
expect(ruleBC).toBeDefined();
|
||||
expect(ruleBC?.ranges.length).toBe(2);
|
||||
expect(ruleBC?.ranges[0].getA1Notation()).toBe('B1:B5');
|
||||
expect(ruleBC?.ranges[1].getA1Notation()).toBe('C1:C5');
|
||||
});
|
||||
|
||||
it('should apply complete configuration with all fields', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission || !worksheet) {
|
||||
throw new Error('Permission or worksheet is null');
|
||||
}
|
||||
|
||||
await permission.applyConfig({
|
||||
mode: 'editable',
|
||||
points: {
|
||||
[WorksheetPermissionPoint.InsertRow]: false,
|
||||
},
|
||||
rangeProtections: [
|
||||
{
|
||||
rangeRefs: ['D1:D10'],
|
||||
options: { name: 'Formula Column' },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Check mode applied
|
||||
expect(permission.getPoint(WorksheetPermissionPoint.Edit)).toBe(true);
|
||||
|
||||
// Check points override
|
||||
expect(permission.getPoint(WorksheetPermissionPoint.InsertRow)).toBe(false);
|
||||
|
||||
// Check range protection
|
||||
const rules = await permission.listRangeProtectionRules();
|
||||
const formulaRule = rules.find((r) => r.options.name === 'Formula Column');
|
||||
expect(formulaRule).toBeDefined();
|
||||
expect(formulaRule?.ranges[0].getA1Notation()).toBe('D1:D10');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Additional Coverage Tests', () => {
|
||||
it('should throw error when protectRanges is called with empty configs', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
// Try to protect with empty configs
|
||||
await expect(permission.protectRanges([])).rejects.toThrow('Configs cannot be empty');
|
||||
});
|
||||
|
||||
it('should handle subscribe method and return unsubscribe function', () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
let callCount = 0;
|
||||
const unsubscribe = permission.subscribe((snapshot) => {
|
||||
callCount++;
|
||||
expect(snapshot).toBeDefined();
|
||||
});
|
||||
|
||||
// Should be called at least once
|
||||
expect(callCount).toBeGreaterThan(0);
|
||||
|
||||
// Unsubscribe should work
|
||||
unsubscribe();
|
||||
});
|
||||
|
||||
it('should handle getSnapshot method', () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
const snapshot = permission.getSnapshot();
|
||||
expect(snapshot).toBeDefined();
|
||||
expect(typeof snapshot[WorksheetPermissionPoint.View]).toBe('boolean');
|
||||
});
|
||||
|
||||
it('should handle multiple setPoint calls with same value', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
// Get current value
|
||||
const currentValue = permission.getPoint(WorksheetPermissionPoint.Edit);
|
||||
|
||||
// Set same value again, should not cause error
|
||||
await expect(permission.setPoint(WorksheetPermissionPoint.Edit, currentValue)).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw error for invalid worksheet permission point in setPoint', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
// Try to set invalid point
|
||||
await expect(permission.setPoint('InvalidPoint' as WorksheetPermissionPoint, true)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should throw error for invalid worksheet permission point in getPoint', () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
// Try to get invalid point
|
||||
expect(() => permission.getPoint('InvalidPoint' as WorksheetPermissionPoint)).toThrow();
|
||||
});
|
||||
|
||||
it('should handle unprotectRules with empty array', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
// Should not throw when called with empty array
|
||||
await expect(permission.unprotectRules([])).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle dispose method', () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
// Dispose should not throw
|
||||
expect(() => permission.dispose()).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,489 +0,0 @@
|
||||
/**
|
||||
* 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 { FUniver } from '@univerjs/core/facade';
|
||||
import { ICommandService } from '@univerjs/core';
|
||||
import {
|
||||
AddRangeProtectionMutation,
|
||||
DeleteRangeProtectionMutation,
|
||||
RangeProtectionRuleModel,
|
||||
SetRangeProtectionMutation,
|
||||
} from '@univerjs/sheets';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { map, take } from 'rxjs/operators';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { createFacadeTestBed } from '../../__tests__/create-test-bed';
|
||||
import { WorkbookPermissionPoint, WorksheetPermissionPoint } from '../permission-types';
|
||||
|
||||
describe('Test Permission Combination Logic', () => {
|
||||
let get: Injector['get'];
|
||||
let univerAPI: FUniver;
|
||||
let commandService: ICommandService;
|
||||
let rangeProtectionRuleModel: RangeProtectionRuleModel;
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createFacadeTestBed();
|
||||
get = testBed.get;
|
||||
univerAPI = testBed.univerAPI;
|
||||
commandService = get(ICommandService);
|
||||
rangeProtectionRuleModel = get(RangeProtectionRuleModel);
|
||||
|
||||
// Register commands
|
||||
commandService.registerCommand(AddRangeProtectionMutation);
|
||||
commandService.registerCommand(SetRangeProtectionMutation);
|
||||
commandService.registerCommand(DeleteRangeProtectionMutation);
|
||||
});
|
||||
|
||||
describe('Hierarchical Permission Combination', () => {
|
||||
it('should respect workbook-level restrictions', async () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const worksheet = workbook?.getActiveSheet();
|
||||
|
||||
if (!workbook || !worksheet) {
|
||||
throw new Error('Workbook or worksheet is null');
|
||||
}
|
||||
|
||||
const workbookPermission = workbook.getWorkbookPermission();
|
||||
const worksheetPermission = worksheet.getWorksheetPermission();
|
||||
|
||||
// Set workbook to read-only
|
||||
await workbookPermission.setMode('viewer');
|
||||
|
||||
// Even if worksheet allows editing, workbook restriction should apply
|
||||
await worksheetPermission.setMode('editable');
|
||||
|
||||
// Workbook level should be restricted
|
||||
expect(workbookPermission.getPoint(WorkbookPermissionPoint.Edit)).toBe(false);
|
||||
|
||||
// Worksheet may show as editable, but in practice workbook-level restriction applies
|
||||
expect(worksheetPermission.getPoint(WorksheetPermissionPoint.Edit)).toBe(true);
|
||||
});
|
||||
|
||||
it('should combine workbook and worksheet permissions', async () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const worksheet = workbook?.getActiveSheet();
|
||||
|
||||
if (!workbook || !worksheet) {
|
||||
throw new Error('Workbook or worksheet is null');
|
||||
}
|
||||
|
||||
const workbookPermission = workbook.getWorkbookPermission();
|
||||
const worksheetPermission = worksheet.getWorksheetPermission();
|
||||
|
||||
// Both allow editing
|
||||
await workbookPermission.setMode('editor');
|
||||
await worksheetPermission.setMode('editable');
|
||||
|
||||
expect(workbookPermission.getPoint(WorkbookPermissionPoint.Edit)).toBe(true);
|
||||
expect(worksheetPermission.getPoint(WorksheetPermissionPoint.Edit)).toBe(true);
|
||||
|
||||
// Set worksheet to read-only
|
||||
await worksheetPermission.setMode('readOnly');
|
||||
|
||||
// Workbook still allows, but worksheet restricts
|
||||
expect(workbookPermission.getPoint(WorkbookPermissionPoint.Edit)).toBe(true);
|
||||
expect(worksheetPermission.getPoint(WorksheetPermissionPoint.Edit)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle three-level permission hierarchy', async () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const worksheet = workbook?.getActiveSheet();
|
||||
|
||||
if (!workbook || !worksheet) {
|
||||
throw new Error('Workbook or worksheet is null');
|
||||
}
|
||||
|
||||
const workbookPermission = workbook.getWorkbookPermission();
|
||||
const worksheetPermission = worksheet.getWorksheetPermission();
|
||||
|
||||
// Set all levels to editable
|
||||
await workbookPermission.setMode('editor');
|
||||
await worksheetPermission.setMode('editable');
|
||||
|
||||
const range = worksheet.getRange('A1:B2');
|
||||
const rangePermission = range.getRangePermission();
|
||||
|
||||
if (!rangePermission) {
|
||||
throw new Error('Range permission is null');
|
||||
}
|
||||
|
||||
// Initially all should allow editing
|
||||
expect(workbookPermission.getPoint(WorkbookPermissionPoint.Edit)).toBe(true);
|
||||
expect(worksheetPermission.getPoint(WorksheetPermissionPoint.Edit)).toBe(true);
|
||||
expect(rangePermission.canEdit()).toBe(true);
|
||||
|
||||
// Protect the range
|
||||
await rangePermission.protect({
|
||||
name: 'Protected Area',
|
||||
allowEdit: false,
|
||||
});
|
||||
|
||||
// Range should now be protected
|
||||
expect(rangePermission.isProtected()).toBe(true);
|
||||
expect(rangePermission.canEdit()).toBe(false);
|
||||
|
||||
// But workbook and worksheet should still allow editing
|
||||
expect(workbookPermission.getPoint(WorkbookPermissionPoint.Edit)).toBe(true);
|
||||
expect(worksheetPermission.getPoint(WorksheetPermissionPoint.Edit)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cell-Level Permission Checks', () => {
|
||||
it('should check cell permissions with range protection', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const worksheetPermission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!worksheet || !worksheetPermission) {
|
||||
throw new Error('Worksheet or permission is null');
|
||||
}
|
||||
|
||||
// Protect A1:B2
|
||||
const range = worksheet.getRange('A1:B2');
|
||||
await range.getRangePermission()?.protect({
|
||||
name: 'Protected Area',
|
||||
allowEdit: false,
|
||||
});
|
||||
|
||||
// Check cell A1 (should be protected)
|
||||
const canEditA1 = worksheetPermission.canEditCell(0, 0);
|
||||
expect(canEditA1).toBe(false);
|
||||
|
||||
// Check cell C3 (should be editable - outside protected range)
|
||||
const canEditC3 = worksheetPermission.canEditCell(2, 2);
|
||||
expect(canEditC3).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle overlapping protection rules', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const worksheetPermission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!worksheet || !worksheetPermission) {
|
||||
throw new Error('Worksheet or permission is null');
|
||||
}
|
||||
|
||||
// Create separate non-overlapping protected ranges
|
||||
const range1 = worksheet.getRange('A1:A10');
|
||||
const range2 = worksheet.getRange('B1:B10');
|
||||
|
||||
await worksheetPermission.protectRanges([
|
||||
{ ranges: [range1], options: { name: 'Column A', allowEdit: false } },
|
||||
{ ranges: [range2], options: { name: 'Column B', allowEdit: false } },
|
||||
]);
|
||||
|
||||
// Check cells in protected columns
|
||||
expect(worksheetPermission.canEditCell(0, 0)).toBe(false); // A1
|
||||
expect(worksheetPermission.canEditCell(0, 1)).toBe(false); // B1
|
||||
|
||||
// Check cell in unprotected column
|
||||
expect(worksheetPermission.canEditCell(0, 2)).toBe(true); // C1
|
||||
});
|
||||
|
||||
it('should debug cell permission with multiple rules', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const worksheetPermission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!worksheet || !worksheetPermission) {
|
||||
throw new Error('Worksheet or permission is null');
|
||||
}
|
||||
|
||||
// Create multiple protection rules
|
||||
await worksheetPermission.protectRanges([
|
||||
{
|
||||
ranges: [worksheet.getRange('A1:C3')],
|
||||
options: { name: 'Area 1', allowEdit: false },
|
||||
},
|
||||
{
|
||||
ranges: [worksheet.getRange('D1:E2')],
|
||||
options: { name: 'Area 2', allowEdit: false },
|
||||
},
|
||||
]);
|
||||
|
||||
// Debug cell A1 (should hit Area 1)
|
||||
const debugA1 = worksheetPermission.debugCellPermission(0, 0);
|
||||
expect(debugA1).toBeDefined();
|
||||
if (debugA1) {
|
||||
expect(debugA1.hitRules.length).toBeGreaterThan(0);
|
||||
const ruleNames = debugA1.hitRules.map((r) => r.options.name);
|
||||
expect(ruleNames).toContain('Area 1');
|
||||
}
|
||||
|
||||
// Debug cell D1 (should hit Area 2)
|
||||
const debugD1 = worksheetPermission.debugCellPermission(0, 3);
|
||||
expect(debugD1).toBeDefined();
|
||||
if (debugD1) {
|
||||
expect(debugD1.hitRules.length).toBeGreaterThan(0);
|
||||
const ruleNames = debugD1.hitRules.map((r) => r.options.name);
|
||||
expect(ruleNames).toContain('Area 2');
|
||||
}
|
||||
|
||||
// Debug cell Z99 (should hit no rules)
|
||||
const debugZ99 = worksheetPermission.debugCellPermission(98, 25);
|
||||
if (debugZ99) {
|
||||
expect(debugZ99.hitRules.length).toBe(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Batch Operations', () => {
|
||||
it('should create multiple protection rules in one batch', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const worksheetPermission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!worksheet || !worksheetPermission) {
|
||||
throw new Error('Worksheet or permission is null');
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
// Batch create 5 rules
|
||||
const rules = await worksheetPermission.protectRanges([
|
||||
{ ranges: [worksheet.getRange('A1:A10')], options: { name: 'Rule 1' } },
|
||||
{ ranges: [worksheet.getRange('B1:B10')], options: { name: 'Rule 2' } },
|
||||
{ ranges: [worksheet.getRange('C1:C10')], options: { name: 'Rule 3' } },
|
||||
{ ranges: [worksheet.getRange('D1:D10')], options: { name: 'Rule 4' } },
|
||||
{ ranges: [worksheet.getRange('E1:E10')], options: { name: 'Rule 5' } },
|
||||
]);
|
||||
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
// Should create 5 rules
|
||||
expect(rules.length).toBe(5);
|
||||
|
||||
// Should be reasonably fast (batch operation)
|
||||
// This is a rough check - in real scenario, batch should be much faster than individual
|
||||
expect(duration).toBeLessThan(5000); // 5 seconds max for test environment
|
||||
|
||||
// Verify all rules exist
|
||||
const allRules = await worksheetPermission.listRangeProtectionRules();
|
||||
const ruleNames = allRules.map((r) => r.options.name);
|
||||
expect(ruleNames).toContain('Rule 1');
|
||||
expect(ruleNames).toContain('Rule 2');
|
||||
expect(ruleNames).toContain('Rule 3');
|
||||
expect(ruleNames).toContain('Rule 4');
|
||||
expect(ruleNames).toContain('Rule 5');
|
||||
});
|
||||
|
||||
it('should batch delete protection rules', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const worksheetPermission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!worksheet || !worksheetPermission) {
|
||||
throw new Error('Worksheet or permission is null');
|
||||
}
|
||||
|
||||
// Create 3 rules
|
||||
const rules = await worksheetPermission.protectRanges([
|
||||
{ ranges: [worksheet.getRange('A1:A10')], options: { name: 'To Delete 1' } },
|
||||
{ ranges: [worksheet.getRange('B1:B10')], options: { name: 'To Delete 2' } },
|
||||
{ ranges: [worksheet.getRange('C1:C10')], options: { name: 'To Delete 3' } },
|
||||
]);
|
||||
|
||||
const ruleIds = rules.map((r) => r.id);
|
||||
|
||||
// Batch delete
|
||||
await worksheetPermission.unprotectRules(ruleIds);
|
||||
|
||||
// Verify all deleted
|
||||
const remainingRules = await worksheetPermission.listRangeProtectionRules();
|
||||
const remainingIds = remainingRules.map((r) => r.id);
|
||||
|
||||
for (const id of ruleIds) {
|
||||
expect(remainingIds).not.toContain(id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Reactive Streams Combination', () => {
|
||||
it('should combine multiple permission streams', async () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const worksheet = workbook?.getActiveSheet();
|
||||
|
||||
if (!workbook || !worksheet) {
|
||||
throw new Error('Workbook or worksheet is null');
|
||||
}
|
||||
|
||||
const workbookPermission = workbook.getWorkbookPermission();
|
||||
const worksheetPermission = worksheet.getWorksheetPermission();
|
||||
|
||||
// Combine workbook and worksheet permission streams
|
||||
const combined$ = combineLatest([
|
||||
workbookPermission.permission$,
|
||||
worksheetPermission.permission$,
|
||||
]).pipe(
|
||||
map(([workbookSnapshot, worksheetSnapshot]) => ({
|
||||
workbookEdit: workbookSnapshot[WorkbookPermissionPoint.Edit],
|
||||
worksheetEdit: worksheetSnapshot[WorksheetPermissionPoint.Edit],
|
||||
})),
|
||||
take(1)
|
||||
);
|
||||
|
||||
const result = await combined$.toPromise();
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.workbookEdit).toBeDefined();
|
||||
expect(result?.worksheetEdit).toBeDefined();
|
||||
});
|
||||
|
||||
it('should monitor range protection changes reactively', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const worksheetPermission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!worksheet || !worksheetPermission) {
|
||||
throw new Error('Worksheet or permission is null');
|
||||
}
|
||||
|
||||
const changes: any[] = [];
|
||||
const subscription = worksheetPermission.rangeProtectionChange$.subscribe((change) => {
|
||||
changes.push(change);
|
||||
});
|
||||
|
||||
// Create protection
|
||||
const range = worksheet.getRange('A1:A10');
|
||||
await range.getRangePermission()?.protect({ name: 'Test' });
|
||||
|
||||
// Should have emitted change
|
||||
expect(changes.length).toBeGreaterThan(0);
|
||||
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
|
||||
it('should track current rules list reactively', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const worksheetPermission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!worksheet || !worksheetPermission) {
|
||||
throw new Error('Worksheet or permission is null');
|
||||
}
|
||||
|
||||
// Subscribe to rules list
|
||||
const rulesLists: any[][] = [];
|
||||
const subscription = worksheetPermission.rangeProtectionRules$.subscribe((rules) => {
|
||||
rulesLists.push([...rules]);
|
||||
});
|
||||
|
||||
// Initial should be received
|
||||
expect(rulesLists.length).toBeGreaterThan(0);
|
||||
|
||||
// Add a rule
|
||||
await worksheetPermission.protectRanges([
|
||||
{ ranges: [worksheet.getRange('A1:A10')], options: { name: 'New Rule' } },
|
||||
]);
|
||||
|
||||
// Should have received updated list
|
||||
expect(rulesLists.length).toBeGreaterThan(1);
|
||||
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mode Transitions', () => {
|
||||
it('should transition through different workbook modes', async () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const permission = workbook?.getWorkbookPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
// Owner -> Editor
|
||||
await permission.setMode('owner');
|
||||
expect(permission.getPoint(WorkbookPermissionPoint.ManageCollaborator)).toBe(true);
|
||||
|
||||
await permission.setMode('editor');
|
||||
expect(permission.getPoint(WorkbookPermissionPoint.Edit)).toBe(true);
|
||||
expect(permission.getPoint(WorkbookPermissionPoint.ManageCollaborator)).toBe(false);
|
||||
|
||||
// Editor -> Viewer
|
||||
await permission.setMode('viewer');
|
||||
expect(permission.getPoint(WorkbookPermissionPoint.Edit)).toBe(false);
|
||||
expect(permission.getPoint(WorkbookPermissionPoint.View)).toBe(true);
|
||||
|
||||
// Viewer -> Owner
|
||||
await permission.setMode('owner');
|
||||
expect(permission.getPoint(WorkbookPermissionPoint.Edit)).toBe(true);
|
||||
expect(permission.getPoint(WorkbookPermissionPoint.ManageCollaborator)).toBe(true);
|
||||
});
|
||||
|
||||
it('should transition through worksheet modes', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const permission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
// Editable -> ReadOnly
|
||||
await permission.setMode('editable');
|
||||
expect(permission.canEdit()).toBe(true);
|
||||
|
||||
await permission.setMode('readOnly');
|
||||
expect(permission.canEdit()).toBe(false);
|
||||
|
||||
// ReadOnly -> FilterOnly
|
||||
await permission.setMode('filterOnly');
|
||||
expect(permission.getPoint(WorksheetPermissionPoint.Edit)).toBe(false);
|
||||
expect(permission.getPoint(WorksheetPermissionPoint.Filter)).toBe(true);
|
||||
|
||||
// FilterOnly -> Editable
|
||||
await permission.setMode('editable');
|
||||
expect(permission.canEdit()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty protection rules list', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const worksheetPermission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!worksheetPermission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
const rules = await worksheetPermission.listRangeProtectionRules();
|
||||
|
||||
// Should return empty array, not undefined
|
||||
expect(Array.isArray(rules)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle checking permissions on non-existent cells', () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const worksheetPermission = worksheet?.getWorksheetPermission();
|
||||
|
||||
if (!worksheetPermission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
// Check very large row/column numbers
|
||||
const canEdit = worksheetPermission.canEditCell(9999, 9999);
|
||||
expect(typeof canEdit).toBe('boolean');
|
||||
});
|
||||
|
||||
it('should handle unprotecting already unprotected range', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
const range = worksheet?.getRange('Z99:Z99');
|
||||
const permission = range?.getRangePermission();
|
||||
|
||||
if (!permission) {
|
||||
throw new Error('Permission is null');
|
||||
}
|
||||
|
||||
// Should not throw error
|
||||
await expect(permission.unprotect()).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -20,16 +20,15 @@ import type { Observable, Subscription } from 'rxjs';
|
||||
import type { FRange } from '../f-range';
|
||||
import type { FWorksheet } from '../f-worksheet';
|
||||
import type {
|
||||
IRangeProtectionRule as IFRangeProtectionRule,
|
||||
IRangePermission,
|
||||
IRangeProtectionOptions,
|
||||
RangePermissionSnapshot,
|
||||
} from './permission-types';
|
||||
import { IAuthzIoService, ICommandService, Inject, Injector, IPermissionService } from '@univerjs/core';
|
||||
import { UnitRole } from '@univerjs/protocol';
|
||||
import { AddRangeProtectionMutation, DeleteRangeProtectionMutation, EditStateEnum, RangeProtectionRuleModel, UnitObject, ViewStateEnum } from '@univerjs/sheets';
|
||||
import { EditStateEnum, RangeProtectionRuleModel, ViewStateEnum } from '@univerjs/sheets';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { distinctUntilChanged, filter, map, shareReplay } from 'rxjs/operators';
|
||||
import { FPermission } from '../f-permission';
|
||||
import { FRangeProtectionRule } from './f-range-protection-rule';
|
||||
import { RANGE_PERMISSION_POINT_MAP } from './permission-point-map';
|
||||
import { RangePermissionPoint } from './permission-types';
|
||||
@@ -40,9 +39,10 @@ import { RangePermissionPoint } from './permission-types';
|
||||
*
|
||||
* @hideconstructor
|
||||
*/
|
||||
export class FRangePermission implements IRangePermission {
|
||||
export class FRangePermission {
|
||||
private readonly _permissionSubject: BehaviorSubject<RangePermissionSnapshot>;
|
||||
private readonly _subscriptions: Subscription[] = [];
|
||||
private readonly _fPermission: FPermission;
|
||||
|
||||
/**
|
||||
* Observable stream of permission snapshot changes
|
||||
@@ -56,7 +56,7 @@ export class FRangePermission implements IRangePermission {
|
||||
*/
|
||||
readonly protectionChange$: Observable<{
|
||||
type: 'protected';
|
||||
rule: IFRangeProtectionRule;
|
||||
rule: FRangeProtectionRule;
|
||||
} | {
|
||||
type: 'unprotected';
|
||||
ruleId: string;
|
||||
@@ -73,6 +73,9 @@ export class FRangePermission implements IRangePermission {
|
||||
@Inject(ICommandService) private readonly _commandService: ICommandService,
|
||||
@Inject(RangeProtectionRuleModel) private readonly _rangeProtectionRuleModel: RangeProtectionRuleModel
|
||||
) {
|
||||
// Initialize FPermission instance
|
||||
this._fPermission = this._injector.createInstance(FPermission);
|
||||
|
||||
this._permissionSubject = new BehaviorSubject<RangePermissionSnapshot>(this._buildSnapshot());
|
||||
|
||||
// Create permission$ stream from IPermissionService
|
||||
@@ -113,7 +116,7 @@ export class FRangePermission implements IRangePermission {
|
||||
*/
|
||||
private _createProtectionChangeStream(): Observable<{
|
||||
type: 'protected';
|
||||
rule: IFRangeProtectionRule;
|
||||
rule: FRangeProtectionRule;
|
||||
} | {
|
||||
type: 'unprotected';
|
||||
ruleId: string;
|
||||
@@ -173,7 +176,7 @@ export class FRangePermission implements IRangePermission {
|
||||
/**
|
||||
* Create a Facade rule from internal rule
|
||||
*/
|
||||
private _createFacadeRule(rule: IRangeProtectionRule): IFRangeProtectionRule {
|
||||
private _createFacadeRule(rule: IRangeProtectionRule): FRangeProtectionRule {
|
||||
const ranges = rule.ranges.map((range) =>
|
||||
this._worksheet.getRange(
|
||||
range.startRow,
|
||||
@@ -219,7 +222,7 @@ export class FRangePermission implements IRangePermission {
|
||||
return false;
|
||||
}
|
||||
|
||||
// First try to get permission from protection rule
|
||||
// Try to get permission from protection rule
|
||||
const rule = this._getProtectionRule();
|
||||
if (rule) {
|
||||
const permissionPoint = new PermissionPointClass(this._unitId, this._subUnitId, rule.permissionId);
|
||||
@@ -229,16 +232,6 @@ export class FRangePermission implements IRangePermission {
|
||||
}
|
||||
}
|
||||
|
||||
// If no rule exists, try to get local-only permission point
|
||||
const localPermissionId = this._getLocalPermissionId();
|
||||
const localPermissionPoint = new PermissionPointClass(this._unitId, this._subUnitId, localPermissionId);
|
||||
const localPermission = this._permissionService.getPermissionPoint(localPermissionPoint.id);
|
||||
|
||||
// If local permission exists, return its value
|
||||
if (localPermission) {
|
||||
return localPermission.value;
|
||||
}
|
||||
|
||||
// Default to true (allowed) when no permission point is set
|
||||
// This aligns with worksheet-level permission behavior
|
||||
// If a range is not explicitly protected, it should be accessible
|
||||
@@ -348,19 +341,33 @@ export class FRangePermission implements IRangePermission {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a specific permission point for the range (low-level API for local runtime control).
|
||||
* This method directly sets the permission point value for the current range protection rule.
|
||||
* If no protection rule exists, it will create permission points without a rule (local-only mode).
|
||||
* Set a specific permission point for the range (low-level API).
|
||||
*
|
||||
* **Important:** This method only updates the permission point value for an existing protection rule.
|
||||
* It does NOT create permission checks that will block actual editing operations.
|
||||
* You must call `protect()` first to create a protection rule before using this method.
|
||||
*
|
||||
* This method is useful for:
|
||||
* - Fine-tuning permissions after creating a protection rule with `protect()`
|
||||
* - Dynamically adjusting permissions based on runtime conditions
|
||||
* - Advanced permission management scenarios
|
||||
*
|
||||
* @param {RangePermissionPoint} point The permission point to set.
|
||||
* @param {boolean} value The value to set (true = allowed, false = denied).
|
||||
* @returns {Promise<void>} A promise that resolves when the point is set.
|
||||
* @throws {Error} If no protection rule exists for this range.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const range = univerAPI.getActiveWorkbook()?.getActiveSheet()?.getRange('A1:B2');
|
||||
* const permission = range?.getRangePermission();
|
||||
* // Can set permission points without calling protect() first (local-only mode)
|
||||
* await permission?.setPoint(univerAPI.Enum.RangePermissionPoint.Edit, false); // Disable edit
|
||||
* await permission?.setPoint(univerAPI.Enum.RangePermissionPoint.View, true); // Enable view
|
||||
*
|
||||
* // First, create a protection rule
|
||||
* await permission?.protect({ name: 'My Range', allowEdit: true });
|
||||
*
|
||||
* // Then you can dynamically update permission points
|
||||
* await permission?.setPoint(univerAPI.Enum.RangePermissionPoint.Edit, false); // Now disable edit
|
||||
* await permission?.setPoint(univerAPI.Enum.RangePermissionPoint.View, true); // Ensure view is enabled
|
||||
* ```
|
||||
*/
|
||||
async setPoint(point: RangePermissionPoint, value: boolean): Promise<void> {
|
||||
@@ -369,93 +376,61 @@ export class FRangePermission implements IRangePermission {
|
||||
throw new Error(`Unknown permission point: ${point}`);
|
||||
}
|
||||
|
||||
// Must have a protection rule to set permission points
|
||||
const rule = this._getProtectionRule();
|
||||
if (!rule) {
|
||||
throw new Error('Cannot set permission point: No protection rule exists for this range. Call protect() first.');
|
||||
}
|
||||
|
||||
const oldValue = this.getPoint(point);
|
||||
if (oldValue === value) {
|
||||
return; // Value unchanged, no update needed
|
||||
}
|
||||
|
||||
// Get permissionId from rule, or use a local-only permissionId
|
||||
const rule = this._getProtectionRule();
|
||||
const permissionId = rule?.permissionId || this._getLocalPermissionId();
|
||||
const permissionId = rule.permissionId;
|
||||
|
||||
const permissionPoint = new PermissionPointClass(this._unitId, this._subUnitId, permissionId);
|
||||
const existingPoint = this._permissionService.getPermissionPoint(permissionPoint.id);
|
||||
|
||||
if (!existingPoint) {
|
||||
this._permissionService.addPermissionPoint(permissionPoint);
|
||||
}
|
||||
|
||||
this._permissionService.updatePermissionPoint(permissionPoint.id, value);
|
||||
// Use FPermission's setRangeProtectionPermissionPoint method
|
||||
this._fPermission.setRangeProtectionPermissionPoint(this._unitId, this._subUnitId, permissionId, PermissionPointClass, value);
|
||||
|
||||
// Update snapshot (the Observable stream will automatically emit the change)
|
||||
this._permissionSubject.next(this._buildSnapshot());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a local-only permission ID for this range (used when no protection rule exists)
|
||||
* @private
|
||||
*/
|
||||
private _getLocalPermissionId(): string {
|
||||
const range = this._range.getRange();
|
||||
return `local-${this._unitId}-${this._subUnitId}-${range.startRow}-${range.startColumn}-${range.endRow}-${range.endColumn}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Protect the current range.
|
||||
* @param {IRangeProtectionOptions} options Protection options.
|
||||
* @returns {Promise<IFRangeProtectionRule>} The created protection rule.
|
||||
* @returns {Promise<FRangeProtectionRule>} The created protection rule.
|
||||
* @example
|
||||
* ```ts
|
||||
* const range = univerAPI.getActiveWorkbook()?.getActiveSheet()?.getRange('A1:B2');
|
||||
* const permission = range?.getRangePermission();
|
||||
* const rule = await permission?.protect({
|
||||
* name: 'My protected range',
|
||||
* allowEdit: false,
|
||||
* allowView: true,
|
||||
* allowManageCollaborator: false,
|
||||
* allowDeleteRule: false
|
||||
* allowEdit: true,
|
||||
* allowedUsers: ['user1', 'user2'],
|
||||
* allowViewByOthers: false,
|
||||
* });
|
||||
* console.log(rule);
|
||||
* ```
|
||||
*/
|
||||
async protect(options?: IRangeProtectionOptions): Promise<IFRangeProtectionRule> {
|
||||
async protect(options?: IRangeProtectionOptions): Promise<FRangeProtectionRule> {
|
||||
if (this.isProtected()) {
|
||||
throw new Error('Range is already protected');
|
||||
}
|
||||
|
||||
// Create permissionId through authz service
|
||||
const permissionId = await this._authzIoService.create({
|
||||
objectType: UnitObject.SelectRange,
|
||||
selectRangeObject: {
|
||||
collaborators: options?.allowedUsers?.map((id) => ({ id, role: UnitRole.Editor, subject: undefined })) ?? [],
|
||||
unitID: this._unitId,
|
||||
name: options?.name || '',
|
||||
scope: undefined,
|
||||
},
|
||||
});
|
||||
// Use FPermission's addRangeBaseProtection method
|
||||
const result = await this._fPermission.addRangeBaseProtection(
|
||||
this._unitId,
|
||||
this._subUnitId,
|
||||
[this._range],
|
||||
options
|
||||
);
|
||||
|
||||
const ruleId = this._rangeProtectionRuleModel.createRuleId(this._unitId, this._subUnitId);
|
||||
const range = this._range.getRange();
|
||||
if (!result) {
|
||||
throw new Error('Failed to create range protection');
|
||||
}
|
||||
|
||||
// Determine view and edit states
|
||||
const viewState = this._determineViewState(options);
|
||||
const editState = this._determineEditState(options);
|
||||
|
||||
await this._commandService.executeCommand(AddRangeProtectionMutation.id, {
|
||||
unitId: this._unitId,
|
||||
subUnitId: this._subUnitId,
|
||||
rules: [{
|
||||
id: ruleId,
|
||||
permissionId,
|
||||
unitType: 3, // UnitObject.SelectRange
|
||||
unitId: this._unitId,
|
||||
subUnitId: this._subUnitId,
|
||||
ranges: [range],
|
||||
description: options?.name,
|
||||
viewState,
|
||||
editState,
|
||||
}],
|
||||
});
|
||||
const { permissionId, ruleId } = result;
|
||||
|
||||
// Set permission points for local runtime control
|
||||
await this._setPermissionPoints(permissionId, options);
|
||||
@@ -535,14 +510,8 @@ export class FRangePermission implements IRangePermission {
|
||||
return;
|
||||
}
|
||||
|
||||
const permissionPoint = new PermissionPointClass(this._unitId, this._subUnitId, permissionId);
|
||||
const existingPoint = this._permissionService.getPermissionPoint(permissionPoint.id);
|
||||
|
||||
if (!existingPoint) {
|
||||
this._permissionService.addPermissionPoint(permissionPoint);
|
||||
}
|
||||
|
||||
this._permissionService.updatePermissionPoint(permissionPoint.id, value);
|
||||
// Use FPermission's setRangeProtectionPermissionPoint method
|
||||
this._fPermission.setRangeProtectionPermissionPoint(this._unitId, this._subUnitId, permissionId, PermissionPointClass, value);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -564,18 +533,13 @@ export class FRangePermission implements IRangePermission {
|
||||
|
||||
const ruleId = rule.id;
|
||||
|
||||
await this._commandService.executeCommand(DeleteRangeProtectionMutation.id, {
|
||||
unitId: this._unitId,
|
||||
subUnitId: this._subUnitId,
|
||||
ruleIds: [ruleId],
|
||||
});
|
||||
|
||||
// The Observable stream will automatically emit the change
|
||||
// Use FPermission's removeRangeProtection method
|
||||
this._fPermission.removeRangeProtection(this._unitId, this._subUnitId, [ruleId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* List all protection rules.
|
||||
* @returns {Promise<IFRangeProtectionRule[]>} Array of protection rules.
|
||||
* @returns {Promise<FRangeProtectionRule[]>} Array of protection rules.
|
||||
* @example
|
||||
* ```ts
|
||||
* const range = univerAPI.getActiveWorkbook()?.getActiveSheet()?.getRange('A1:B2');
|
||||
@@ -584,7 +548,7 @@ export class FRangePermission implements IRangePermission {
|
||||
* console.log(rules);
|
||||
* ```
|
||||
*/
|
||||
async listRules(): Promise<IFRangeProtectionRule[]> {
|
||||
async listRules(): Promise<FRangeProtectionRule[]> {
|
||||
return await this._buildProtectionRulesAsync();
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import type { FRange } from '../f-range';
|
||||
import type { IRangeProtectionOptions, IRangeProtectionRule } from './permission-types';
|
||||
import type { IRangeProtectionOptions } from './permission-types';
|
||||
import { ICommandService, Inject, Injector } from '@univerjs/core';
|
||||
import { DeleteRangeProtectionMutation, RangeProtectionRuleModel, SetRangeProtectionMutation } from '@univerjs/sheets';
|
||||
|
||||
@@ -25,7 +25,7 @@ import { DeleteRangeProtectionMutation, RangeProtectionRuleModel, SetRangeProtec
|
||||
*
|
||||
* @hideconstructor
|
||||
*/
|
||||
export class FRangeProtectionRule implements IRangeProtectionRule {
|
||||
export class FRangeProtectionRule {
|
||||
constructor(
|
||||
private readonly _unitId: string,
|
||||
private readonly _subUnitId: string,
|
||||
@@ -143,45 +143,6 @@ export class FRangeProtectionRule implements IRangeProtectionRule {
|
||||
this._ranges.push(...ranges);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update protection options.
|
||||
* @param {Partial<IRangeProtectionOptions>} options Partial options to update (will be merged with existing options).
|
||||
* @returns {Promise<void>} A promise that resolves when the options are updated.
|
||||
* @example
|
||||
* ```ts
|
||||
* const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
* const permission = worksheet?.getWorksheetPermission();
|
||||
* const rules = await permission?.listRangeProtectionRules();
|
||||
* const rule = rules?.[0];
|
||||
* await rule?.updateOptions({ name: 'New Protection Name', allowEdit: true });
|
||||
* ```
|
||||
*/
|
||||
async updateOptions(options: Partial<IRangeProtectionOptions>): Promise<void> {
|
||||
const rule = this._rangeProtectionRuleModel.getRule(this._unitId, this._subUnitId, this._ruleId);
|
||||
if (!rule) {
|
||||
throw new Error(`Rule ${this._ruleId} not found`);
|
||||
}
|
||||
|
||||
// Merge options
|
||||
const newOptions = { ...this._options, ...options };
|
||||
|
||||
// Execute update
|
||||
await this._commandService.executeCommand(SetRangeProtectionMutation.id, {
|
||||
unitId: this._unitId,
|
||||
subUnitId: this._subUnitId,
|
||||
ruleId: this._ruleId,
|
||||
rule: {
|
||||
...rule,
|
||||
// Note: Current underlying implementation may not support storing options directly,
|
||||
// may need to update through permissionId
|
||||
// This is just an example, actual implementation may need adjustment
|
||||
},
|
||||
});
|
||||
|
||||
// Update local reference
|
||||
Object.assign(this._options, newOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the current protection rule.
|
||||
* @returns {Promise<void>} A promise that resolves when the rule is removed.
|
||||
|
||||
@@ -20,6 +20,7 @@ import type { ICollaborator, IWorkbookPermission, UnsubscribeFn, WorkbookMode, W
|
||||
import { IAuthzIoService, Inject, Injector, IPermissionService } from '@univerjs/core';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { distinctUntilChanged, filter, map, shareReplay } from 'rxjs/operators';
|
||||
import { FPermission } from '../f-permission';
|
||||
import { WORKBOOK_PERMISSION_POINT_MAP } from './permission-point-map';
|
||||
import { UnitRole, WorkbookPermissionPoint } from './permission-types';
|
||||
|
||||
@@ -65,6 +66,7 @@ export class FWorkbookPermission implements IWorkbookPermission {
|
||||
}>;
|
||||
|
||||
private _subscriptions: Subscription[] = [];
|
||||
private readonly _fPermission: FPermission;
|
||||
|
||||
constructor(
|
||||
private readonly _unitId: string,
|
||||
@@ -72,6 +74,9 @@ export class FWorkbookPermission implements IWorkbookPermission {
|
||||
@IPermissionService private readonly _permissionService: IPermissionService,
|
||||
@IAuthzIoService private readonly _authzIoService: IAuthzIoService
|
||||
) {
|
||||
// Initialize FPermission instance
|
||||
this._fPermission = this._injector.createInstance(FPermission);
|
||||
|
||||
// Initialize BehaviorSubject (with initial value)
|
||||
this._permissionSubject = new BehaviorSubject(this._buildSnapshot());
|
||||
|
||||
@@ -267,14 +272,8 @@ export class FWorkbookPermission implements IWorkbookPermission {
|
||||
continue; // Skip unchanged values
|
||||
}
|
||||
|
||||
const instance = new PointClass(this._unitId);
|
||||
const permissionPoint = this._permissionService.getPermissionPoint(instance.id);
|
||||
|
||||
if (!permissionPoint) {
|
||||
this._permissionService.addPermissionPoint(instance);
|
||||
}
|
||||
|
||||
this._permissionService.updatePermissionPoint(instance.id, value);
|
||||
// Use FPermission's setWorkbookPermissionPoint method
|
||||
this._fPermission.setWorkbookPermissionPoint(this._unitId, PointClass, value);
|
||||
pointsChanged.push({ point: pointKey, value, oldValue });
|
||||
}
|
||||
|
||||
@@ -352,14 +351,8 @@ export class FWorkbookPermission implements IWorkbookPermission {
|
||||
return; // Value unchanged, no update needed
|
||||
}
|
||||
|
||||
const instance = new PointClass(this._unitId);
|
||||
const permissionPoint = this._permissionService.getPermissionPoint(instance.id);
|
||||
|
||||
if (!permissionPoint) {
|
||||
this._permissionService.addPermissionPoint(instance);
|
||||
}
|
||||
|
||||
this._permissionService.updatePermissionPoint(instance.id, value);
|
||||
// Use FPermission's setWorkbookPermissionPoint method
|
||||
this._fPermission.setWorkbookPermissionPoint(this._unitId, PointClass, value);
|
||||
|
||||
// Update snapshot (the Observable stream will automatically emit the change)
|
||||
const newSnapshot = this._buildSnapshot();
|
||||
|
||||
@@ -14,15 +14,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type {
|
||||
IRangeProtectionRule,
|
||||
} from '@univerjs/sheets';
|
||||
import type { Observable, Subscription } from 'rxjs';
|
||||
import type { FRange } from '../f-range';
|
||||
import type { FWorksheet } from '../f-worksheet';
|
||||
import type {
|
||||
ICellPermissionDebugInfo,
|
||||
IRangeProtectionOptions,
|
||||
IRangeProtectionRule,
|
||||
IWorksheetPermission,
|
||||
IWorksheetPermissionConfig,
|
||||
IWorksheetProtectionOptions,
|
||||
UnsubscribeFn,
|
||||
WorksheetMode,
|
||||
WorksheetPermissionSnapshot,
|
||||
@@ -31,18 +32,22 @@ import { IAuthzIoService, ICommandService, Inject, Injector, IPermissionService
|
||||
import { UnitRole } from '@univerjs/protocol';
|
||||
import {
|
||||
AddRangeProtectionMutation,
|
||||
DeleteRangeProtectionMutation,
|
||||
EditStateEnum,
|
||||
RangeProtectionRuleModel,
|
||||
UnitObject,
|
||||
ViewStateEnum,
|
||||
WorksheetProtectionPointModel,
|
||||
WorksheetProtectionRuleModel,
|
||||
} from '@univerjs/sheets';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { distinctUntilChanged, filter, map, shareReplay } from 'rxjs/operators';
|
||||
import { FPermission } from '../f-permission';
|
||||
import { FRangeProtectionRule } from './f-range-protection-rule';
|
||||
import { RANGE_PERMISSION_POINT_MAP, WORKSHEET_PERMISSION_POINT_MAP } from './permission-point-map';
|
||||
import { RangePermissionPoint, WorksheetPermissionPoint } from './permission-types';
|
||||
import {
|
||||
RangePermissionPoint,
|
||||
WorksheetPermissionPoint,
|
||||
} from './permission-types';
|
||||
|
||||
/**
|
||||
* Implementation class for WorksheetPermission
|
||||
@@ -50,9 +55,9 @@ import { RangePermissionPoint, WorksheetPermissionPoint } from './permission-typ
|
||||
*
|
||||
* @hideconstructor
|
||||
*/
|
||||
export class FWorksheetPermission implements IWorksheetPermission {
|
||||
export class FWorksheetPermission {
|
||||
private readonly _permissionSubject: BehaviorSubject<WorksheetPermissionSnapshot>;
|
||||
private readonly _rangeRulesSubject: BehaviorSubject<IRangeProtectionRule[]>;
|
||||
private readonly _rangeRulesSubject: BehaviorSubject<FRangeProtectionRule[]>;
|
||||
|
||||
/**
|
||||
* Observable stream of permission snapshot changes (BehaviorSubject)
|
||||
@@ -76,18 +81,19 @@ export class FWorksheetPermission implements IWorksheetPermission {
|
||||
*/
|
||||
readonly rangeProtectionChange$: Observable<{
|
||||
type: 'add' | 'update' | 'delete';
|
||||
rules: IRangeProtectionRule[];
|
||||
rules: FRangeProtectionRule[];
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Observable stream of current range protection rules list (BehaviorSubject)
|
||||
* Emits immediately on subscription with current rules, then auto-updates when rules change
|
||||
*/
|
||||
readonly rangeProtectionRules$: Observable<IRangeProtectionRule[]>;
|
||||
readonly rangeProtectionRules$: Observable<FRangeProtectionRule[]>;
|
||||
|
||||
private readonly _unitId: string;
|
||||
private readonly _subUnitId: string;
|
||||
private readonly _subscriptions: Subscription[] = [];
|
||||
private readonly _fPermission: FPermission;
|
||||
|
||||
constructor(
|
||||
private readonly _worksheet: FWorksheet,
|
||||
@@ -96,15 +102,19 @@ export class FWorksheetPermission implements IWorksheetPermission {
|
||||
@IAuthzIoService private readonly _authzIoService: IAuthzIoService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@Inject(RangeProtectionRuleModel) private readonly _rangeProtectionRuleModel: RangeProtectionRuleModel,
|
||||
@Inject(WorksheetProtectionPointModel) private readonly _worksheetProtectionPointModel: WorksheetProtectionPointModel
|
||||
@Inject(WorksheetProtectionPointModel) private readonly _worksheetProtectionPointModel: WorksheetProtectionPointModel,
|
||||
@Inject(WorksheetProtectionRuleModel) private readonly _worksheetProtectionRuleModel: WorksheetProtectionRuleModel
|
||||
) {
|
||||
// Get unitId and subUnitId from worksheet
|
||||
this._unitId = this._worksheet.getWorkbook().getUnitId();
|
||||
this._subUnitId = this._worksheet.getSheetId();
|
||||
|
||||
// Initialize FPermission instance
|
||||
this._fPermission = this._injector.createInstance(FPermission);
|
||||
|
||||
// Initialize BehaviorSubject
|
||||
this._permissionSubject = new BehaviorSubject(this._buildSnapshot());
|
||||
this._rangeRulesSubject = new BehaviorSubject<IRangeProtectionRule[]>(this._buildRangeProtectionRules());
|
||||
this._rangeRulesSubject = new BehaviorSubject<FRangeProtectionRule[]>(this._buildRangeProtectionRules());
|
||||
|
||||
// Setup observables from internal services
|
||||
this.permission$ = this._createPermissionStream();
|
||||
@@ -157,7 +167,7 @@ export class FWorksheetPermission implements IWorksheetPermission {
|
||||
* Create range protection change stream from RangeProtectionRuleModel
|
||||
* @private
|
||||
*/
|
||||
private _createRangeProtectionChangeStream(): Observable<{ type: 'add' | 'update' | 'delete'; rules: IRangeProtectionRule[] }> {
|
||||
private _createRangeProtectionChangeStream(): Observable<{ type: 'add' | 'update' | 'delete'; rules: FRangeProtectionRule[] }> {
|
||||
return this._rangeProtectionRuleModel.ruleChange$.pipe(
|
||||
filter((change) => change.unitId === this._unitId && change.subUnitId === this._subUnitId),
|
||||
map((change) => {
|
||||
@@ -173,7 +183,7 @@ export class FWorksheetPermission implements IWorksheetPermission {
|
||||
* Create range protection rules list stream from RangeProtectionRuleModel
|
||||
* @private
|
||||
*/
|
||||
private _createRangeProtectionRulesStream(): Observable<IRangeProtectionRule[]> {
|
||||
private _createRangeProtectionRulesStream(): Observable<FRangeProtectionRule[]> {
|
||||
const ruleChangeSub = this._rangeProtectionRuleModel.ruleChange$.pipe(
|
||||
filter((change) => change.unitId === this._unitId && change.subUnitId === this._subUnitId)
|
||||
).subscribe(() => {
|
||||
@@ -239,7 +249,7 @@ export class FWorksheetPermission implements IWorksheetPermission {
|
||||
/**
|
||||
* Build range protection rules list
|
||||
*/
|
||||
private _buildRangeProtectionRules(): IRangeProtectionRule[] {
|
||||
private _buildRangeProtectionRules(): FRangeProtectionRule[] {
|
||||
const rules = this._rangeProtectionRuleModel.getSubunitRuleList(this._unitId, this._subUnitId);
|
||||
return rules.map((rule) => {
|
||||
// Convert IRange to FRange using worksheet
|
||||
@@ -267,8 +277,148 @@ export class FWorksheetPermission implements IWorksheetPermission {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Facade objects for all protection rules
|
||||
*/
|
||||
private _buildProtectionRule(rule: IRangeProtectionRule): FRangeProtectionRule {
|
||||
const ranges = rule.ranges.map((range) =>
|
||||
this._worksheet.getRange(range)
|
||||
);
|
||||
|
||||
// Build options from rule state
|
||||
const options: IRangeProtectionOptions = {
|
||||
name: rule.description || '',
|
||||
allowViewByOthers: rule.viewState !== ViewStateEnum.NoOneElseCanView,
|
||||
};
|
||||
|
||||
// Handle allowEdit based on editState
|
||||
if (rule.editState === EditStateEnum.DesignedUserCanEdit) {
|
||||
// Get collaborators list synchronously for this rule
|
||||
// Note: This is a synchronous context, but we need async data
|
||||
// We'll use a placeholder here and expect the caller to handle async properly
|
||||
// For now, we set it to an empty array as a fallback
|
||||
options.allowEdit = true;
|
||||
} else {
|
||||
options.allowEdit = false;
|
||||
}
|
||||
|
||||
return this._injector.createInstance(
|
||||
FRangeProtectionRule,
|
||||
this._unitId,
|
||||
this._subUnitId,
|
||||
rule.id,
|
||||
rule.permissionId,
|
||||
ranges,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug cell permission information.
|
||||
* @param {number} row Row index.
|
||||
* @param {number} col Column index.
|
||||
* @returns {ICellPermissionDebugInfo | null} Debug information about which rules affect this cell, or null if no rules apply.
|
||||
* @example
|
||||
* ```ts
|
||||
* const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
* const permission = worksheet?.getWorksheetPermission();
|
||||
* const debugInfo = permission?.debugCellPermission(0, 0);
|
||||
* console.log(debugInfo);
|
||||
* ```
|
||||
*/
|
||||
debugCellPermission(row: number, col: number): FRangeProtectionRule | undefined {
|
||||
const info = this._fPermission.getPermissionInfoWithCell(this._unitId, this._subUnitId, row, col);
|
||||
if (!info) {
|
||||
return undefined;
|
||||
}
|
||||
const { ruleId } = info;
|
||||
const rule = this._rangeProtectionRuleModel.getRule(this._unitId, this._subUnitId, ruleId);
|
||||
if (!rule) {
|
||||
return undefined;
|
||||
}
|
||||
return this._buildProtectionRule(rule);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create worksheet protection with collaborators support.
|
||||
* This must be called before setting permission points for collaboration to work.
|
||||
* @param {IWorksheetProtectionOptions} options Protection options including allowed users.
|
||||
* @returns {Promise<string>} The permissionId for the created protection.
|
||||
* @example
|
||||
* ```ts
|
||||
* const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
* const permission = worksheet?.getWorksheetPermission();
|
||||
*
|
||||
* // Create worksheet protection with collaborators
|
||||
* const permissionId = await permission?.protect({
|
||||
* allowedUsers: ['user1', 'user2'],
|
||||
* name: 'My Worksheet Protection'
|
||||
* });
|
||||
*
|
||||
* // Now set permission points
|
||||
* await permission?.setMode('readOnly');
|
||||
* ```
|
||||
*/
|
||||
async protect(options?: IWorksheetProtectionOptions): Promise<string> {
|
||||
// Check if already protected
|
||||
if (this.isProtected()) {
|
||||
throw new Error('Worksheet is already protected. Call unprotect() first.');
|
||||
}
|
||||
|
||||
// Use FPermission's addWorksheetBasePermission method
|
||||
const permissionId = await this._fPermission.addWorksheetBasePermission(this._unitId, this._subUnitId, options);
|
||||
|
||||
if (!permissionId) {
|
||||
throw new Error('Failed to create worksheet protection');
|
||||
}
|
||||
|
||||
return permissionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove worksheet protection.
|
||||
* This deletes the protection rule and resets all permission points to allowed.
|
||||
* @returns {Promise<void>} A promise that resolves when protection is removed.
|
||||
* @example
|
||||
* ```ts
|
||||
* const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
* const permission = worksheet?.getWorksheetPermission();
|
||||
* await permission?.unprotect();
|
||||
* ```
|
||||
*/
|
||||
async unprotect(): Promise<void> {
|
||||
if (!this.isProtected()) {
|
||||
return; // Already unprotected
|
||||
}
|
||||
|
||||
// Use FPermission's removeWorksheetPermission method
|
||||
this._fPermission.removeWorksheetPermission(this._unitId, this._subUnitId);
|
||||
|
||||
// Update snapshot
|
||||
const newSnapshot = this._buildSnapshot();
|
||||
this._permissionSubject.next(newSnapshot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if worksheet is currently protected.
|
||||
* @returns {boolean} true if protected, false otherwise.
|
||||
* @example
|
||||
* ```ts
|
||||
* const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
* const permission = worksheet?.getWorksheetPermission();
|
||||
* if (permission?.isProtected()) {
|
||||
* console.log('Worksheet is protected');
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
isProtected(): boolean {
|
||||
const rule = this._worksheetProtectionRuleModel.getRule(this._unitId, this._subUnitId);
|
||||
return !!rule;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set permission mode for the worksheet.
|
||||
* Automatically creates worksheet protection if not already protected.
|
||||
* @param {WorksheetMode} mode The permission mode to set ('editable' | 'readOnly' | 'filterOnly' | 'commentOnly').
|
||||
* @returns {Promise<void>} A promise that resolves when the mode is set.
|
||||
* @example
|
||||
@@ -279,6 +429,8 @@ export class FWorksheetPermission implements IWorksheetPermission {
|
||||
* ```
|
||||
*/
|
||||
async setMode(mode: WorksheetMode): Promise<void> {
|
||||
// Ensure worksheet protection exists before setting permission points
|
||||
|
||||
const pointsToSet = this._getModePermissions(mode);
|
||||
await this._batchSetPermissionPoints(pointsToSet);
|
||||
}
|
||||
@@ -339,14 +491,8 @@ export class FWorksheetPermission implements IWorksheetPermission {
|
||||
continue; // Skip unchanged values
|
||||
}
|
||||
|
||||
const instance = new PointClass(this._unitId, this._subUnitId);
|
||||
const permissionPoint = this._permissionService.getPermissionPoint(instance.id);
|
||||
|
||||
if (!permissionPoint) {
|
||||
this._permissionService.addPermissionPoint(instance);
|
||||
}
|
||||
|
||||
this._permissionService.updatePermissionPoint(instance.id, value);
|
||||
// Use FPermission's setWorksheetPermissionPoint method
|
||||
await this._fPermission.setWorksheetPermissionPoint(this._unitId, this._subUnitId, PointClass, value);
|
||||
pointsChanged.push({ point: pointKey, value, oldValue });
|
||||
}
|
||||
|
||||
@@ -457,59 +603,9 @@ export class FWorksheetPermission implements IWorksheetPermission {
|
||||
return this.getPoint(WorksheetPermissionPoint.View);
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug cell permission information.
|
||||
* @param {number} row Row index.
|
||||
* @param {number} col Column index.
|
||||
* @returns {ICellPermissionDebugInfo | null} Debug information about which rules affect this cell, or null if no rules apply.
|
||||
* @example
|
||||
* ```ts
|
||||
* const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
* const permission = worksheet?.getWorksheetPermission();
|
||||
* const debugInfo = permission?.debugCellPermission(0, 0);
|
||||
* console.log(debugInfo);
|
||||
* ```
|
||||
*/
|
||||
debugCellPermission(row: number, col: number): ICellPermissionDebugInfo | null {
|
||||
const hitRules = [];
|
||||
const rules = this._rangeProtectionRuleModel.getSubunitRuleList(this._unitId, this._subUnitId);
|
||||
|
||||
for (const rule of rules) {
|
||||
for (const range of rule.ranges) {
|
||||
if (
|
||||
row >= range.startRow &&
|
||||
row <= range.endRow &&
|
||||
col >= range.startColumn &&
|
||||
col <= range.endColumn
|
||||
) {
|
||||
hitRules.push({
|
||||
ruleId: rule.id,
|
||||
rangeRefs: rule.ranges.map(
|
||||
(r) => `R${r.startRow}C${r.startColumn}:R${r.endRow}C${r.endColumn}`
|
||||
),
|
||||
options: {
|
||||
name: rule.description || '',
|
||||
allowEdit: this._getRuleEditPermission(rule),
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hitRules.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
row,
|
||||
col,
|
||||
hitRules,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a specific permission point for the worksheet.
|
||||
* Automatically creates worksheet protection if not already protected.
|
||||
* @param {WorksheetPermissionPoint} point The permission point to set.
|
||||
* @param {boolean} value The value to set (true = allowed, false = denied).
|
||||
* @returns {Promise<void>} A promise that resolves when the point is set.
|
||||
@@ -521,6 +617,8 @@ export class FWorksheetPermission implements IWorksheetPermission {
|
||||
* ```
|
||||
*/
|
||||
async setPoint(point: WorksheetPermissionPoint, value: boolean): Promise<void> {
|
||||
// Ensure worksheet protection exists before setting permission points
|
||||
|
||||
const PointClass = WORKSHEET_PERMISSION_POINT_MAP[point];
|
||||
if (!PointClass) {
|
||||
throw new Error(`Unknown worksheet permission point: ${point}`);
|
||||
@@ -531,14 +629,8 @@ export class FWorksheetPermission implements IWorksheetPermission {
|
||||
return; // Value unchanged, no update needed
|
||||
}
|
||||
|
||||
const instance = new PointClass(this._unitId, this._subUnitId);
|
||||
const permissionPoint = this._permissionService.getPermissionPoint(instance.id);
|
||||
|
||||
if (!permissionPoint) {
|
||||
this._permissionService.addPermissionPoint(instance);
|
||||
}
|
||||
|
||||
this._permissionService.updatePermissionPoint(instance.id, value);
|
||||
// Use FPermission's setWorksheetPermissionPoint method
|
||||
await this._fPermission.setWorksheetPermissionPoint(this._unitId, this._subUnitId, PointClass, value);
|
||||
|
||||
// Update snapshot (the Observable stream will automatically emit the change)
|
||||
const newSnapshot = this._buildSnapshot();
|
||||
@@ -629,7 +721,7 @@ export class FWorksheetPermission implements IWorksheetPermission {
|
||||
/**
|
||||
* Protect multiple ranges at once (batch operation).
|
||||
* @param {Array<{ ranges: FRange[]; options?: IRangeProtectionOptions }>} configs Array of protection configurations.
|
||||
* @returns {Promise<IRangeProtectionRule[]>} Array of created protection rules.
|
||||
* @returns {Promise<FRangeProtectionRule[]>} Array of created protection rules.
|
||||
* @example
|
||||
* ```ts
|
||||
* const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
@@ -652,7 +744,7 @@ export class FWorksheetPermission implements IWorksheetPermission {
|
||||
ranges: FRange[];
|
||||
options?: IRangeProtectionOptions;
|
||||
}>
|
||||
): Promise<IRangeProtectionRule[]> {
|
||||
): Promise<FRangeProtectionRule[]> {
|
||||
if (!configs || configs.length === 0) {
|
||||
throw new Error('Configs cannot be empty');
|
||||
}
|
||||
@@ -779,14 +871,8 @@ export class FWorksheetPermission implements IWorksheetPermission {
|
||||
return;
|
||||
}
|
||||
|
||||
const permissionPoint = new PermissionPointClass(this._unitId, this._subUnitId, permissionId);
|
||||
const existingPoint = this._permissionService.getPermissionPoint(permissionPoint.id);
|
||||
|
||||
if (!existingPoint) {
|
||||
this._permissionService.addPermissionPoint(permissionPoint);
|
||||
}
|
||||
|
||||
this._permissionService.updatePermissionPoint(permissionPoint.id, value);
|
||||
// Use FPermission's setRangeProtectionPermissionPoint method
|
||||
this._fPermission.setRangeProtectionPermissionPoint(this._unitId, this._subUnitId, permissionId, PermissionPointClass, value);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -805,18 +891,15 @@ export class FWorksheetPermission implements IWorksheetPermission {
|
||||
return;
|
||||
}
|
||||
|
||||
await this._commandService.executeCommand(DeleteRangeProtectionMutation.id, {
|
||||
unitId: this._unitId,
|
||||
subUnitId: this._subUnitId,
|
||||
ruleIds,
|
||||
});
|
||||
// Use FPermission's removeRangeProtection method
|
||||
this._fPermission.removeRangeProtection(this._unitId, this._subUnitId, ruleIds);
|
||||
|
||||
// The Observable stream will automatically emit the change event
|
||||
}
|
||||
|
||||
/**
|
||||
* List all range protection rules for the worksheet.
|
||||
* @returns {Promise<IRangeProtectionRule[]>} Array of protection rules.
|
||||
* @returns {Promise<FRangeProtectionRule[]>} Array of protection rules.
|
||||
* @example
|
||||
* ```ts
|
||||
* const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
|
||||
@@ -825,7 +908,7 @@ export class FWorksheetPermission implements IWorksheetPermission {
|
||||
* console.log(rules);
|
||||
* ```
|
||||
*/
|
||||
async listRangeProtectionRules(): Promise<IRangeProtectionRule[]> {
|
||||
async listRangeProtectionRules(): Promise<FRangeProtectionRule[]> {
|
||||
return this._buildRangeProtectionRules();
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
import type { IUser } from '@univerjs/protocol';
|
||||
import type { Observable } from 'rxjs';
|
||||
import type { FRange } from '../f-range';
|
||||
|
||||
/**
|
||||
* ========================
|
||||
@@ -198,6 +197,26 @@ export type RangePermissionSnapshot = Record<RangePermissionPoint, boolean>;
|
||||
*/
|
||||
export type UnsubscribeFn = () => void;
|
||||
|
||||
/**
|
||||
* ========================
|
||||
* Worksheet Protection Configuration
|
||||
* ========================
|
||||
*/
|
||||
|
||||
/**
|
||||
* Worksheet protection options configuration
|
||||
*/
|
||||
export interface IWorksheetProtectionOptions {
|
||||
/** Whitelist of users allowed to edit; empty means only owner */
|
||||
allowedUsers?: string[];
|
||||
|
||||
/** Protection name for UI display */
|
||||
name?: string;
|
||||
|
||||
/** Custom metadata */
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================
|
||||
* Range Protection Configuration and Rules
|
||||
@@ -223,30 +242,6 @@ export interface IRangeProtectionOptions {
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Range protection rule Facade
|
||||
* Encapsulates internal permissionId / ruleId
|
||||
*/
|
||||
export interface IRangeProtectionRule {
|
||||
/** Internal rule id, for debugging/logging, generally not directly used by callers */
|
||||
readonly id: string;
|
||||
|
||||
/** List of ranges covered by this rule */
|
||||
readonly ranges: FRange[];
|
||||
|
||||
/** Current rule configuration */
|
||||
readonly options: IRangeProtectionOptions;
|
||||
|
||||
/** Update protected ranges */
|
||||
updateRanges(ranges: FRange[]): Promise<void>;
|
||||
|
||||
/** Partially update configuration */
|
||||
updateOptions(options: Partial<IRangeProtectionOptions>): Promise<void>;
|
||||
|
||||
/** Delete current protection rule */
|
||||
remove(): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cell permission debug rule information
|
||||
*/
|
||||
@@ -257,16 +252,6 @@ export interface ICellPermissionDebugRuleInfo {
|
||||
options: IRangeProtectionOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cell permission debug information
|
||||
*/
|
||||
export interface ICellPermissionDebugInfo {
|
||||
row: number;
|
||||
col: number;
|
||||
/** List of protection rules that apply */
|
||||
hitRules: ICellPermissionDebugRuleInfo[];
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================
|
||||
* Facade: WorkbookPermission
|
||||
@@ -385,197 +370,13 @@ export interface IWorksheetPermissionConfig {
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Worksheet-level permission Facade interface
|
||||
*/
|
||||
export interface IWorksheetPermission {
|
||||
/**
|
||||
* Set worksheet overall mode:
|
||||
* - 'readOnly' → Lock write-related points
|
||||
* - 'filterOnly' → Only enable Filter/Sort, close other write-related points
|
||||
* - 'commentOnly' → Close write, keep comment
|
||||
* - 'editable' → Most write-related points enabled
|
||||
*/
|
||||
setMode(mode: WorksheetMode): Promise<void>;
|
||||
|
||||
/** Shortcut: Read-only */
|
||||
setReadOnly(): Promise<void>;
|
||||
|
||||
/** Shortcut: Editable */
|
||||
setEditable(): Promise<void>;
|
||||
|
||||
/** Whether current user can "overall" edit this sheet (not considering local range protection) */
|
||||
canEdit(): boolean;
|
||||
|
||||
/**
|
||||
* Cell-level high-level check (combines sheet-level & range-level rules)
|
||||
*/
|
||||
canEditCell(row: number, col: number): boolean;
|
||||
canViewCell(row: number, col: number): boolean;
|
||||
|
||||
/**
|
||||
* Debug use: View protection rule information for a specific cell
|
||||
*/
|
||||
debugCellPermission(row: number, col: number): ICellPermissionDebugInfo | null;
|
||||
|
||||
/**
|
||||
* Point operations (low-level)
|
||||
*/
|
||||
setPoint(point: WorksheetPermissionPoint, value: boolean): Promise<void>;
|
||||
getPoint(point: WorksheetPermissionPoint): boolean;
|
||||
getSnapshot(): WorksheetPermissionSnapshot;
|
||||
|
||||
/**
|
||||
* Batch apply permission configuration (for "configuration-driven" scenarios)
|
||||
* Internally uses Command to ensure undo/redo
|
||||
*/
|
||||
applyConfig(config: IWorksheetPermissionConfig): Promise<void>;
|
||||
|
||||
/**
|
||||
* Range protection management
|
||||
*/
|
||||
|
||||
/** Batch create multiple range protection rules (one-time operation, better performance) */
|
||||
protectRanges(configs: Array<{
|
||||
ranges: FRange[];
|
||||
options?: IRangeProtectionOptions;
|
||||
}>): Promise<IRangeProtectionRule[]>;
|
||||
|
||||
/** Batch delete multiple protection rules */
|
||||
unprotectRules(ruleIds: string[]): Promise<void>;
|
||||
|
||||
/**
|
||||
* List all range protection rules on current sheet
|
||||
*/
|
||||
listRangeProtectionRules(): Promise<IRangeProtectionRule[]>;
|
||||
|
||||
/**
|
||||
* ========================
|
||||
* RxJS Observable Reactive Interface
|
||||
* ========================
|
||||
*/
|
||||
|
||||
/**
|
||||
* Permission snapshot change stream (BehaviorSubject, immediately provides current state on subscription)
|
||||
* Triggers when any permission point changes
|
||||
*/
|
||||
readonly permission$: Observable<WorksheetPermissionSnapshot>;
|
||||
|
||||
/**
|
||||
* Single permission point change stream
|
||||
* For scenarios that only care about specific permission point changes
|
||||
*/
|
||||
readonly pointChange$: Observable<{
|
||||
point: WorksheetPermissionPoint;
|
||||
value: boolean;
|
||||
oldValue: boolean;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Range protection rule change stream (add, delete, update)
|
||||
*/
|
||||
readonly rangeProtectionChange$: Observable<{
|
||||
type: 'add' | 'update' | 'delete';
|
||||
rules: IRangeProtectionRule[];
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Current all range protection rules list stream (BehaviorSubject)
|
||||
* Immediately provides current rule list on subscription, auto-updates when rules change
|
||||
*/
|
||||
readonly rangeProtectionRules$: Observable<IRangeProtectionRule[]>;
|
||||
|
||||
/**
|
||||
* Compatibility method: Simplified subscription (for users unfamiliar with RxJS)
|
||||
* Internally implemented based on permission$ Observable
|
||||
*/
|
||||
subscribe(listener: (snapshot: WorksheetPermissionSnapshot) => void): UnsubscribeFn;
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================
|
||||
* Facade: RangePermission
|
||||
* ========================
|
||||
*/
|
||||
|
||||
/**
|
||||
* Range-level permission Facade interface
|
||||
*/
|
||||
export interface IRangePermission {
|
||||
/**
|
||||
* Create protection rule on current range
|
||||
* - Default options.allowEdit = false → Treated as "locked"
|
||||
*/
|
||||
protect(options?: IRangeProtectionOptions): Promise<IRangeProtectionRule>;
|
||||
|
||||
/**
|
||||
* Remove all protection rules covered by current range
|
||||
* (Internally can calculate range → ruleId mapping)
|
||||
*/
|
||||
unprotect(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Whether current range is in protected state (for current user)
|
||||
*/
|
||||
isProtected(): boolean;
|
||||
|
||||
/** Whether current user can edit this range (combines Worksheet / Workbook / Range levels) */
|
||||
canEdit(): boolean;
|
||||
|
||||
/** Whether current user can view this range */
|
||||
canView(): boolean;
|
||||
|
||||
/**
|
||||
* Range-level point reading (generally for debugging / advanced scenarios)
|
||||
* Usually only need Edit/View two points
|
||||
*/
|
||||
getPoint(point: RangePermissionPoint): boolean;
|
||||
getSnapshot(): RangePermissionSnapshot;
|
||||
|
||||
/**
|
||||
* Set a specific permission point for the range (low-level API for local runtime control)
|
||||
* @param {RangePermissionPoint} point The permission point to set
|
||||
* @param {boolean} value The value to set (true = allowed, false = denied)
|
||||
* @returns {Promise<void>} A promise that resolves when the point is set
|
||||
* @example
|
||||
* ```ts
|
||||
* const range = univerAPI.getActiveWorkbook()?.getActiveSheet()?.getRange('A1:B2');
|
||||
* const permission = range?.getRangePermission();
|
||||
* await permission?.setPoint(RangePermissionPoint.Edit, false); // Disable edit for current user
|
||||
* ```
|
||||
*/
|
||||
setPoint(point: RangePermissionPoint, value: boolean): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get snapshot of all protection rules in current worksheet (can also proxy worksheet interface)
|
||||
*/
|
||||
listRules(): Promise<IRangeProtectionRule[]>;
|
||||
|
||||
/**
|
||||
* ========================
|
||||
* RxJS Observable Reactive Interface
|
||||
* ========================
|
||||
*/
|
||||
|
||||
/**
|
||||
* Permission snapshot change stream (BehaviorSubject, immediately provides current state on subscription)
|
||||
*/
|
||||
readonly permission$: Observable<RangePermissionSnapshot>;
|
||||
|
||||
/**
|
||||
* Protection state change stream
|
||||
*/
|
||||
readonly protectionChange$: Observable<{
|
||||
type: 'protected';
|
||||
rule: IRangeProtectionRule;
|
||||
} | {
|
||||
type: 'unprotected';
|
||||
ruleId: string;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Compatibility method: Simplified subscription (for users unfamiliar with RxJS)
|
||||
* Internally implemented based on permission$ Observable
|
||||
*/
|
||||
subscribe(listener: (snapshot: RangePermissionSnapshot) => void): UnsubscribeFn;
|
||||
export interface ICellPermissionDebugInfo {
|
||||
permissionId: string;
|
||||
ruleId: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user