mirror of
https://github.com/dream-num/univer.git
synced 2026-09-24 15:32:18 +08:00
refactor(sheets): refactor facade api of sheet permission module (#6127)
Co-authored-by: Wpxp123456 <2677556700@qq.com>
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user