refactor(sheets): refactor facade api of sheet permission module (#6127)

Co-authored-by: Wpxp123456 <2677556700@qq.com>
This commit is contained in:
WEI ZHANG
2025-11-15 17:47:54 +08:00
committed by GitHub
co-authored by Wpxp123456
parent 9b63474291
commit 714fb96e67
17 changed files with 7321 additions and 2 deletions
+41 -2
View File
@@ -24,6 +24,29 @@ import { AddRangeProtectionMutation, AddWorksheetProtectionMutation, DeleteRange
/**
* @description Used to generate permission instances to control permissions for the entire workbook
* @deprecated This class is deprecated. Use the new permission API instead:
* - For workbook-level permissions, use `workbook.getWorkbookPermission()`
* - For worksheet-level permissions, use `worksheet.getWorksheetPermission()`
* - For range-level permissions, use `range.getRangePermission()`
*
* The new API provides:
* - More intuitive and type-safe interfaces
* - Better support for RxJS Observable streams
* - Enum-based permission points instead of class constructors
* - Simplified collaborator management
* - Mode-based permission settings (viewer, editor, owner, etc.)
*
* Migration examples:
* ```ts
* // Old API
* const permission = workbook.getPermission();
* await permission.addRangeBaseProtection(unitId, subUnitId, ranges);
*
* // New API
* const worksheet = workbook.getSheetBySheetId(subUnitId);
* const permission = worksheet.getWorksheetPermission();
* await permission.protectRanges([{ ranges, options: { name: 'Protected', allowEdit: false } }]);
* ```
* @hideconstructor
*/
export class FPermission extends FBase {
@@ -304,6 +327,7 @@ export class FPermission extends FBase {
* Adds a range protection to the worksheet.
* Note that after adding, only the background mask of the permission module will be rendered. If you want to modify the function permissions,
* you need to modify the permission points with the permissionId returned by this function.
* @deprecated Use `worksheet.getWorksheetPermission().protectRanges()` instead
* @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.
@@ -311,6 +335,7 @@ export class FPermission extends FBase {
*
* @example
* ```typescript
* // Old API
* const workbook = univerAPI.getActiveWorkbook();
* const permission = workbook.getPermission();
* const unitId = workbook.getId();
@@ -319,11 +344,18 @@ export class FPermission extends FBase {
* const range = worksheet.getRange('A1:B2');
* const ranges = [];
* ranges.push(range);
* // 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 **`setRangeProtectionPermissionPoint`** API.
* const res = await permission.addRangeBaseProtection(unitId, subUnitId, ranges);
* const {permissionId, ruleId} = res;
* console.log('debugger', permissionId, ruleId);
*
* // New API (recommended)
* const worksheet = univerAPI.getActiveWorkbook().getActiveSheet();
* const permission = worksheet.getWorksheetPermission();
* const range = worksheet.getRange('A1:B2');
* await permission.protectRanges([{
* ranges: [range],
* options: { name: 'Protected Area', allowEdit: false }
* }]);
* ```
*/
async addRangeBaseProtection(unitId: string, subUnitId: string, ranges: FRange[]): Promise<{
@@ -378,12 +410,14 @@ export class FPermission extends FBase {
/**
* Removes the range protection from the worksheet.
* @deprecated Use `worksheet.getWorksheetPermission().unprotectRules()` instead
* @param {string} unitId - The unique identifier of the workbook.
* @param {string} subUnitId - The unique identifier of the worksheet.
* @param {string[]} ruleIds - The rule IDs of the range protection to be removed.
*
* @example
* ```typescript
* // Old API
* const workbook = univerAPI.getActiveWorkbook();
* const permission = workbook.getPermission();
* const unitId = workbook.getId();
@@ -395,6 +429,11 @@ export class FPermission extends FBase {
* const res = await permission.addRangeBaseProtection(unitId, subUnitId, ranges);
* const ruleId = res.ruleId;
* permission.removeRangeProtection(unitId, subUnitId, [ruleId]);
*
* // New API (recommended)
* const worksheet = univerAPI.getActiveWorkbook().getActiveSheet();
* const permission = worksheet.getWorksheetPermission();
* await permission.unprotectRules([ruleId]);
* ```
*/
removeRangeProtection(unitId: string, subUnitId: string, ruleIds: string[]): void {
+42
View File
@@ -23,6 +23,8 @@ import { FBaseInitialable } from '@univerjs/core/facade';
import { FormulaDataModel, serializeRange, serializeRangeWithSheet } from '@univerjs/engine-formula';
import { addMergeCellsUtil, ClearSelectionAllCommand, ClearSelectionContentCommand, ClearSelectionFormatCommand, DeleteRangeMoveLeftCommand, DeleteRangeMoveUpCommand, DeleteWorksheetRangeThemeStyleCommand, getAddMergeMutationRangeByType, getPrimaryForRange, InsertRangeMoveDownCommand, InsertRangeMoveRightCommand, RemoveWorksheetMergeCommand, SetBorderBasicCommand, SetHorizontalTextAlignCommand, SetRangeValuesCommand, SetSelectionsOperation, SetStyleCommand, SetTextRotationCommand, SetTextWrapCommand, SetVerticalTextAlignCommand, SetWorksheetRangeThemeStyleCommand, SheetRangeThemeService, SplitTextToColumnsCommand } from '@univerjs/sheets';
import { FWorkbook } from './f-workbook';
import { FWorksheet } from './f-worksheet';
import { FRangePermission } from './permission/f-range-permission';
import { transformCoreHorizontalAlignment, transformCoreVerticalAlignment, transformFacadeHorizontalAlignment, transformFacadeVerticalAlignment } from './utils';
export type FontLine = 'none' | 'underline' | 'line-through';
@@ -2571,4 +2573,44 @@ export class FRange extends FBaseInitialable {
setFormulas(formulas: string[][]): FRange {
return this.setValues(formulas.map((row) => row.map((formula) => ({ f: formula }))));
}
/**
* Get the RangePermission instance for managing range-level permissions.
* This is the new permission API that provides range-specific permission control.
* @returns {FRangePermission} - The RangePermission instance.
* @example
* ```ts
* const fWorksheet = univerAPI.getActiveWorkbook().getActiveSheet();
* const fRange = fWorksheet.getRange('A1:B10');
* const permission = fRange.getRangePermission();
*
* // Protect the range
* await permission.protect({ name: 'Protected Area', allowEdit: false });
*
* // Check if range is protected
* const isProtected = permission.isProtected();
*
* // Check if current user can edit
* const canEdit = permission.canEdit();
*
* // Unprotect the range
* await permission.unprotect();
*
* // Subscribe to protection changes
* permission.protectionChange$.subscribe(change => {
* console.log('Protection changed:', change);
* });
* ```
*/
getRangePermission(): FRangePermission {
const fWorksheet = this._injector.createInstance(FWorksheet, this._injector.createInstance(FWorkbook, this._workbook), this._workbook, this._worksheet);
return this._injector.createInstance(
FRangePermission,
this._workbook.getUnitId(),
this._worksheet.getSheetId(),
this,
fWorksheet
);
}
}
+31
View File
@@ -26,6 +26,7 @@ import { FDefinedName, FDefinedNameBuilder } from './f-defined-name';
import { FPermission } from './f-permission';
import { FRange } from './f-range';
import { FWorksheet } from './f-worksheet';
import { FWorkbookPermission } from './permission/f-workbook-permission';
/**
* Facade API object bounded to a workbook. It provides a set of methods to interact with the workbook.
@@ -802,6 +803,7 @@ export class FWorkbook extends FBaseInitialable {
/**
* Get the PermissionInstance.
* @returns {FPermission} - The PermissionInstance.
* @deprecated Use `getWorkbookPermission()` instead for the new permission API
* @example
* ```ts
* const fWorkbook = univerAPI.getActiveWorkbook();
@@ -813,6 +815,35 @@ export class FWorkbook extends FBaseInitialable {
return this._injector.createInstance(FPermission);
}
/**
* Get the WorkbookPermission instance for managing workbook-level permissions.
* This is the new permission API that provides a more intuitive and type-safe interface.
* @returns {FWorkbookPermission} - The WorkbookPermission instance.
* @example
* ```ts
* const fWorkbook = univerAPI.getActiveWorkbook();
* const permission = fWorkbook.getWorkbookPermission();
*
* // Set workbook to read-only mode
* await permission.setMode('viewer');
*
* // Add a collaborator
* await permission.addCollaborator({
* userId: 'user123',
* name: 'John Doe',
* role: 'editor'
* });
*
* // Subscribe to permission changes
* permission.permission$.subscribe(snapshot => {
* console.log('Permissions changed:', snapshot);
* });
* ```
*/
getWorkbookPermission(): FWorkbookPermission {
return this._injector.createInstance(FWorkbookPermission, this._workbook.getUnitId());
}
/**
* Get the defined name by name.
* @param {string} name The name of the defined name to get
+37
View File
@@ -25,6 +25,7 @@ import { AppendRowCommand, CancelFrozenCommand, ClearSelectionAllCommand, ClearS
import { FDefinedNameBuilder } from './f-defined-name';
import { FRange } from './f-range';
import { FSelection } from './f-selection';
import { FWorksheetPermission } from './permission/f-worksheet-permission';
import { covertToColRange, covertToRowRange } from './utils';
export interface IFacadeClearOptions {
@@ -2561,4 +2562,40 @@ export class FWorksheet extends FBaseInitialable {
});
return this;
}
/**
* Get the WorksheetPermission instance for managing worksheet-level permissions.
* This is the new permission API that provides worksheet-specific permission control.
* @returns {FWorksheetPermission} - The WorksheetPermission instance.
* @example
* ```ts
* const fWorksheet = univerAPI.getActiveWorkbook().getActiveSheet();
* const permission = fWorksheet.getWorksheetPermission();
*
* // Set worksheet to read-only mode
* await permission.setMode('readOnly');
*
* // Check if a specific cell can be edited
* const canEdit = permission.canEditCell(0, 0);
*
* // Protect multiple ranges at once
* const range1 = fWorksheet.getRange('A1:B10');
* const range2 = fWorksheet.getRange('D1:E10');
* await permission.protectRanges([
* { ranges: [range1], options: { name: 'Range 1', allowEdit: false } },
* { ranges: [range2], options: { name: 'Range 2', allowEdit: false } }
* ]);
*
* // Subscribe to permission changes
* permission.permission$.subscribe(snapshot => {
* console.log('Worksheet permissions changed:', snapshot);
* });
* ```
*/
getWorksheetPermission(): FWorksheetPermission {
return this._injector.createInstance(
FWorksheetPermission,
this
);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,461 @@
/**
* 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();
});
});
});
@@ -0,0 +1,560 @@
/**
* 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();
});
});
});
@@ -0,0 +1,621 @@
/**
* 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();
});
});
});
@@ -0,0 +1,694 @@
/**
* 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();
});
});
});
@@ -0,0 +1,489 @@
/**
* 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();
});
});
});
@@ -0,0 +1,768 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Nullable } from '@univerjs/core';
import type { IRangeProtectionRule } from '@univerjs/sheets';
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 { BehaviorSubject } from 'rxjs';
import { distinctUntilChanged, filter, map, shareReplay } from 'rxjs/operators';
import { FRangeProtectionRule } from './f-range-protection-rule';
import { RANGE_PERMISSION_POINT_MAP } from './permission-point-map';
import { RangePermissionPoint } from './permission-types';
/**
* Implementation class for RangePermission
* Manages range-level permissions
*
* @hideconstructor
*/
export class FRangePermission implements IRangePermission {
private readonly _permissionSubject: BehaviorSubject<RangePermissionSnapshot>;
private readonly _subscriptions: Subscription[] = [];
/**
* Observable stream of permission snapshot changes
* @returns Observable that emits when permission snapshot changes
*/
readonly permission$: Observable<RangePermissionSnapshot>;
/**
* Observable stream of protection state changes
* @returns Observable that emits when protection state changes
*/
readonly protectionChange$: Observable<{
type: 'protected';
rule: IFRangeProtectionRule;
} | {
type: 'unprotected';
ruleId: string;
}>;
constructor(
private readonly _unitId: string,
private readonly _subUnitId: string,
private readonly _range: FRange,
private readonly _worksheet: FWorksheet,
@Inject(Injector) private readonly _injector: Injector,
@Inject(IPermissionService) private readonly _permissionService: IPermissionService,
@Inject(IAuthzIoService) private readonly _authzIoService: IAuthzIoService,
@Inject(ICommandService) private readonly _commandService: ICommandService,
@Inject(RangeProtectionRuleModel) private readonly _rangeProtectionRuleModel: RangeProtectionRuleModel
) {
this._permissionSubject = new BehaviorSubject<RangePermissionSnapshot>(this._buildSnapshot());
// Create permission$ stream from IPermissionService
this.permission$ = this._createPermissionStream();
// Create protectionChange$ stream from RangeProtectionRuleModel
this.protectionChange$ = this._createProtectionChangeStream();
}
/**
* Create permission snapshot stream from IPermissionService
* @private
*/
private _createPermissionStream(): Observable<RangePermissionSnapshot> {
// Listen to permission point changes from IPermissionService
const sub = this._permissionService.permissionPointUpdate$.pipe(
filter((point) => {
// Filter for permission points related to this range
const pointId = point.id;
return pointId.includes(this._unitId) && pointId.includes(this._subUnitId);
})
).subscribe(() => {
this._permissionSubject.next(this._buildSnapshot());
});
// Store subscription for cleanup
this._subscriptions.push(sub);
return this._permissionSubject.asObservable().pipe(
distinctUntilChanged((prev, curr) => JSON.stringify(prev) === JSON.stringify(curr)),
shareReplay({ bufferSize: 1, refCount: true })
);
}
/**
* Create protection change stream from RangeProtectionRuleModel
* @private
*/
private _createProtectionChangeStream(): Observable<{
type: 'protected';
rule: IFRangeProtectionRule;
} | {
type: 'unprotected';
ruleId: string;
}> {
return this._rangeProtectionRuleModel.ruleChange$.pipe(
filter((change) => {
// Only process changes for this worksheet
if (change.unitId !== this._unitId || change.subUnitId !== this._subUnitId) {
return false;
}
// Only emit for changes that affect this specific range
if (change.type === 'delete') {
// Check if the deleted rule was protecting this range
return this._rangeMatches(change.rule);
} else if (change.type === 'add') {
// Check if the new rule protects this range
return this._rangeMatches(change.rule);
}
return false;
}),
map((change) => {
// Also update permission snapshot
this._permissionSubject.next(this._buildSnapshot());
if (change.type === 'delete') {
return {
type: 'unprotected' as const,
ruleId: change.rule.id,
};
} else {
// change.type === 'add'
const rule = this._createFacadeRule(change.rule);
return {
type: 'protected' as const,
rule,
};
}
}),
shareReplay({ bufferSize: 1, refCount: true })
);
}
/**
* Check if a protection rule matches this range
*/
private _rangeMatches(rule: IRangeProtectionRule): boolean {
const range = this._range.getRange();
return rule.ranges.some((ruleRange) =>
range.startRow === ruleRange.startRow &&
range.startColumn === ruleRange.startColumn &&
range.endRow === ruleRange.endRow &&
range.endColumn === ruleRange.endColumn
);
}
/**
* Create a Facade rule from internal rule
*/
private _createFacadeRule(rule: IRangeProtectionRule): IFRangeProtectionRule {
const ranges = rule.ranges.map((range) =>
this._worksheet.getRange(
range.startRow,
range.startColumn,
range.endRow - range.startRow + 1,
range.endColumn - range.startColumn + 1
)
);
const options: IRangeProtectionOptions = {
name: rule.description || '',
allowViewByOthers: rule.viewState !== ViewStateEnum.NoOneElseCanView,
allowEdit: rule.editState === EditStateEnum.DesignedUserCanEdit,
};
return this._injector.createInstance(
FRangeProtectionRule,
this._unitId,
this._subUnitId,
rule.id,
rule.permissionId,
ranges,
options
);
}
/**
* Get the value of a specific permission point.
* @param {RangePermissionPoint} point The permission point to query.
* @returns {boolean} true if allowed, false if denied.
* @example
* ```ts
* const range = univerAPI.getActiveWorkbook()?.getActiveSheet()?.getRange('A1:B2');
* const permission = range?.getRangePermission();
* const canEdit = permission?.getPoint(RangePermissionPoint.Edit);
* console.log(canEdit);
* ```
*/
getPoint(point: RangePermissionPoint): boolean {
const PermissionPointClass = RANGE_PERMISSION_POINT_MAP[point];
if (!PermissionPointClass) {
console.warn(`Unknown permission point: ${point}`);
return false;
}
// First try to get permission from protection rule
const rule = this._getProtectionRule();
if (rule) {
const permissionPoint = new PermissionPointClass(this._unitId, this._subUnitId, rule.permissionId);
const permission = this._permissionService.getPermissionPoint(permissionPoint.id);
if (permission) {
return permission.value;
}
}
// 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
return true;
}
/**
* Get the current permission snapshot.
* @returns {RangePermissionSnapshot} Snapshot of all permission points.
* @example
* ```ts
* const range = univerAPI.getActiveWorkbook()?.getActiveSheet()?.getRange('A1:B2');
* const permission = range?.getRangePermission();
* const snapshot = permission?.getSnapshot();
* console.log(snapshot);
* ```
*/
getSnapshot(): RangePermissionSnapshot {
return this._buildSnapshot();
}
/**
* Check if the current range is protected.
* @returns {boolean} true if protected, false otherwise.
* @example
* ```ts
* const range = univerAPI.getActiveWorkbook()?.getActiveSheet()?.getRange('A1:B2');
* const permission = range?.getRangePermission();
* const isProtected = permission?.isProtected();
* console.log(isProtected);
* ```
*/
isProtected(): boolean {
return this._getProtectionRule() !== null;
}
/**
* Check if the current user can edit this range.
* @returns {boolean} true if editable, false otherwise.
* @example
* ```ts
* const range = univerAPI.getActiveWorkbook()?.getActiveSheet()?.getRange('A1:B2');
* const permission = range?.getRangePermission();
* if (permission?.canEdit()) {
* console.log('You can edit this range');
* }
* ```
*/
canEdit(): boolean {
// Always check the permission point value first
// This handles cases where setPoint() was called without protect()
return this.getPoint(RangePermissionPoint.Edit);
}
/**
* Check if the current user can view this range.
* @returns {boolean} true if viewable, false otherwise.
* @example
* ```ts
* const range = univerAPI.getActiveWorkbook()?.getActiveSheet()?.getRange('A1:B2');
* const permission = range?.getRangePermission();
* if (permission?.canView()) {
* console.log('You can view this range');
* }
* ```
*/
canView(): boolean {
// Always check the permission point value first
// This handles cases where setPoint() was called without protect()
return this.getPoint(RangePermissionPoint.View);
}
/**
* Check if the current user can manage collaborators for this range.
* @returns {boolean} true if can manage collaborators, false otherwise.
* @example
* ```ts
* const range = univerAPI.getActiveWorkbook()?.getActiveSheet()?.getRange('A1:B2');
* const permission = range?.getRangePermission();
* if (permission?.canManageCollaborator()) {
* console.log('You can manage collaborators for this range');
* }
* ```
*/
canManageCollaborator(): boolean {
// Always check the permission point value first
// This handles cases where setPoint() was called without protect()
return this.getPoint(RangePermissionPoint.ManageCollaborator);
}
/**
* Check if the current user can delete this protection rule.
* @returns {boolean} true if can delete rule, false otherwise.
* @example
* ```ts
* const range = univerAPI.getActiveWorkbook()?.getActiveSheet()?.getRange('A1:B2');
* const permission = range?.getRangePermission();
* if (permission?.canDelete()) {
* console.log('You can delete this protection rule');
* }
* ```
*/
canDelete(): boolean {
// Always check the permission point value first
// This handles cases where setPoint() was called without protect()
return this.getPoint(RangePermissionPoint.Delete);
}
/**
* 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).
* @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();
* // Can set permission points without calling protect() first (local-only mode)
* await permission?.setPoint(RangePermissionPoint.Edit, false); // Disable edit
* await permission?.setPoint(RangePermissionPoint.View, true); // Enable view
* ```
*/
async setPoint(point: RangePermissionPoint, value: boolean): Promise<void> {
const PermissionPointClass = RANGE_PERMISSION_POINT_MAP[point];
if (!PermissionPointClass) {
throw new Error(`Unknown permission point: ${point}`);
}
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 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);
// 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.
* @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
* });
* console.log(rule);
* ```
*/
async protect(options?: IRangeProtectionOptions): Promise<IFRangeProtectionRule> {
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,
},
});
const ruleId = this._rangeProtectionRuleModel.createRuleId(this._unitId, this._subUnitId);
const range = this._range.getRange();
// 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,
}],
});
// Set permission points for local runtime control
await this._setPermissionPoints(permissionId, options);
// Create and return FRangeProtectionRule instance
const rule = this._injector.createInstance(
FRangeProtectionRule,
this._unitId,
this._subUnitId,
ruleId,
permissionId,
[this._range],
options || {}
);
// The Observable stream will automatically emit the change
return rule;
}
/**
* Determine view state from options
* @private
*/
private _determineViewState(options?: IRangeProtectionOptions): ViewStateEnum {
if (options?.allowViewByOthers === false) {
return ViewStateEnum.NoOneElseCanView; // Only owner can view
}
// For true, undefined, or string[], default to OthersCanView
return ViewStateEnum.OthersCanView;
}
/**
* Determine edit state from options
* @private
*/
private _determineEditState(options?: IRangeProtectionOptions): EditStateEnum {
if (options?.allowEdit === true && options?.allowedUsers?.length) {
return EditStateEnum.DesignedUserCanEdit; // Designed users can edit
}
// For false or undefined, default to OnlyMe
return EditStateEnum.OnlyMe;
}
/**
* Set permission points based on options (for local runtime control)
* @private
*/
private async _setPermissionPoints(permissionId: string, options?: IRangeProtectionOptions): Promise<void> {
if (!options) {
return;
}
// Helper function to determine permission value
const getPermissionValue = (option: boolean | string[] | undefined, defaultValue: boolean): boolean => {
if (option === undefined) {
return defaultValue;
}
if (typeof option === 'boolean') {
return option;
}
// For string[] (whitelist), we default to true and let the collaboration system handle it
return true;
};
// Set permission points
await this._setPermissionPoint(permissionId, RangePermissionPoint.Edit, getPermissionValue(options.allowEdit, false));
await this._setPermissionPoint(permissionId, RangePermissionPoint.View, getPermissionValue(options.allowViewByOthers, true));
}
/**
* Set a single permission point
* @private
*/
private async _setPermissionPoint(permissionId: string, point: RangePermissionPoint, value: boolean): Promise<void> {
const PermissionPointClass = RANGE_PERMISSION_POINT_MAP[point];
if (!PermissionPointClass) {
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);
}
/**
* Unprotect the current range.
* @returns {Promise<void>} A promise that resolves when the range is unprotected.
* @example
* ```ts
* const range = univerAPI.getActiveWorkbook()?.getActiveSheet()?.getRange('A1:B2');
* const permission = range?.getRangePermission();
* await permission?.unprotect();
* ```
*/
async unprotect(): Promise<void> {
const rule = this._getProtectionRule();
if (!rule) {
// Silently handle unprotecting a non-protected range
return;
}
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
}
/**
* List all protection rules.
* @returns {Promise<IFRangeProtectionRule[]>} Array of protection rules.
* @example
* ```ts
* const range = univerAPI.getActiveWorkbook()?.getActiveSheet()?.getRange('A1:B2');
* const permission = range?.getRangePermission();
* const rules = await permission?.listRules();
* console.log(rules);
* ```
*/
async listRules(): Promise<IFRangeProtectionRule[]> {
return await this._buildProtectionRulesAsync();
}
/**
* Subscribe to permission changes (simplified interface).
* @param {Function} listener Callback function to be called when permissions change.
* @returns {Function} Unsubscribe function.
* @example
* ```ts
* const range = univerAPI.getActiveWorkbook()?.getActiveSheet()?.getRange('A1:B2');
* const permission = range?.getRangePermission();
* const unsubscribe = permission?.subscribe((snapshot) => {
* console.log('Permission changed:', snapshot);
* });
* // Later, to stop listening:
* unsubscribe?.();
* ```
*/
subscribe(listener: (snapshot: RangePermissionSnapshot) => void): (() => void) {
const subscription = this.permission$.subscribe(listener);
return () => subscription.unsubscribe();
}
/**
* Get the protection rule for the current range
*/
private _getProtectionRule(): Nullable<IRangeProtectionRule> {
const rules = this._rangeProtectionRuleModel.getSubunitRuleList(this._unitId, this._subUnitId);
const range = this._range.getRange();
for (const rule of rules) {
for (const ruleRange of rule.ranges) {
if (
range.startRow === ruleRange.startRow &&
range.startColumn === ruleRange.startColumn &&
range.endRow === ruleRange.endRow &&
range.endColumn === ruleRange.endColumn
) {
return rule;
}
}
}
return null;
}
/**
* Build Facade objects for all protection rules
*/
private _buildProtectionRules(): FRangeProtectionRule[] {
const rules = this._rangeProtectionRuleModel.getSubunitRuleList(this._unitId, this._subUnitId);
return rules.map((rule) => {
const ranges = rule.ranges.map((range) =>
this._worksheet.getRange(
range.startRow,
range.startColumn,
range.endRow - range.startRow + 1,
range.endColumn - range.startColumn + 1
)
);
// 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
);
});
}
/**
* Build Facade objects for all protection rules (async version with collaborator data)
* @private
*/
private async _buildProtectionRulesAsync(): Promise<FRangeProtectionRule[]> {
const rules = this._rangeProtectionRuleModel.getSubunitRuleList(this._unitId, this._subUnitId);
// Use Promise.all to fetch collaborators for all rules in parallel
const rulesWithOptions = await Promise.all(
rules.map(async (rule) => {
const ranges = rule.ranges.map((range) =>
this._worksheet.getRange(
range.startRow,
range.startColumn,
range.endRow - range.startRow + 1,
range.endColumn - range.startColumn + 1
)
);
// 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) {
try {
// Fetch collaborators for this rule
const collaborators = await this._authzIoService.listCollaborators({
objectID: rule.permissionId,
unitID: this._unitId,
});
// Extract collaborator IDs with Editor role
const editorIds = collaborators
.filter((c) => c.role === UnitRole.Editor)
.map((c) => c.subject?.userID || c.id);
options.allowEdit = editorIds.length > 0;
} catch (error) {
// If fetching collaborators fails, fall back to empty array
console.warn(`Failed to fetch collaborators for rule ${rule.id}:`, error);
options.allowEdit = false;
}
} else {
options.allowEdit = false;
}
return {
rule,
ranges,
options,
};
})
);
// Create FRangeProtectionRule instances
return rulesWithOptions.map(({ rule, ranges, options }) =>
this._injector.createInstance(
FRangeProtectionRule,
this._unitId,
this._subUnitId,
rule.id,
rule.permissionId,
ranges,
options
)
);
}
/**
* Build permission snapshot
*/
private _buildSnapshot(): RangePermissionSnapshot {
const snapshot: RangePermissionSnapshot = {} as RangePermissionSnapshot;
Object.values(RangePermissionPoint).forEach((point) => {
snapshot[point] = this.getPoint(point);
});
return snapshot;
}
/**
* Clean up resources
*/
dispose(): void {
this._subscriptions.forEach((sub) => sub.unsubscribe());
this._permissionSubject.complete();
}
}
@@ -0,0 +1,221 @@
/**
* 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 { FRange } from '../f-range';
import type { IRangeProtectionOptions, IRangeProtectionRule } from './permission-types';
import { ICommandService, Inject, Injector } from '@univerjs/core';
import { DeleteRangeProtectionMutation, RangeProtectionRuleModel, SetRangeProtectionMutation } from '@univerjs/sheets';
/**
* Implementation class for range protection rules
* Encapsulates operations on a single protection rule
*
* @hideconstructor
*/
export class FRangeProtectionRule implements IRangeProtectionRule {
constructor(
private readonly _unitId: string,
private readonly _subUnitId: string,
private readonly _ruleId: string,
private readonly _permissionId: string,
private readonly _ranges: FRange[],
private readonly _options: IRangeProtectionOptions,
@Inject(Injector) private readonly _injector: Injector,
@Inject(ICommandService) private readonly _commandService: ICommandService,
@Inject(RangeProtectionRuleModel) private readonly _rangeProtectionRuleModel: RangeProtectionRuleModel
) {}
/**
* Get the rule ID.
* @returns {string} The unique identifier of this protection rule.
* @example
* ```ts
* const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
* const permission = worksheet?.permission();
* const rules = await permission?.listRangeProtectionRules();
* const ruleId = rules?.[0]?.id;
* console.log(ruleId);
* ```
*/
get id(): string {
return this._ruleId;
}
/**
* Get the protected ranges.
* @returns {FRange[]} Array of protected ranges.
* @example
* ```ts
* const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
* const permission = worksheet?.permission();
* const rules = await permission?.listRangeProtectionRules();
* const ranges = rules?.[0]?.ranges;
* console.log(ranges);
* ```
*/
get ranges(): FRange[] {
return this._ranges;
}
/**
* Get the protection options.
* @returns {IRangeProtectionOptions} Copy of the protection options.
* @example
* ```ts
* const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
* const permission = worksheet?.permission();
* const rules = await permission?.listRangeProtectionRules();
* const options = rules?.[0]?.options;
* console.log(options);
* ```
*/
get options(): IRangeProtectionOptions {
return { ...this._options };
}
/**
* Update the protected ranges.
* @param {FRange[]} ranges New ranges to protect.
* @returns {Promise<void>} A promise that resolves when the ranges are updated.
* @example
* ```ts
* const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
* const permission = worksheet?.permission();
* const rules = await permission?.listRangeProtectionRules();
* const rule = rules?.[0];
* await rule?.updateRanges([worksheet.getRange('A1:C3')]);
* ```
*/
async updateRanges(ranges: FRange[]): Promise<void> {
if (!ranges || ranges.length === 0) {
throw new Error('Ranges cannot be empty');
}
const rule = this._rangeProtectionRuleModel.getRule(this._unitId, this._subUnitId, this._ruleId);
if (!rule) {
throw new Error(`Rule ${this._ruleId} not found`);
}
// Check for overlap with other rules
const subunitRuleList = this._rangeProtectionRuleModel
.getSubunitRuleList(this._unitId, this._subUnitId)
.filter((r) => r.id !== this._ruleId);
const hasOverlap = subunitRuleList.some((otherRule) =>
otherRule.ranges.some((otherRange) =>
ranges.some((newRange) => {
const newRangeData = newRange.getRange();
return this._rangesIntersect(newRangeData, otherRange);
})
)
);
if (hasOverlap) {
throw new Error('Range protection cannot intersect with other protection rules');
}
// Execute update
await this._commandService.executeCommand(SetRangeProtectionMutation.id, {
unitId: this._unitId,
subUnitId: this._subUnitId,
ruleId: this._ruleId,
rule: {
...rule,
ranges: ranges.map((range) => range.getRange()),
},
});
// Update local reference
(this._ranges as FRange[]).length = 0;
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?.permission();
* 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.
* @example
* ```ts
* const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
* const permission = worksheet?.permission();
* const rules = await permission?.listRangeProtectionRules();
* const rule = rules?.[0];
* await rule?.remove();
* ```
*/
async remove(): Promise<void> {
await this._commandService.executeCommand(DeleteRangeProtectionMutation.id, {
unitId: this._unitId,
subUnitId: this._subUnitId,
ruleIds: [this._ruleId],
});
}
/**
* Check if two ranges intersect
* @returns true if ranges intersect, false otherwise
* @private
*/
private _rangesIntersect(
range1: { startRow: number; startColumn: number; endRow: number; endColumn: number },
range2: { startRow: number; startColumn: number; endRow: number; endColumn: number }
): boolean {
return !(
range1.endRow < range2.startRow ||
range1.startRow > range2.endRow ||
range1.endColumn < range2.startColumn ||
range1.startColumn > range2.endColumn
);
}
}
@@ -0,0 +1,623 @@
/**
* 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 { ICollaborator as IProtocolCollaborator, IUser } from '@univerjs/protocol';
import type { Observable, Subscription } from 'rxjs';
import type { ICollaborator, IWorkbookPermission, UnsubscribeFn, WorkbookMode, WorkbookPermissionSnapshot } from './permission-types';
import { IAuthzIoService, Inject, Injector, IPermissionService } from '@univerjs/core';
import { BehaviorSubject, Subject } from 'rxjs';
import { distinctUntilChanged, filter, map, shareReplay } from 'rxjs/operators';
import { WORKBOOK_PERMISSION_POINT_MAP } from './permission-point-map';
import { UnitRole, WorkbookPermissionPoint } from './permission-types';
/**
* Implementation class for WorkbookPermission
* Provides workbook-level permission control
*
* @hideconstructor
*/
export class FWorkbookPermission implements IWorkbookPermission {
private readonly _permissionSubject: BehaviorSubject<WorkbookPermissionSnapshot>;
// Collaborator changes are tracked manually since IAuthzIoService doesn't provide an observable
// TODO: If IAuthzIoService adds an observable in the future, migrate to use that
private readonly _collaboratorChangeSubject = new Subject<{
type: 'add' | 'update' | 'delete';
collaborator: ICollaborator;
}>();
/**
* Observable stream of permission snapshot changes (BehaviorSubject)
* Emits immediately on subscription with current state, then on any permission point change
*/
readonly permission$: Observable<WorkbookPermissionSnapshot>;
/**
* Observable stream of individual permission point changes
* Emits when a specific permission point value changes
*/
readonly pointChange$: Observable<{
point: WorkbookPermissionPoint;
value: boolean;
oldValue: boolean;
}>;
/**
* Observable stream of collaborator changes
* Emits when collaborators are added, updated, or removed
*/
readonly collaboratorChange$: Observable<{
type: 'add' | 'update' | 'delete';
collaborator: ICollaborator;
}>;
private _subscriptions: Subscription[] = [];
constructor(
private readonly _unitId: string,
@Inject(Injector) private readonly _injector: Injector,
@IPermissionService private readonly _permissionService: IPermissionService,
@IAuthzIoService private readonly _authzIoService: IAuthzIoService
) {
// Initialize BehaviorSubject (with initial value)
this._permissionSubject = new BehaviorSubject(this._buildSnapshot());
// Setup observables from internal services
this.permission$ = this._createPermissionStream();
this.pointChange$ = this._createPointChangeStream();
// Collaborator changes are tracked manually since IAuthzIoService doesn't provide an observable
this.collaboratorChange$ = this._collaboratorChangeSubject.asObservable().pipe(
shareReplay({ bufferSize: 1, refCount: true })
);
}
/**
* Create permission snapshot stream from IPermissionService
* @private
*/
private _createPermissionStream(): Observable<WorkbookPermissionSnapshot> {
const permissionSub = this._permissionService.permissionPointUpdate$.pipe(
filter((point) => point.id.includes(this._unitId))
).subscribe(() => {
this._permissionSubject.next(this._buildSnapshot());
});
this._subscriptions.push(permissionSub);
return this._permissionSubject.asObservable().pipe(
distinctUntilChanged((prev, curr) => JSON.stringify(prev) === JSON.stringify(curr)),
shareReplay({ bufferSize: 1, refCount: true })
);
}
/**
* Create point change stream from IPermissionService
* @private
*/
private _createPointChangeStream(): Observable<{ point: WorkbookPermissionPoint; value: boolean; oldValue: boolean }> {
// Cache to store previous values for comparison
const valueCache = new Map<WorkbookPermissionPoint, boolean>();
// Initialize cache with current values for all permission points
for (const point in WorkbookPermissionPoint) {
const pointKey = WorkbookPermissionPoint[point as keyof typeof WorkbookPermissionPoint];
valueCache.set(pointKey, this.getPoint(pointKey));
}
return this._permissionService.permissionPointUpdate$.pipe(
filter((point) => point.id.includes(this._unitId)),
map((permissionPoint) => {
// Find which WorkbookPermissionPoint this corresponds to
const pointType = this._extractWorkbookPointType(permissionPoint.id);
if (!pointType) return null;
const newValue: boolean = Boolean(permissionPoint.value);
// Get old value from cache
const oldValue: boolean = valueCache.get(pointType)!;
// Update cache for next time
valueCache.set(pointType, newValue);
if (oldValue === newValue) return null;
return { point: pointType, value: newValue, oldValue };
}),
filter((change): change is { point: WorkbookPermissionPoint; value: boolean; oldValue: boolean } => change !== null),
shareReplay({ bufferSize: 1, refCount: true })
);
}
/**
* Extract WorkbookPermissionPoint type from permission point ID
* @private
*/
private _extractWorkbookPointType(pointId: string): WorkbookPermissionPoint | null {
for (const point in WorkbookPermissionPoint) {
const pointKey = WorkbookPermissionPoint[point as keyof typeof WorkbookPermissionPoint];
const PointClass = WORKBOOK_PERMISSION_POINT_MAP[pointKey];
if (!PointClass) continue;
const instance = new PointClass(this._unitId);
if (instance.id === pointId) {
return pointKey;
}
}
return null;
}
/**
* Build permission snapshot
*/
private _buildSnapshot(): WorkbookPermissionSnapshot {
const snapshot = {} as WorkbookPermissionSnapshot;
for (const point in WorkbookPermissionPoint) {
const pointKey = WorkbookPermissionPoint[point as keyof typeof WorkbookPermissionPoint];
snapshot[pointKey] = this.getPoint(pointKey);
}
return snapshot;
}
/**
* Listen to permission point changes
/**
* Set permission mode for the workbook.
* @param {WorkbookMode} mode The permission mode to set ('owner' | 'editor' | 'viewer' | 'commenter').
* @returns {Promise<void>} A promise that resolves when the mode is set.
* @example
* ```ts
* const workbook = univerAPI.getActiveWorkbook();
* const permission = workbook?.getWorkbookPermission();
* await permission?.setMode('editor');
* ```
*/
async setMode(mode: WorkbookMode): Promise<void> {
const pointsToSet = this._getModePermissions(mode);
await this._batchSetPermissionPoints(pointsToSet);
}
/**
* Get permission configuration for a specific mode
* @private
*/
private _getModePermissions(mode: WorkbookMode): Record<WorkbookPermissionPoint, boolean> {
// Initialize all permission points to false first
const pointsToSet: Record<WorkbookPermissionPoint, boolean> = {} as Record<WorkbookPermissionPoint, boolean>;
Object.values(WorkbookPermissionPoint).forEach((point) => {
pointsToSet[point] = false;
});
switch (mode) {
case 'owner':
// Owner has all permissions
Object.values(WorkbookPermissionPoint).forEach((point) => {
pointsToSet[point] = true;
});
break;
case 'editor':
// Editor can edit, view, print, export, and perform basic operations
pointsToSet[WorkbookPermissionPoint.Edit] = true;
pointsToSet[WorkbookPermissionPoint.View] = true;
pointsToSet[WorkbookPermissionPoint.Print] = true;
pointsToSet[WorkbookPermissionPoint.Export] = true;
pointsToSet[WorkbookPermissionPoint.CopyContent] = true;
pointsToSet[WorkbookPermissionPoint.Comment] = true;
pointsToSet[WorkbookPermissionPoint.CreateSheet] = true;
pointsToSet[WorkbookPermissionPoint.DeleteSheet] = true;
pointsToSet[WorkbookPermissionPoint.RenameSheet] = true;
pointsToSet[WorkbookPermissionPoint.MoveSheet] = true;
pointsToSet[WorkbookPermissionPoint.HideSheet] = true;
pointsToSet[WorkbookPermissionPoint.InsertRow] = true;
pointsToSet[WorkbookPermissionPoint.InsertColumn] = true;
pointsToSet[WorkbookPermissionPoint.DeleteRow] = true;
pointsToSet[WorkbookPermissionPoint.DeleteColumn] = true;
pointsToSet[WorkbookPermissionPoint.CopySheet] = true;
pointsToSet[WorkbookPermissionPoint.CreateProtection] = true;
// Not allowed: ManageCollaborator, Share, DuplicateFile, etc. (remain false)
break;
case 'viewer':
// Viewer can only view and print
pointsToSet[WorkbookPermissionPoint.View] = true;
pointsToSet[WorkbookPermissionPoint.Print] = true;
// All other permissions remain false
break;
case 'commenter':
// Commenter can view, comment, and print
pointsToSet[WorkbookPermissionPoint.View] = true;
pointsToSet[WorkbookPermissionPoint.Comment] = true;
pointsToSet[WorkbookPermissionPoint.Print] = true;
// All other permissions remain false
break;
}
return pointsToSet;
}
/**
* Batch set multiple permission points efficiently
* @private
*/
private async _batchSetPermissionPoints(pointsToSet: Record<WorkbookPermissionPoint, boolean>): Promise<void> {
// Note: IPermissionService doesn't have a batch update API, so we update individually
// but we optimize by only updating the snapshot once at the end
const pointsChanged: Array<{ point: WorkbookPermissionPoint; value: boolean; oldValue: boolean }> = [];
for (const [point, value] of Object.entries(pointsToSet)) {
const pointKey = point as WorkbookPermissionPoint;
const PointClass = WORKBOOK_PERMISSION_POINT_MAP[pointKey];
if (!PointClass) {
throw new Error(`Unknown workbook permission point: ${pointKey}`);
}
const oldValue = this.getPoint(pointKey);
if (oldValue === value) {
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);
pointsChanged.push({ point: pointKey, value, oldValue });
}
// Update snapshot once at the end (the Observable stream will pick up the changes automatically)
if (pointsChanged.length > 0) {
const newSnapshot = this._buildSnapshot();
this._permissionSubject.next(newSnapshot);
}
}
/**
* Set the workbook to read-only mode (viewer mode).
* @returns {Promise<void>} A promise that resolves when the mode is set.
* @example
* ```ts
* const workbook = univerAPI.getActiveWorkbook();
* const permission = workbook?.getWorkbookPermission();
* await permission?.setReadOnly();
* ```
*/
async setReadOnly(): Promise<void> {
await this.setMode('viewer');
}
/**
* Set the workbook to editable mode (editor mode).
* @returns {Promise<void>} A promise that resolves when the mode is set.
* @example
* ```ts
* const workbook = univerAPI.getActiveWorkbook();
* const permission = workbook?.getWorkbookPermission();
* await permission?.setEditable();
* ```
*/
async setEditable(): Promise<void> {
await this.setMode('editor');
}
/**
* Check if the workbook is editable.
* @returns {boolean} true if the workbook can be edited, false otherwise.
* @example
* ```ts
* const workbook = univerAPI.getActiveWorkbook();
* const permission = workbook?.getWorkbookPermission();
* if (permission?.canEdit()) {
* console.log('Workbook is editable');
* }
* ```
*/
canEdit(): boolean {
return this.getPoint(WorkbookPermissionPoint.Edit);
}
/**
* Set a specific permission point.
* @param {WorkbookPermissionPoint} 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 workbook = univerAPI.getActiveWorkbook();
* const permission = workbook?.getWorkbookPermission();
* await permission?.setPoint(WorkbookPermissionPoint.Print, false);
* ```
*/
async setPoint(point: WorkbookPermissionPoint, value: boolean): Promise<void> {
const PointClass = WORKBOOK_PERMISSION_POINT_MAP[point];
if (!PointClass) {
throw new Error(`Unknown workbook permission point: ${point}`);
}
const oldValue = this.getPoint(point);
if (oldValue === value) {
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);
// Update snapshot (the Observable stream will automatically emit the change)
const newSnapshot = this._buildSnapshot();
this._permissionSubject.next(newSnapshot);
}
/**
* Get the value of a specific permission point.
* @param {WorkbookPermissionPoint} point The permission point to query.
* @returns {boolean} true if allowed, false if denied.
* @example
* ```ts
* const workbook = univerAPI.getActiveWorkbook();
* const permission = workbook?.getWorkbookPermission();
* const canPrint = permission?.getPoint(WorkbookPermissionPoint.Print);
* console.log(canPrint);
* ```
*/
getPoint(point: WorkbookPermissionPoint): boolean {
const PointClass = WORKBOOK_PERMISSION_POINT_MAP[point];
if (!PointClass) {
throw new Error(`Unknown workbook permission point: ${point}`);
}
const instance = new PointClass(this._unitId);
const permissionPoint = this._permissionService.getPermissionPoint(instance.id);
return permissionPoint?.value ?? true; // Default to true (allowed)
}
/**
* Get a snapshot of all permission points.
* @returns {WorkbookPermissionSnapshot} An object containing all permission point values.
* @example
* ```ts
* const workbook = univerAPI.getActiveWorkbook();
* const permission = workbook?.getWorkbookPermission();
* const snapshot = permission?.getSnapshot();
* console.log(snapshot);
* ```
*/
getSnapshot(): WorkbookPermissionSnapshot {
return this._buildSnapshot();
}
/**
* Set multiple collaborators at once (replaces existing collaborators).
* @param {Array<{ user: IUser; role: UnitRole }>} collaborators Array of collaborators with user information and role.
* @returns {Promise<void>} A promise that resolves when the collaborators are set.
* @example
* ```ts
* const workbook = univerAPI.getActiveWorkbook();
* const permission = workbook?.getWorkbookPermission();
* await permission?.setCollaborators([
* {
* user: { userID: 'user1', name: 'John Doe', avatar: 'https://...' },
* role: UnitRole.Editor
* },
* {
* user: { userID: 'user2', name: 'Jane Smith', avatar: '' },
* role: UnitRole.Reader
* }
* ]);
* ```
*/
async setCollaborators(collaborators: Array<{ user: IUser; role: UnitRole }>): Promise<void> {
// Convert to protocol format
const protocolCollaborators: IProtocolCollaborator[] = collaborators.map((c) => ({
id: c.user.userID,
subject: c.user,
role: c.role,
}));
// Batch set collaborators (replace mode)
await this._authzIoService.putCollaborators({
objectID: this._unitId,
unitID: this._unitId,
collaborators: protocolCollaborators,
});
// Trigger change events
collaborators.forEach((c) => {
this._collaboratorChangeSubject.next({
type: 'add',
collaborator: {
user: { id: c.user.userID },
role: c.role,
},
});
});
}
/**
* Add a single collaborator.
* @param {IUser} user The user information (userID, name, avatar).
* @param {UnitRole} role The role to assign.
* @returns {Promise<void>} A promise that resolves when the collaborator is added.
* @example
* ```ts
* const workbook = univerAPI.getActiveWorkbook();
* const permission = workbook?.getWorkbookPermission();
* await permission?.addCollaborator(
* { userID: 'user1', name: 'John Doe', avatar: 'https://...' },
* UnitRole.Editor
* );
* ```
*/
async addCollaborator(user: IUser, role: UnitRole): Promise<void> {
await this._authzIoService.createCollaborator({
objectID: this._unitId,
unitID: this._unitId,
collaborators: [{
id: user.userID,
subject: user,
role,
}],
});
this._collaboratorChangeSubject.next({
type: 'add',
collaborator: {
user: { id: user.userID },
role,
},
});
}
/**
* Update an existing collaborator's role and information.
* @param {IUser} user The updated user information (userID, name, avatar).
* @param {UnitRole} role The new role to assign.
* @returns {Promise<void>} A promise that resolves when the collaborator is updated.
* @example
* ```ts
* const workbook = univerAPI.getActiveWorkbook();
* const permission = workbook?.getWorkbookPermission();
* await permission?.updateCollaborator(
* { userID: 'user1', name: 'John Doe Updated', avatar: 'https://...' },
* UnitRole.Reader
* );
* ```
*/
async updateCollaborator(user: IUser, role: UnitRole): Promise<void> {
await this._authzIoService.updateCollaborator({
objectID: this._unitId,
unitID: this._unitId,
collaborator: {
id: user.userID,
subject: user,
role,
},
});
this._collaboratorChangeSubject.next({
type: 'update',
collaborator: {
user: { id: user.userID },
role,
},
});
}
/**
* Remove a collaborator from the workbook.
* @param {string} userId The user ID to remove.
* @returns {Promise<void>} A promise that resolves when the collaborator is removed.
* @example
* ```ts
* const workbook = univerAPI.getActiveWorkbook();
* const permission = workbook?.getWorkbookPermission();
* await permission?.removeCollaborator('user1');
* ```
*/
async removeCollaborator(userId: string): Promise<void> {
await this._authzIoService.deleteCollaborator({
objectID: this._unitId,
unitID: this._unitId,
collaboratorID: userId,
});
this._collaboratorChangeSubject.next({
type: 'delete',
collaborator: {
user: { id: userId },
role: UnitRole.Reader, // Placeholder value
},
});
}
/**
* Remove multiple collaborators at once.
* @param {string[]} userIds Array of user IDs to remove.
* @returns {Promise<void>} A promise that resolves when the collaborators are removed.
* @example
* ```ts
* const workbook = univerAPI.getActiveWorkbook();
* const permission = workbook?.getWorkbookPermission();
* await permission?.removeCollaborators(['user1', 'user2']);
* ```
*/
async removeCollaborators(userIds: string[]): Promise<void> {
for (const userId of userIds) {
await this.removeCollaborator(userId);
}
}
/**
* List all collaborators of the workbook.
* @returns {Promise<ICollaborator[]>} Array of collaborators with their roles.
* @example
* ```ts
* const workbook = univerAPI.getActiveWorkbook();
* const permission = workbook?.getWorkbookPermission();
* const collaborators = await permission?.listCollaborators();
* console.log(collaborators);
* ```
*/
async listCollaborators(): Promise<ICollaborator[]> {
const protocolCollaborators = await this._authzIoService.listCollaborators({
objectID: this._unitId,
unitID: this._unitId,
});
return protocolCollaborators.map((c) => ({
user: {
id: c.subject?.userID || c.id,
displayName: c.subject?.name || '',
},
role: c.role as UnitRole, // Type conversion: protocol UnitRole to our UnitRole
}));
}
/**
* Subscribe to permission changes (simplified interface for users not familiar with RxJS).
* @param {Function} listener Callback function to be called when permissions change.
* @returns {UnsubscribeFn} Unsubscribe function.
* @example
* ```ts
* const workbook = univerAPI.getActiveWorkbook();
* const permission = workbook?.getWorkbookPermission();
* const unsubscribe = permission?.subscribe((snapshot) => {
* console.log('Permission changed:', snapshot);
* });
* // Later, to stop listening:
* unsubscribe?.();
* ```
*/
subscribe(listener: (snapshot: WorkbookPermissionSnapshot) => void): UnsubscribeFn {
const subscription = this.permission$.subscribe(listener);
return () => subscription.unsubscribe();
}
/**
* Clean up resources
*/
dispose(): void {
this._subscriptions.forEach((s) => s.unsubscribe());
this._permissionSubject.complete();
this._collaboratorChangeSubject.complete();
}
}
@@ -0,0 +1,860 @@
/**
* 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 { Observable, Subscription } from 'rxjs';
import type { FRange } from '../f-range';
import type { FWorksheet } from '../f-worksheet';
import type {
ICellPermissionDebugInfo,
IRangeProtectionOptions,
IRangeProtectionRule,
IWorksheetPermission,
IWorksheetPermissionConfig,
UnsubscribeFn,
WorksheetMode,
WorksheetPermissionSnapshot,
} from './permission-types';
import { IAuthzIoService, ICommandService, Inject, Injector, IPermissionService } from '@univerjs/core';
import { UnitRole } from '@univerjs/protocol';
import {
AddRangeProtectionMutation,
DeleteRangeProtectionMutation,
EditStateEnum,
RangeProtectionRuleModel,
UnitObject,
ViewStateEnum,
WorksheetProtectionPointModel,
} from '@univerjs/sheets';
import { BehaviorSubject } from 'rxjs';
import { distinctUntilChanged, filter, map, shareReplay } from 'rxjs/operators';
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';
/**
* Implementation class for WorksheetPermission
* Provides worksheet-level permission control
*
* @hideconstructor
*/
export class FWorksheetPermission implements IWorksheetPermission {
private readonly _permissionSubject: BehaviorSubject<WorksheetPermissionSnapshot>;
private readonly _rangeRulesSubject: BehaviorSubject<IRangeProtectionRule[]>;
/**
* Observable stream of permission snapshot changes (BehaviorSubject)
* Emits immediately on subscription with current state, then on any permission point change
*/
readonly permission$: Observable<WorksheetPermissionSnapshot>;
/**
* Observable stream of individual permission point changes
* Emits when a specific permission point value changes
*/
readonly pointChange$: Observable<{
point: WorksheetPermissionPoint;
value: boolean;
oldValue: boolean;
}>;
/**
* Observable stream of range protection rule changes
* Emits when protection rules are added, updated, or deleted
*/
readonly rangeProtectionChange$: Observable<{
type: 'add' | 'update' | 'delete';
rules: IRangeProtectionRule[];
}>;
/**
* 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[]>;
private readonly _unitId: string;
private readonly _subUnitId: string;
private readonly _subscriptions: Subscription[] = [];
constructor(
private readonly _worksheet: FWorksheet,
@Inject(Injector) private readonly _injector: Injector,
@IPermissionService private readonly _permissionService: IPermissionService,
@IAuthzIoService private readonly _authzIoService: IAuthzIoService,
@ICommandService private readonly _commandService: ICommandService,
@Inject(RangeProtectionRuleModel) private readonly _rangeProtectionRuleModel: RangeProtectionRuleModel,
@Inject(WorksheetProtectionPointModel) private readonly _worksheetProtectionPointModel: WorksheetProtectionPointModel
) {
// Get unitId and subUnitId from worksheet
this._unitId = this._worksheet.getWorkbook().getUnitId();
this._subUnitId = this._worksheet.getSheetId();
// Initialize BehaviorSubject
this._permissionSubject = new BehaviorSubject(this._buildSnapshot());
this._rangeRulesSubject = new BehaviorSubject<IRangeProtectionRule[]>(this._buildRangeProtectionRules());
// Setup observables from internal services
this.permission$ = this._createPermissionStream();
this.pointChange$ = this._createPointChangeStream();
this.rangeProtectionChange$ = this._createRangeProtectionChangeStream();
this.rangeProtectionRules$ = this._createRangeProtectionRulesStream();
}
/**
* Create permission snapshot stream from IPermissionService
* @private
*/
private _createPermissionStream(): Observable<WorksheetPermissionSnapshot> {
// Listen to permission point changes from IPermissionService
const permissionSub = this._permissionService.permissionPointUpdate$.pipe(
filter((point) => point.id.includes(this._unitId) && point.id.includes(this._subUnitId))
).subscribe(() => {
this._permissionSubject.next(this._buildSnapshot());
});
this._subscriptions.push(permissionSub);
return this._permissionSubject.asObservable().pipe(
distinctUntilChanged((prev, curr) => JSON.stringify(prev) === JSON.stringify(curr)),
shareReplay({ bufferSize: 1, refCount: true })
);
}
/**
* Create point change stream from IPermissionService
* @private
*/
private _createPointChangeStream(): Observable<{ point: WorksheetPermissionPoint; value: boolean; oldValue: boolean }> {
return this._permissionService.permissionPointUpdate$.pipe(
filter((point) => point.id.includes(this._unitId) && point.id.includes(this._subUnitId)),
map((point) => {
const pointType = this._extractWorksheetPointType(point.id);
if (!pointType) return null;
return {
point: pointType,
value: point.value ?? false,
oldValue: !(point.value ?? false),
};
}),
filter((change): change is { point: WorksheetPermissionPoint; value: boolean; oldValue: boolean } => change !== null),
shareReplay({ bufferSize: 1, refCount: true })
);
}
/**
* Create range protection change stream from RangeProtectionRuleModel
* @private
*/
private _createRangeProtectionChangeStream(): Observable<{ type: 'add' | 'update' | 'delete'; rules: IRangeProtectionRule[] }> {
return this._rangeProtectionRuleModel.ruleChange$.pipe(
filter((change) => change.unitId === this._unitId && change.subUnitId === this._subUnitId),
map((change) => {
const rules = this._buildRangeProtectionRules();
const type: 'add' | 'update' | 'delete' = change.type === 'delete' ? 'delete' : (change.type === 'set' ? 'update' : 'add');
return { type, rules };
}),
shareReplay({ bufferSize: 1, refCount: true })
);
}
/**
* Create range protection rules list stream from RangeProtectionRuleModel
* @private
*/
private _createRangeProtectionRulesStream(): Observable<IRangeProtectionRule[]> {
const ruleChangeSub = this._rangeProtectionRuleModel.ruleChange$.pipe(
filter((change) => change.unitId === this._unitId && change.subUnitId === this._subUnitId)
).subscribe(() => {
this._rangeRulesSubject.next(this._buildRangeProtectionRules());
});
this._subscriptions.push(ruleChangeSub);
return this._rangeRulesSubject.asObservable().pipe(
distinctUntilChanged((prev, curr) => {
if (prev.length !== curr.length) return false;
return prev.every((p, i) => p.id === curr[i].id);
}),
shareReplay({ bufferSize: 1, refCount: true })
);
}
/**
* Extract WorksheetPermissionPoint type from permission point ID
* @private
*/
private _extractWorksheetPointType(pointId: string): WorksheetPermissionPoint | null {
// Try to match against known worksheet permission points
for (const [pointName, PointClass] of Object.entries(WORKSHEET_PERMISSION_POINT_MAP)) {
const testPoint = new PointClass(this._unitId, this._subUnitId);
if (testPoint.id === pointId) {
return pointName as WorksheetPermissionPoint;
}
}
return null;
}
/**
* Read the actual edit permission from a rule's permissionId
*/
private _getRuleEditPermission(rule: { permissionId: string }): boolean {
const PermissionPointClass = RANGE_PERMISSION_POINT_MAP[RangePermissionPoint.Edit];
if (!PermissionPointClass) {
return false;
}
const permissionPoint = new PermissionPointClass(
this._unitId,
this._subUnitId,
rule.permissionId
);
const permission = this._permissionService.getPermissionPoint(permissionPoint.id);
return permission?.value ?? false;
}
/**
* Build permission snapshot
*/
private _buildSnapshot(): WorksheetPermissionSnapshot {
const snapshot = {} as WorksheetPermissionSnapshot;
for (const point in WorksheetPermissionPoint) {
const pointKey = WorksheetPermissionPoint[point as keyof typeof WorksheetPermissionPoint];
snapshot[pointKey] = this.getPoint(pointKey);
}
return snapshot;
}
/**
* Build range protection rules list
*/
private _buildRangeProtectionRules(): IRangeProtectionRule[] {
const rules = this._rangeProtectionRuleModel.getSubunitRuleList(this._unitId, this._subUnitId);
return rules.map((rule) => {
// Convert IRange to FRange using worksheet
const ranges = rule.ranges.map((range) =>
this._worksheet.getRange(
range.startRow,
range.startColumn,
range.endRow - range.startRow + 1,
range.endColumn - range.startColumn + 1
)
);
return this._injector.createInstance(
FRangeProtectionRule,
this._unitId,
this._subUnitId,
rule.id,
rule.permissionId,
ranges,
{
name: rule.description || '',
allowEdit: this._getRuleEditPermission(rule),
}
);
});
}
/**
* Set permission mode for the worksheet.
* @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
* ```ts
* const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
* const permission = worksheet?.getWorksheetPermission();
* await permission?.setMode('readOnly');
* ```
*/
async setMode(mode: WorksheetMode): Promise<void> {
const pointsToSet = this._getModePermissions(mode);
await this._batchSetPermissionPoints(pointsToSet);
}
/**
* Get permission configuration for a specific mode
* @private
*/
private _getModePermissions(mode: WorksheetMode): Record<WorksheetPermissionPoint, boolean> {
// Initialize all permission points to false first
const pointsToSet: Record<WorksheetPermissionPoint, boolean> = {} as Record<WorksheetPermissionPoint, boolean>;
Object.values(WorksheetPermissionPoint).forEach((point) => {
pointsToSet[point] = false;
});
switch (mode) {
case 'editable':
// Fully editable - set all to true
Object.values(WorksheetPermissionPoint).forEach((point) => {
pointsToSet[point] = true;
});
break;
case 'readOnly':
// Fully read-only - only View is allowed
pointsToSet[WorksheetPermissionPoint.View] = true;
// All other permissions remain false
break;
case 'filterOnly':
// Can only filter/sort
pointsToSet[WorksheetPermissionPoint.View] = true;
pointsToSet[WorksheetPermissionPoint.Sort] = true;
pointsToSet[WorksheetPermissionPoint.Filter] = true;
// All other permissions remain false
break;
}
return pointsToSet;
}
/**
* Batch set multiple permission points efficiently
* @private
*/
private async _batchSetPermissionPoints(pointsToSet: Record<WorksheetPermissionPoint, boolean>): Promise<void> {
// Note: IPermissionService doesn't have a batch update API, so we update individually
// but we optimize by only updating the snapshot once at the end
const pointsChanged: Array<{ point: WorksheetPermissionPoint; value: boolean; oldValue: boolean }> = [];
for (const [point, value] of Object.entries(pointsToSet)) {
const pointKey = point as WorksheetPermissionPoint;
const PointClass = WORKSHEET_PERMISSION_POINT_MAP[pointKey];
if (!PointClass) {
throw new Error(`Unknown worksheet permission point: ${pointKey}`);
}
const oldValue = this.getPoint(pointKey);
if (oldValue === value) {
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);
pointsChanged.push({ point: pointKey, value, oldValue });
}
// Update snapshot once at the end (the Observable stream will pick up the changes automatically)
if (pointsChanged.length > 0) {
const newSnapshot = this._buildSnapshot();
this._permissionSubject.next(newSnapshot);
}
}
/**
* Set the worksheet to read-only mode.
* @returns {Promise<void>} A promise that resolves when the mode is set.
* @example
* ```ts
* const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
* const permission = worksheet?.getWorksheetPermission();
* await permission?.setReadOnly();
* ```
*/
async setReadOnly(): Promise<void> {
await this.setMode('readOnly');
}
/**
* Set the worksheet to editable mode.
* @returns {Promise<void>} A promise that resolves when the mode is set.
* @example
* ```ts
* const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
* const permission = worksheet?.getWorksheetPermission();
* await permission?.setEditable();
* ```
*/
async setEditable(): Promise<void> {
await this.setMode('editable');
}
/**
* Check if the worksheet is editable.
* @returns {boolean} true if the worksheet can be edited, false otherwise.
* @example
* ```ts
* const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
* const permission = worksheet?.getWorksheetPermission();
* if (permission?.canEdit()) {
* console.log('Worksheet is editable');
* }
* ```
*/
canEdit(): boolean {
return this.getPoint(WorksheetPermissionPoint.Edit);
}
/**
* Check if a specific cell can be edited.
* @param {number} row Row index.
* @param {number} col Column index.
* @returns {boolean} true if the cell can be edited, false otherwise.
* @example
* ```ts
* const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
* const permission = worksheet?.getWorksheetPermission();
* const canEdit = permission?.canEditCell(0, 0);
* console.log(canEdit);
* ```
*/
canEditCell(row: number, col: number): boolean {
// First check worksheet-level permission
if (!this.canEdit()) {
return false;
}
// Check if there are range protection rules covering this cell
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
) {
// Cell is within protected range, check the rule's edit permission
return this._getRuleEditPermission(rule);
}
}
}
return true;
}
/**
* Check if a specific cell can be viewed.
* @param {number} _row Row index (unused, for API consistency).
* @param {number} _col Column index (unused, for API consistency).
* @returns {boolean} true if the cell can be viewed, false otherwise.
* @example
* ```ts
* const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
* const permission = worksheet?.getWorksheetPermission();
* const canView = permission?.canViewCell(0, 0);
* console.log(canView);
* ```
*/
canViewCell(_row: number, _col: number): boolean {
// View permission is usually true by default
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.
* @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.
* @example
* ```ts
* const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
* const permission = worksheet?.getWorksheetPermission();
* await permission?.setPoint(WorksheetPermissionPoint.InsertRow, false);
* ```
*/
async setPoint(point: WorksheetPermissionPoint, value: boolean): Promise<void> {
const PointClass = WORKSHEET_PERMISSION_POINT_MAP[point];
if (!PointClass) {
throw new Error(`Unknown worksheet permission point: ${point}`);
}
const oldValue = this.getPoint(point);
if (oldValue === value) {
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);
// Update snapshot (the Observable stream will automatically emit the change)
const newSnapshot = this._buildSnapshot();
this._permissionSubject.next(newSnapshot);
}
/**
* Get the value of a specific permission point.
* @param {WorksheetPermissionPoint} point The permission point to query.
* @returns {boolean} true if allowed, false if denied.
* @example
* ```ts
* const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
* const permission = worksheet?.getWorksheetPermission();
* const canInsertRow = permission?.getPoint(WorksheetPermissionPoint.InsertRow);
* console.log(canInsertRow);
* ```
*/
getPoint(point: WorksheetPermissionPoint): boolean {
const PointClass = WORKSHEET_PERMISSION_POINT_MAP[point];
if (!PointClass) {
throw new Error(`Unknown worksheet permission point: ${point}`);
}
const instance = new PointClass(this._unitId, this._subUnitId);
const permissionPoint = this._permissionService.getPermissionPoint(instance.id);
return permissionPoint?.value ?? true; // Default to true (allowed)
}
/**
* Get a snapshot of all permission points.
* @returns {WorksheetPermissionSnapshot} An object containing all permission point values.
* @example
* ```ts
* const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
* const permission = worksheet?.getWorksheetPermission();
* const snapshot = permission?.getSnapshot();
* console.log(snapshot);
* ```
*/
getSnapshot(): WorksheetPermissionSnapshot {
return this._buildSnapshot();
}
/**
* Apply a permission configuration to the worksheet.
* @param {IWorksheetPermissionConfig} config The configuration to apply.
* @returns {Promise<void>} A promise that resolves when the configuration is applied.
* @example
* ```ts
* const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
* const permission = worksheet?.getWorksheetPermission();
* await permission?.applyConfig({
* mode: 'readOnly',
* points: {
* [WorksheetPermissionPoint.View]: true,
* [WorksheetPermissionPoint.Edit]: false
* }
* });
* ```
*/
async applyConfig(config: IWorksheetPermissionConfig): Promise<void> {
// Apply mode
if (config.mode) {
await this.setMode(config.mode);
}
// Apply permission point configuration
if (config.points) {
for (const [point, value] of Object.entries(config.points)) {
if (typeof value === 'boolean') {
await this.setPoint(point as WorksheetPermissionPoint, value);
}
}
}
// Batch create range protection
if (config.rangeProtections && config.rangeProtections.length > 0) {
const protectionConfigs = config.rangeProtections.map((protection: { rangeRefs: string[]; options?: IRangeProtectionOptions }) => ({
ranges: protection.rangeRefs.map((rangeRef: string) => this._worksheet.getRange(rangeRef)),
options: protection.options,
}));
await this.protectRanges(protectionConfigs);
}
}
/**
* 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.
* @example
* ```ts
* const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
* const permission = worksheet?.getWorksheetPermission();
* const rules = await permission?.protectRanges([
* {
* ranges: [worksheet.getRange('A1:B2')],
* options: { name: 'Protected Area 1', allowEdit: false, allowView: true }
* },
* {
* ranges: [worksheet.getRange('C3:D4')],
* options: { name: 'Protected Area 2', allowEdit: true, allowView: false }
* }
* ]);
* console.log(rules);
* ```
*/
async protectRanges(
configs: Array<{
ranges: FRange[];
options?: IRangeProtectionOptions;
}>
): Promise<IRangeProtectionRule[]> {
if (!configs || configs.length === 0) {
throw new Error('Configs cannot be empty');
}
// 1. Create permissionId in parallel
const permissionIds = await Promise.all(
configs.map((c) =>
this._authzIoService.create({
objectType: UnitObject.SelectRange,
selectRangeObject: {
collaborators: c.options?.allowedUsers?.map((id) => ({ id, role: UnitRole.Editor, subject: undefined })) ?? [],
unitID: this._unitId,
name: c.options?.name || '',
scope: undefined,
},
})
)
);
// 2. Build rule parameters with proper viewState and editState
const ruleParams = configs.map((c, i) => {
const viewState = this._determineViewState(c.options);
const editState = this._determineEditState(c.options);
return {
permissionId: permissionIds[i],
unitType: UnitObject.SelectRange,
unitId: this._unitId,
subUnitId: this._subUnitId,
ranges: c.ranges.map((r) => r.getRange()),
id: this._rangeProtectionRuleModel.createRuleId(this._unitId, this._subUnitId),
description: c.options?.name || '',
viewState,
editState,
};
});
// 3. Execute command to add multiple rules at once
const result = await this._commandService.executeCommand(AddRangeProtectionMutation.id, {
unitId: this._unitId,
subUnitId: this._subUnitId,
rules: ruleParams,
});
if (!result) {
throw new Error('Failed to create range protection rules');
}
// 4. Set permission points for each rule
await Promise.all(
configs.map((c, i) => this._setPermissionPoints(permissionIds[i], c.options))
);
// 5. Create RangeProtectionRule objects
const rules = ruleParams.map((param, i) =>
this._injector.createInstance(
FRangeProtectionRule,
this._unitId,
this._subUnitId,
param.id,
param.permissionId,
configs[i].ranges,
configs[i].options || {}
)
);
// The Observable stream will automatically emit the change event
return rules;
}
/**
* Determine view state from options
* @private
*/
private _determineViewState(options?: IRangeProtectionOptions): ViewStateEnum {
if (options?.allowViewByOthers === false) {
return ViewStateEnum.NoOneElseCanView; // Only owner can view
}
return ViewStateEnum.OthersCanView;
}
/**
* Determine edit state from options
* @private
*/
private _determineEditState(options?: IRangeProtectionOptions): EditStateEnum {
if (options?.allowEdit === true && options?.allowedUsers?.length) {
return EditStateEnum.DesignedUserCanEdit; // Designed users can edit
}
return EditStateEnum.OnlyMe;
}
/**
* Set permission points based on options (for local runtime control)
* @private
*/
private async _setPermissionPoints(permissionId: string, options?: IRangeProtectionOptions): Promise<void> {
if (!options) {
return;
}
const getPermissionValue = (option: boolean | string[] | undefined, defaultValue: boolean): boolean => {
if (option === undefined) {
return defaultValue;
}
if (typeof option === 'boolean') {
return option;
}
return true; // For string[] whitelist
};
// Set permission points
await this._setPermissionPoint(permissionId, RangePermissionPoint.Edit, getPermissionValue(options.allowEdit, false));
await this._setPermissionPoint(permissionId, RangePermissionPoint.View, getPermissionValue(options.allowViewByOthers, true));
}
/**
* Set a single permission point
* @private
*/
private async _setPermissionPoint(permissionId: string, point: RangePermissionPoint, value: boolean): Promise<void> {
const PermissionPointClass = RANGE_PERMISSION_POINT_MAP[point];
if (!PermissionPointClass) {
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);
}
/**
* Remove multiple protection rules at once.
* @param {string[]} ruleIds Array of rule IDs to remove.
* @returns {Promise<void>} A promise that resolves when the rules are removed.
* @example
* ```ts
* const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
* const permission = worksheet?.getWorksheetPermission();
* await permission?.unprotectRules(['rule1', 'rule2']);
* ```
*/
async unprotectRules(ruleIds: string[]): Promise<void> {
if (!ruleIds || ruleIds.length === 0) {
return;
}
await this._commandService.executeCommand(DeleteRangeProtectionMutation.id, {
unitId: this._unitId,
subUnitId: 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.
* @example
* ```ts
* const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
* const permission = worksheet?.getWorksheetPermission();
* const rules = await permission?.listRangeProtectionRules();
* console.log(rules);
* ```
*/
async listRangeProtectionRules(): Promise<IRangeProtectionRule[]> {
return this._buildRangeProtectionRules();
}
/**
* Subscribe to permission changes (simplified interface for users not familiar with RxJS).
* @param {Function} listener Callback function to be called when permissions change.
* @returns {UnsubscribeFn} Unsubscribe function.
* @example
* ```ts
* const worksheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
* const permission = worksheet?.getWorksheetPermission();
* const unsubscribe = permission?.subscribe((snapshot) => {
* console.log('Permission changed:', snapshot);
* });
* // Later, to stop listening:
* unsubscribe?.();
* ```
*/
subscribe(listener: (snapshot: WorksheetPermissionSnapshot) => void): UnsubscribeFn {
const subscription = this.permission$.subscribe(listener);
return () => subscription.unsubscribe();
}
/**
* Clean up resources
*/
dispose(): void {
this._subscriptions.forEach((sub) => sub.unsubscribe());
this._permissionSubject.complete();
this._rangeRulesSubject.complete();
}
}
@@ -0,0 +1,22 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { FRangePermission } from './f-range-permission';
export { FRangeProtectionRule } from './f-range-protection-rule';
export { FWorkbookPermission } from './f-workbook-permission';
export { FWorksheetPermission } from './f-worksheet-permission';
export * from './permission-point-map';
export * from './permission-types';
@@ -0,0 +1,132 @@
/**
* 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 { RangePermissionPointConstructor, WorkbookPermissionPointConstructor, WorkSheetPermissionPointConstructor } from '@univerjs/core';
import {
RangeProtectionPermissionDeleteProtectionPoint,
RangeProtectionPermissionEditPoint,
RangeProtectionPermissionManageCollaPoint,
RangeProtectionPermissionViewPoint,
WorkbookCommentPermission,
WorkbookCopyPermission,
WorkbookCopySheetPermission,
WorkbookCreateProtectPermission,
WorkbookCreateSheetPermission,
WorkbookDeleteColumnPermission,
WorkbookDeleteRowPermission,
WorkbookDeleteSheetPermission,
WorkbookDuplicatePermission,
WorkbookEditablePermission,
WorkbookExportPermission,
WorkbookHideSheetPermission,
WorkbookHistoryPermission,
WorkbookInsertColumnPermission,
WorkbookInsertRowPermission,
WorkbookManageCollaboratorPermission,
WorkbookMoveSheetPermission,
WorkbookPrintPermission,
WorkbookRecoverHistoryPermission,
WorkbookRenameSheetPermission,
WorkbookSharePermission,
WorkbookViewHistoryPermission,
WorkbookViewPermission,
WorksheetCopyPermission,
WorksheetDeleteColumnPermission,
WorksheetDeleteProtectionPermission,
WorksheetDeleteRowPermission,
WorksheetEditExtraObjectPermission,
WorksheetEditPermission,
WorksheetFilterPermission,
WorksheetInsertColumnPermission,
WorksheetInsertHyperlinkPermission,
WorksheetInsertRowPermission,
WorksheetManageCollaboratorPermission,
WorksheetPivotTablePermission,
WorksheetSelectProtectedCellsPermission,
WorksheetSelectUnProtectedCellsPermission,
WorksheetSetCellStylePermission,
WorksheetSetCellValuePermission,
WorksheetSetColumnStylePermission,
WorksheetSetRowStylePermission,
WorksheetSortPermission,
WorksheetViewPermission,
} from '@univerjs/sheets';
import { RangePermissionPoint, WorkbookPermissionPoint, WorksheetPermissionPoint } from './permission-types';
/**
* Mapping table from Workbook permission point enum to class constructors
*/
export const WORKBOOK_PERMISSION_POINT_MAP: Record<WorkbookPermissionPoint, WorkbookPermissionPointConstructor> = {
[WorkbookPermissionPoint.Edit]: WorkbookEditablePermission,
[WorkbookPermissionPoint.View]: WorkbookViewPermission,
[WorkbookPermissionPoint.Print]: WorkbookPrintPermission,
[WorkbookPermissionPoint.Export]: WorkbookExportPermission,
[WorkbookPermissionPoint.Share]: WorkbookSharePermission,
[WorkbookPermissionPoint.CopyContent]: WorkbookCopyPermission,
[WorkbookPermissionPoint.DuplicateFile]: WorkbookDuplicatePermission,
[WorkbookPermissionPoint.Comment]: WorkbookCommentPermission,
[WorkbookPermissionPoint.ManageCollaborator]: WorkbookManageCollaboratorPermission,
[WorkbookPermissionPoint.CreateSheet]: WorkbookCreateSheetPermission,
[WorkbookPermissionPoint.DeleteSheet]: WorkbookDeleteSheetPermission,
[WorkbookPermissionPoint.RenameSheet]: WorkbookRenameSheetPermission,
[WorkbookPermissionPoint.MoveSheet]: WorkbookMoveSheetPermission,
[WorkbookPermissionPoint.HideSheet]: WorkbookHideSheetPermission,
[WorkbookPermissionPoint.ViewHistory]: WorkbookViewHistoryPermission,
[WorkbookPermissionPoint.ManageHistory]: WorkbookHistoryPermission,
[WorkbookPermissionPoint.RecoverHistory]: WorkbookRecoverHistoryPermission,
[WorkbookPermissionPoint.CreateProtection]: WorkbookCreateProtectPermission,
[WorkbookPermissionPoint.InsertRow]: WorkbookInsertRowPermission,
[WorkbookPermissionPoint.InsertColumn]: WorkbookInsertColumnPermission,
[WorkbookPermissionPoint.DeleteRow]: WorkbookDeleteRowPermission,
[WorkbookPermissionPoint.DeleteColumn]: WorkbookDeleteColumnPermission,
[WorkbookPermissionPoint.CopySheet]: WorkbookCopySheetPermission,
};
/**
* Mapping table from Worksheet permission point enum to class constructors
*/
export const WORKSHEET_PERMISSION_POINT_MAP: Record<WorksheetPermissionPoint, WorkSheetPermissionPointConstructor> = {
[WorksheetPermissionPoint.Edit]: WorksheetEditPermission,
[WorksheetPermissionPoint.View]: WorksheetViewPermission,
[WorksheetPermissionPoint.Copy]: WorksheetCopyPermission,
[WorksheetPermissionPoint.SetCellValue]: WorksheetSetCellValuePermission,
[WorksheetPermissionPoint.SetCellStyle]: WorksheetSetCellStylePermission,
[WorksheetPermissionPoint.SetRowStyle]: WorksheetSetRowStylePermission,
[WorksheetPermissionPoint.SetColumnStyle]: WorksheetSetColumnStylePermission,
[WorksheetPermissionPoint.InsertRow]: WorksheetInsertRowPermission,
[WorksheetPermissionPoint.InsertColumn]: WorksheetInsertColumnPermission,
[WorksheetPermissionPoint.DeleteRow]: WorksheetDeleteRowPermission,
[WorksheetPermissionPoint.DeleteColumn]: WorksheetDeleteColumnPermission,
[WorksheetPermissionPoint.Sort]: WorksheetSortPermission,
[WorksheetPermissionPoint.Filter]: WorksheetFilterPermission,
[WorksheetPermissionPoint.PivotTable]: WorksheetPivotTablePermission,
[WorksheetPermissionPoint.InsertHyperlink]: WorksheetInsertHyperlinkPermission,
[WorksheetPermissionPoint.EditExtraObject]: WorksheetEditExtraObjectPermission,
[WorksheetPermissionPoint.ManageCollaborator]: WorksheetManageCollaboratorPermission,
[WorksheetPermissionPoint.DeleteProtection]: WorksheetDeleteProtectionPermission,
[WorksheetPermissionPoint.SelectProtectedCells]: WorksheetSelectProtectedCellsPermission,
[WorksheetPermissionPoint.SelectUnProtectedCells]: WorksheetSelectUnProtectedCellsPermission,
};
/**
* Mapping table from Range permission point enum to class constructors
*/
export const RANGE_PERMISSION_POINT_MAP: Record<RangePermissionPoint, RangePermissionPointConstructor> = {
[RangePermissionPoint.Edit]: RangeProtectionPermissionEditPoint,
[RangePermissionPoint.View]: RangeProtectionPermissionViewPoint,
[RangePermissionPoint.ManageCollaborator]: RangeProtectionPermissionManageCollaPoint,
[RangePermissionPoint.Delete]: RangeProtectionPermissionDeleteProtectionPoint,
};
@@ -0,0 +1,581 @@
/**
* 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 { IUser } from '@univerjs/protocol';
import type { Observable } from 'rxjs';
import type { FRange } from '../f-range';
/**
* ========================
* Basic Types / Enums
* ========================
*/
/**
* User role in a unit (Workbook)
*/
export enum UnitRole {
Reader = 0,
Editor = 1,
Owner = 2,
}
/**
* User reference information
*/
export interface IUserRef {
/** User ID (defined by host system) */
id: string;
/** Display name */
displayName?: string;
/** Email address */
email?: string;
}
/**
* Collaborator information
*/
export interface ICollaborator {
/** User information */
user: IUserRef;
/** Role */
role: UnitRole;
}
/**
* Workbook-level permission point enumeration
*/
export enum WorkbookPermissionPoint {
/** Edit permission */
Edit = 'WorkbookEdit',
/** View permission */
View = 'WorkbookView',
/** Print permission */
Print = 'WorkbookPrint',
/** Export permission */
Export = 'WorkbookExport',
/** Share permission */
Share = 'WorkbookShare',
/** Copy content permission */
CopyContent = 'WorkbookCopy',
/** Duplicate file permission */
DuplicateFile = 'WorkbookDuplicate',
/** Comment permission */
Comment = 'WorkbookComment',
/** Manage collaborators permission */
ManageCollaborator = 'WorkbookManageCollaborator',
/** Create sheet permission */
CreateSheet = 'WorkbookCreateSheet',
/** Delete sheet permission */
DeleteSheet = 'WorkbookDeleteSheet',
/** Rename sheet permission */
RenameSheet = 'WorkbookRenameSheet',
/** Move sheet permission */
MoveSheet = 'WorkbookMoveSheet',
/** Hide sheet permission */
HideSheet = 'WorkbookHideSheet',
/** View history permission */
ViewHistory = 'WorkbookViewHistory',
/** Manage history permission */
ManageHistory = 'WorkbookHistory',
/** Recover history permission */
RecoverHistory = 'WorkbookRecoverHistory',
/** Create protection permission */
CreateProtection = 'WorkbookCreateProtect',
/** Insert row permission */
InsertRow = 'WorkbookInsertRow',
/** Insert column permission */
InsertColumn = 'WorkbookInsertColumn',
/** Delete row permission */
DeleteRow = 'WorkbookDeleteRow',
/** Delete column permission */
DeleteColumn = 'WorkbookDeleteColumn',
/** Copy sheet permission */
CopySheet = 'WorkbookCopySheet',
}
/**
* Worksheet-level permission point enumeration
*/
export enum WorksheetPermissionPoint {
/** Edit permission */
Edit = 'WorksheetEdit',
/** View permission */
View = 'WorksheetView',
/** Copy permission */
Copy = 'WorksheetCopy',
/** Set cell value permission */
SetCellValue = 'WorksheetSetCellValue',
/** Set cell style permission */
SetCellStyle = 'WorksheetSetCellStyle',
/** Set row style permission */
SetRowStyle = 'WorksheetSetRowStyle',
/** Set column style permission */
SetColumnStyle = 'WorksheetSetColumnStyle',
/** Insert row permission */
InsertRow = 'WorksheetInsertRow',
/** Insert column permission */
InsertColumn = 'WorksheetInsertColumn',
/** Delete row permission */
DeleteRow = 'WorksheetDeleteRow',
/** Delete column permission */
DeleteColumn = 'WorksheetDeleteColumn',
/** Sort permission */
Sort = 'WorksheetSort',
/** Filter permission */
Filter = 'WorksheetFilter',
/** Pivot table permission */
PivotTable = 'WorksheetPivotTable',
/** Insert hyperlink permission */
InsertHyperlink = 'WorksheetInsertHyperlink',
/** Edit extra object permission */
EditExtraObject = 'WorksheetEditExtraObject',
/** Manage collaborators permission */
ManageCollaborator = 'WorksheetManageCollaborator',
/** Delete protection permission */
DeleteProtection = 'WorksheetDeleteProtection',
/** Select protected cells permission */
SelectProtectedCells = 'WorksheetSelectProtectedCells',
/** Select unprotected cells permission */
SelectUnProtectedCells = 'WorksheetSelectUnProtectedCells',
}
/**
* Range-level permission point enumeration
*/
export enum RangePermissionPoint {
/** Edit permission */
Edit = 'RangeEdit',
/** View permission */
View = 'RangeView',
ManageCollaborator = 'RangeManageCollaborator',
Delete = 'RangeDeleteProtection',
}
/**
* Workbook permission mode
*/
export type WorkbookMode = 'owner' | 'editor' | 'viewer' | 'commenter';
/**
* Worksheet permission mode
*/
export type WorksheetMode =
| 'editable' // Fully editable
| 'readOnly' // Fully read-only
| 'filterOnly'; // Filter / sort only
/**
* Workbook permission snapshot (state of all permission points)
*/
export type WorkbookPermissionSnapshot = Record<WorkbookPermissionPoint, boolean>;
/**
* Worksheet permission snapshot (state of all permission points)
*/
export type WorksheetPermissionSnapshot = Record<WorksheetPermissionPoint, boolean>;
/**
* Range permission snapshot (state of all permission points)
*/
export type RangePermissionSnapshot = Record<RangePermissionPoint, boolean>;
/**
* Unsubscribe function type
*/
export type UnsubscribeFn = () => void;
/**
* ========================
* Range Protection Configuration and Rules
* ========================
*/
/**
* Range protection options configuration
*/
export interface IRangeProtectionOptions {
/** Whether to allow current user to edit (default false = protected, not editable) */
allowEdit?: boolean;
/** Whitelist of users allowed to edit; empty means determined by role or global policy */
allowedUsers?: string[];
allowViewByOthers?: boolean;
/** Rule name for UI display and management */
name?: string;
/** Custom metadata (logs, tags, etc.) */
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
*/
export interface ICellPermissionDebugRuleInfo {
ruleId: string;
/** Range reference string list, e.g., ['A1:B10', 'D1:D5'] */
rangeRefs: string[];
options: IRangeProtectionOptions;
}
/**
* Cell permission debug information
*/
export interface ICellPermissionDebugInfo {
row: number;
col: number;
/** List of protection rules that apply */
hitRules: ICellPermissionDebugRuleInfo[];
}
/**
* ========================
* Facade: WorkbookPermission
* ========================
*/
/**
* Workbook-level permission Facade interface
*/
export interface IWorkbookPermission {
/**
* High-level mode setting: By Owner / Editor / Viewer / Commenter semantics
* Internally automatically combines multiple WorkbookPermissionPoints
*/
setMode(mode: WorkbookMode): Promise<void>;
/** Shortcut: Set workbook to read-only (equivalent to setMode('viewer')) */
setReadOnly(): Promise<void>;
/** Shortcut: Set workbook to editable (equivalent to setMode('editor') or owner subset) */
setEditable(): Promise<void>;
/** Whether current user can edit this workbook (calculated from combined permissions) */
canEdit(): boolean;
/**
* Collaborator management (wraps IAuthzIoService)
*/
/** Batch set collaborators (replace mode, overwrites existing collaborator list) */
setCollaborators(collaborators: Array<{ user: IUser; role: UnitRole }>): Promise<void>;
/** Add a single collaborator */
addCollaborator(user: IUser, role: UnitRole): Promise<void>;
/** Update collaborator role and information */
updateCollaborator(user: IUser, role: UnitRole): Promise<void>;
/** Remove collaborator */
removeCollaborator(userId: string): Promise<void>;
/** Batch remove collaborators */
removeCollaborators(userIds: string[]): Promise<void>;
/** List all collaborators */
listCollaborators(): Promise<ICollaborator[]>;
/**
* Low-level point operations: Directly set boolean value of a WorkbookPermissionPoint
*/
setPoint(point: WorkbookPermissionPoint, value: boolean): Promise<void>;
/** Read current value of a point (synchronous, reads from local state) */
getPoint(point: WorkbookPermissionPoint): boolean;
/** Get snapshot of all current points */
getSnapshot(): WorkbookPermissionSnapshot;
/**
* ========================
* RxJS Observable Reactive Interface
* ========================
*/
/**
* Permission snapshot change stream (BehaviorSubject, immediately provides current state on subscription)
* Triggers when any permission point changes
*/
readonly permission$: Observable<WorkbookPermissionSnapshot>;
/**
* Single permission point change stream
* For scenarios that only care about specific permission point changes
*/
readonly pointChange$: Observable<{
point: WorkbookPermissionPoint;
value: boolean;
oldValue: boolean;
}>;
/**
* Collaborator change stream
*/
readonly collaboratorChange$: Observable<{
type: 'add' | 'update' | 'delete';
collaborator: ICollaborator;
}>;
/**
* Compatibility method: Simplified subscription (for users unfamiliar with RxJS)
* Internally implemented based on permission$ Observable
*/
subscribe(listener: (snapshot: WorkbookPermissionSnapshot) => void): UnsubscribeFn;
}
/**
* ========================
* Facade: WorksheetPermission
* ========================
*/
/**
* Worksheet permission configuration
*/
export interface IWorksheetPermissionConfig {
/** One-time mode setting */
mode?: WorksheetMode;
/** Point-level configuration patch */
points?: Partial<Record<WorksheetPermissionPoint, boolean>>;
/** Batch range protection configuration (optional, for simplified scenarios) */
rangeProtections?: Array<{
rangeRefs: string[]; // e.g., ['A1:B10', 'D1:D5']
options?: IRangeProtectionOptions; // If not provided, defaults to "protected, not editable"
}>;
}
/**
* 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;
}