univer
univer copied to clipboard
feat(docs): support setting read-only
close #xxx
Description
This PR implements read-only mode for Univer Docs
Key Changes
-
Permission System
- Added
DocumentEditablePermissionpermission point (similar to Sheets'WorkbookEditablePermission) - Integrated with
IPermissionServicefor centralized permission management
- Added
-
Facade API
- Added
FDocument.setEditable(value: boolean)method - Usage:
univerAPI.getActiveDocument().setEditable(false)
- Added
-
Command & UI Protection
- Added permission checks to all editing commands (insert, delete, paste, list operations, etc.)
- Added
getCurrentDocRangeDisable$()observable for reactive UI updates - Updated all menu items and context menus to respect read-only state
Testing
- step1: replace with the following code
examples/src/docs/main.tspnpm run dev - step2: pnpm run dev
- step3: go to localhost:3000/docs
Then you will find that the docs are set to read-only status after 3 seconds
import { LocaleType, LogLevel, Univer, UniverInstanceType, UserManagerService } from '@univerjs/core';
import { FUniver } from '@univerjs/core/facade';
import { UniverDebuggerPlugin } from '@univerjs/debugger';
import { UniverDocsPlugin } from '@univerjs/docs';
import { UniverDocsDrawingUIPlugin } from '@univerjs/docs-drawing-ui';
import { UniverDocsHyperLinkUIPlugin } from '@univerjs/docs-hyper-link-ui';
import { UniverDocsMentionUIPlugin } from '@univerjs/docs-mention-ui';
import { UniverDocsQuickInsertUIPlugin } from '@univerjs/docs-quick-insert-ui';
import { UniverDocsThreadCommentUIPlugin } from '@univerjs/docs-thread-comment-ui';
import { UniverDocsUIPlugin } from '@univerjs/docs-ui';
import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula';
import { UniverRenderEnginePlugin } from '@univerjs/engine-render';
import { DEFAULT_DOCUMENT_DATA_SIMPLE } from '@univerjs/mockdata';
import zhCN from '@univerjs/mockdata/locales/zh-CN';
import { UniverUIPlugin } from '@univerjs/ui';
import '@univerjs/docs-ui/facade';
import '../global.css';
/* eslint-disable node/prefer-global/process */
const IS_E2E: boolean = !!process.env.IS_E2E;
// univer
const univer = new Univer({
locale: LocaleType.ZH_CN,
locales: {
[LocaleType.ZH_CN]: zhCN,
},
logLevel: LogLevel.VERBOSE,
});
// core plugins
univer.registerPlugin(UniverRenderEnginePlugin);
univer.registerPlugin(UniverFormulaEnginePlugin);
univer.registerPlugin(UniverUIPlugin, {
container: 'app',
});
univer.registerPlugin(UniverDocsPlugin);
univer.registerPlugin(UniverDocsUIPlugin, {
container: 'univerdoc',
layout: {
docContainerConfig: {
innerLeft: false,
},
},
disableAutoFocus: true, // Disable auto-focus
});
univer.registerPlugin(UniverDocsDrawingUIPlugin);
univer.registerPlugin(UniverDocsThreadCommentUIPlugin);
univer.registerPlugin(UniverDocsHyperLinkUIPlugin);
univer.registerPlugin(UniverDocsMentionUIPlugin);
univer.registerPlugin(UniverDocsQuickInsertUIPlugin);
if (!IS_E2E) {
univer.createUnit(UniverInstanceType.UNIVER_DOC, DEFAULT_DOCUMENT_DATA_SIMPLE);
univer.registerPlugin(UniverDebuggerPlugin);
} else {
univer.registerPlugin(UniverDebuggerPlugin, {
fab: false,
performanceMonitor: {
enabled: false,
},
});
}
// use for console test
declare global {
// eslint-disable-next-line ts/naming-convention
interface Window {
univer?: Univer;
}
}
window.univer = univer;
const injector = univer.__getInjector();
const userManagerService = injector.get(UserManagerService);
const mockUser = {
userID: 'Owner_qxVnhPbQ',
name: 'Owner',
avatar: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAInSURBVHgBtZU9TxtBEIbfWRzFSIdkikhBSqRQkJqkCKTCFkqVInSUSaT0wC8w/gXxD4gU2nRJkXQWhAZowDUUWKIwEgWWbEEB3mVmx3dn4DA2nB/ppNuPeWd29mMIPXDr+RxwtgRHeW6+guNPRxogqnL7Dwz9psJ27S4NShaeZTH3kwXy6I81dlRKcmRui88swdq9AcSFL7Buz1Vmlns64MiLsCjzwnIYHLH57tbfFbs7KRaXyEU8FVZofqccOfA5l7Q8LPIkGrwnb2RPNEXWFVMUF3L+kDCk0btDDAMzOm5YfAHDwp4tG74wnzAsiOYMnJ3GoDybA7IT98/jm5+JNnfiIzAS6LlqHQBN/i6b2t/cV1Hh6BfwYlHnHP4AXi5q/8kmMMpOs8+BixZw/Fd6xUEHEbnkgclvQP2fGp7uShRKnQ3G32rkjV1th8JhIGG7tR/JyjGteSOZELwGMmNqIIigRCLRh2OZIE6BjItdd7pCW6Uhm1zzkUtungSxwEUzNpQ+GQumtH1ej1MqgmNT6vwmhCq5yuwq56EYTbgeQUz3yvrpV1b4ok3nYJ+eYhgYmjRUqErx2EDq0Fr8FhG++iqVGqxlUJI/70Ar0UgJaWHj6hYVHJrfKssAHot1JfqwE9WVWzXZVd5z2Ws/4PnmtEjkXeKJDvxUecLbWOXH/DP6QQ4J72NS0adedp1aseBfXP8odlZFfPvBF7SN/8hky1TYuPOAXAEipMx15u5ToAAAAABJRU5ErkJggg==',
anonymous: false,
canBindAnonymous: false,
};
userManagerService.setCurrentUser(mockUser);
window.univerAPI = FUniver.newAPI(univer);
// core: set readonly
setTimeout(() => {
const univerAPI = window.univerAPI;
const activeDocument = univerAPI?.getActiveDocument();
if (activeDocument) {
// 设置文档为只读模式(类似 Excel 的 setEditable)
activeDocument.setEditable(false);
console.log('✅ 文档已设置为只读模式');
console.log('💡 可以在控制台使用以下命令切换:');
console.log(' - window.univerAPI.getActiveDocument().setEditable(false) // 只读');
console.log(' - window.univerAPI.getActiveDocument().setEditable(true) // 可编辑');
}
}, 3000);
Pull Request Checklist
- [x] Related tickets or issues have been linked in the PR description (or missing issue).
- [x] Naming convention is followed.
- [x] Unit tests have been added for the changes (if applicable).
- [x] Breaking changes have been documented (or no breaking changes introduced in this PR).