fix(sheets): permission api (#6202)

Co-authored-by: Wpxp123456 <2677556700@qq.com>
This commit is contained in:
WEI ZHANG
2025-11-26 16:45:29 +08:00
committed by GitHub
parent 92593431ae
commit c4d7fe75c3
13 changed files with 366 additions and 3326 deletions
+17 -1
View File
@@ -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()));
+71 -24
View File
@@ -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 IDruleId 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;
}