【技术教程】如何为ONLYOFFICE协作空间开发文件过滤UI插件
在企业运营的过程中,文件体量快速增长,文件结构变得复杂且层级较深。而文件过滤 UI 插件的出现帮助用户通过直观的筛选条件精准定位内容,解决文件增长后的 "信息过载" 问题,从根本上提升用户的内容获取效率和协作体验。阅读本文,了解如何开发文件过滤UI插件。
关于 ONLYOFFICE 协作空间
ONLYOFFICE 协作空间是一个协同办公平台,能够帮助用户更好地与客户、业务合作伙伴、承包商及第三方,进行文档、表格、幻灯片、PDF 和表单的在线编辑与协作。设置灵活的访问权限和用户角色设置,可支持用户对整个或单独房间的访问权限调整。
开发者版是我们开源解决方案的商业版本,主要用途是集成至您自有的商业软件和服务器。这一版本还支持自定义品牌选项、连接外部服务和存储等。查看视频了解更多信息:
什么是开发者版 ONLYOFFICE 协作空间?无缝集成至您的软件和服务器
关于 ONLYOFFICE 协作空间插件
协作空间插件是一种用于增强团队协作效率的工具,通常集成在办公软件、项目管理平台或浏览器中,支持多人实时编辑、任务分配、文件共享等功能。ONLYOFFICE 协作空间插件 SDK 可以为开发者提供丰富的接口,帮助开发自定义插件并集成到协作空间门户中。
您可以查看此文章了解完整的插件开发过程。
文件过滤插件的优势
在协作空间中,随着文件数量增长,尤其是团队共同管理大量文档、表格、演示文稿时,用户需要快速定位特定文件。插件的过滤功能(如按类型、创建日期、作者、标签等筛选)能减少查找时间,直接提升工作效率:
除此之外,还可以避免切换视图、提升使用体验感和界面整洁度,使其更符合专业团队对文档组织的严格要求。
下面就让我们一起学习,如何创建插件,为房间添加上下文菜单操作,打开带有文件扩展名过滤器的模态,并使用 UI 组件显示匹配文件的列表。
操作指南
前期准备
请确保您已运行协作空间服务器,并全局安装协作空间插件 SDK:
npm i -g @onlyoffice/docspace-plugin-sdk
步骤 1:创建插件
1. 使用 CLI 初始化插件:
npx create-docspace-plugin
2. 填写基本元数据:插件名称、版本、作者、描述、logo、许可证、主页。
3. 从可用选项列表中选择所需的范围。使用箭头键突出显示上下文菜单,按 Space 键选择,然后按 Enter 键确认并生成插件模板。
步骤 2:确认插件配置
确保 package.json 包含所有必需字段。最重要的是,确保它包含:
{"scopes": ["ContextMenu"]
}
另外,请验证 scripts/createZip.js 文件是否存在。此脚本将:
- 编译插件;
- 将所有内容打包到 dist/plugin.zip 中。
步骤 3:审查并扩展插件代码
默认情况下,插件模板在 src/index.ts 文件中提供了一个基本实现。以下是一个上下文菜单插件的示例:
import {IPlugin,PluginStatus,IContextMenuPlugin,IContextMenuItem,FilesType,UsersType,IMessage,Actions,IComboBox,IComboBoxItem,IButton,ButtonSize,IBox,IModalDialog,ModalDisplayType,Components
} from '@onlyoffice/docspace-plugin-sdk';/*** Main plugin class implementing context menu extension*/
class ExtSearchPlugin implements IPlugin, IContextMenuPlugin {status: PluginStatus = PluginStatus.active;origin = "";proxy = "";prefix = "";contextMenuItems: Map<string, IContextMenuItem> = new Map();// Called when the plugin loadsonLoadCallback = async () => {};updateStatus = (status: PluginStatus) => {this.status = status;};getStatus = () => this.status;setOnLoadCallback = (callback: () => Promise<void>) => {this.onLoadCallback = callback;};addContextMenuItem = (item: IContextMenuItem): void => {this.contextMenuItems.set(item.key, item);};getContextMenuItems = (): Map<string, IContextMenuItem> => this.contextMenuItems;getContextMenuItemsKeys = (): string[] => Array.from(this.contextMenuItems.keys());updateContextMenuItem = (item: IContextMenuItem): void => {this.contextMenuItems.set(item.key, item);};// Set API parameters provided by DocSpacesetAPI = (origin: string, proxy: string, prefix: string): void => {this.origin = origin;this.proxy = proxy;this.prefix = prefix;};// Get API parametersgetAPI = () => ({origin: this.origin,proxy: this.proxy,prefix: this.prefix});
}const plugin = new ExtSearchPlugin();// Add the plugin items and components below the plugin initialization line// Register plugin globally for DocSpace to find
declare global {interface Window {Plugins: any;}
}window.Plugins = window.Plugins || {};
window.Plugins.Extsearch = plugin;export default plugin;
步骤 4:添加上下文菜单和 UI 逻辑
定义一个下拉菜单来选择文件扩展名,以及一个按钮来过滤和渲染文件:
// Store current API base URL and selected room ID
let apiBaseURL: string = plugin.getAPI().origin;
let currentRoomId: number | null = null;// ComboBox configuration with extension filter options
const extensionOptions: IComboBoxItem[] = [{ key: "auto", label: "Auto" },{ key: ".docx", label: "Document" },{ key: ".jpg", label: "JPEG" },
];// Triggered when user selects a new extension from dropdown
const onExtensionSelect = (option: IComboBoxItem): IMessage => {comboBox.selectedOption = option;return {actions: [Actions.updateProps],newProps: comboBox};
};// ComboBox component definition
const comboBox: IComboBox = {options: extensionOptions,selectedOption: { key: "auto", label: "Auto" },onSelect: onExtensionSelect,scaled: true,dropDownMaxHeight: 400,directionY: "both",scaledOptions: true,
};// Button that fetches files and filters by selected extension
const viewFilesButtonProps: IButton = {label: "View Files",primary: true,size: ButtonSize.normal,scale: true,isDisabled: false,withLoadingAfterClick: true,onClick: async (): Promise<IMessage> => {// Request file list from current roomconst response = await fetch(`${apiBaseURL}/api/2.0/files/${currentRoomId}`, {method: "GET",headers: {"Content-Type": "application/json;charset=utf-8",}});if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);const data = await response.json();const files = data.response.files;const extension = comboBox.selectedOption.key;// Filter files by selected extension (or show all if "auto" selected)const filtered = files.filter((file: any) =>extension === "auto" || file.fileExst === extension);// Create UI blocks for each fileconst fileBlocks = filtered.map((file: any) => ({component: Components.box,props: {displayProp: "flex",justifyContent: "space-between",alignItems: "center",marginProp: "0 0 24px",children: [{component: Components.text,props: {text: file.title,fontSize: "16px",fontWeight: 500,lineHeight: "20px",noSelect: true,}},{component: Components.button,props: {label: "Open File",size: ButtonSize.small,scale: false,primary: true,onClick: () => {// Open file in new tabwindow.open(file.webUrl, "_blank");}},}]}}));// Replace modal content with new listmodalBody.children = [...fileBlocks];return {actions: [Actions.showModal],modalDialogProps: modalProps};}
};// Modal layout combining dropdown and action button
const modalBody: IBox = {widthProp: "700px",heightProp: "150px",marginProp: "0 0 24px",children: [{component: Components.comboBox,props: comboBox},{component: Components.button,props: viewFilesButtonProps}]
};// Modal configuration object
const modalProps: IModalDialog = {dialogHeader: "Filter Files by Extension",dialogBody: modalBody,displayType: ModalDisplayType.modal,onClose: () => ({ actions: [Actions.closeModal] }),onLoad: async () => ({newDialogHeader: modalProps.dialogHeader,newDialogBody: modalProps.dialogBody}),autoMaxHeight: true,autoMaxWidth: true,
};// Context menu item definition for room entities
const contextMenuItem: IContextMenuItem = {key: "extsearch-context-menu",label: "Ext Search",icon: "icon.svg",onClick: (id: number) => {// Store selected room ID and show modalcurrentRoomId = id;return {actions: [Actions.showModal],modalDialogProps: modalProps};},fileType: [FilesType.room],usersTypes: [UsersType.owner, UsersType.docSpaceAdmin, UsersType.roomAdmin],
};// Register menu item inside the plugin
plugin.addContextMenuItem(contextMenuItem);
步骤 5:构建插件
在插件的根目录中,运行以下命令:
npm run build
这会将 src/index.ts 编译为 dist/plugin.js 并运行 scripts/createZip.js,以将所有内容压缩到 dist/plugin.zip 中。
步骤 6:上传至协作空间
- 以管理员身份登录。
- 导航至:设置 → 集成 → 插件。
- 点击上传,然后选择生成的 dist/plugin.zip 文件。
- 如果插件按钮尚未启用,请启用。
步骤 7:测试插件
- 前往任意房间
- 右键单击房间
- 点击 Ext Search 菜单项
- 选择文件类型,然后点击 View Files
- 系统将出现一个已筛选文件列表,每个文件都带有 Open File 按钮
完整的代码示例如下:
import {IPlugin,PluginStatus,IContextMenuPlugin,IContextMenuItem,FilesType,UsersType,IMessage,Actions,IComboBox,IComboBoxItem,IButton,ButtonSize,IBox,IModalDialog,ModalDisplayType,Components
} from '@onlyoffice/docspace-plugin-sdk';/*** Main plugin class implementing context menu extension*/
class ExtSearchPlugin implements IPlugin, IContextMenuPlugin {status: PluginStatus = PluginStatus.active;origin = "";proxy = "";prefix = "";contextMenuItems: Map<string, IContextMenuItem> = new Map();// Called when the plugin loadsonLoadCallback = async () => {};updateStatus = (status: PluginStatus) => {this.status = status;};getStatus = () => this.status;setOnLoadCallback = (callback: () => Promise<void>) => {this.onLoadCallback = callback;};addContextMenuItem = (item: IContextMenuItem): void => {this.contextMenuItems.set(item.key, item);};getContextMenuItems = (): Map<string, IContextMenuItem> => this.contextMenuItems;getContextMenuItemsKeys = (): string[] => Array.from(this.contextMenuItems.keys());updateContextMenuItem = (item: IContextMenuItem): void => {this.contextMenuItems.set(item.key, item);};// Set API parameters provided by DocSpacesetAPI = (origin: string, proxy: string, prefix: string): void => {this.origin = origin;this.proxy = proxy;this.prefix = prefix;};// Get API parametersgetAPI = () => ({origin: this.origin,proxy: this.proxy,prefix: this.prefix});
}const plugin = new ExtSearchPlugin();// Store current API base URL and selected room ID
let apiBaseURL: string = plugin.getAPI().origin;
let currentRoomId: number | null = null;// ComboBox configuration with extension filter options
const extensionOptions: IComboBoxItem[] = [{ key: "auto", label: "Auto" },{ key: ".docx", label: "Document" },{ key: ".jpg", label: "JPEG" },
];// Triggered when user selects a new extension from dropdown
const onExtensionSelect = (option: IComboBoxItem): IMessage => {comboBox.selectedOption = option;return {actions: [Actions.updateProps],newProps: comboBox};
};// ComboBox component definition
const comboBox: IComboBox = {options: extensionOptions,selectedOption: { key: "auto", label: "Auto" },onSelect: onExtensionSelect,scaled: true,dropDownMaxHeight: 400,directionY: "both",scaledOptions: true,
};// Button that fetches files and filters by selected extension
const viewFilesButtonProps: IButton = {label: "View Files",primary: true,size: ButtonSize.normal,scale: true,isDisabled: false,withLoadingAfterClick: true,onClick: async (): Promise<IMessage> => {// Request file list from current roomconst response = await fetch(`${apiBaseURL}/api/2.0/files/${currentRoomId}`, {method: "GET",headers: {"Content-Type": "application/json;charset=utf-8",}});if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);const data = await response.json();const files = data.response.files;const extension = comboBox.selectedOption.key;// Filter files by selected extension (or show all if "auto" selected)const filtered = files.filter((file: any) =>extension === "auto" || file.fileExst === extension);// Create UI blocks for each fileconst fileBlocks = filtered.map((file: any) => ({component: Components.box,props: {displayProp: "flex",justifyContent: "space-between",alignItems: "center",marginProp: "0 0 24px",children: [{component: Components.text,props: {text: file.title,fontSize: "16px",fontWeight: 500,lineHeight: "20px",noSelect: true,}},{component: Components.button,props: {label: "Open File",size: ButtonSize.small,scale: false,primary: true,onClick: () => {// Open file in new tabwindow.open(file.webUrl, "_blank");}},}]}}));// Replace modal content with new listmodalBody.children = [...fileBlocks];return {actions: [Actions.showModal],modalDialogProps: modalProps};}
};// Modal layout combining dropdown and action button
const modalBody: IBox = {widthProp: "700px",heightProp: "150px",marginProp: "0 0 24px",children: [{component: Components.comboBox,props: comboBox},{component: Components.button,props: viewFilesButtonProps}]
};// Modal configuration object
const modalProps: IModalDialog = {dialogHeader: "Filter Files by Extension",dialogBody: modalBody,displayType: ModalDisplayType.modal,onClose: () => ({ actions: [Actions.closeModal] }),onLoad: async () => ({newDialogHeader: modalProps.dialogHeader,newDialogBody: modalProps.dialogBody}),autoMaxHeight: true,autoMaxWidth: true,
};// Context menu item definition for room entities
const contextMenuItem: IContextMenuItem = {key: "extsearch-context-menu",label: "Ext Search",icon: "icon.svg",onClick: (id: number) => {// Store selected room ID and show modalcurrentRoomId = id;return {actions: [Actions.showModal],modalDialogProps: modalProps};},fileType: [FilesType.room],usersTypes: [UsersType.owner, UsersType.docSpaceAdmin, UsersType.roomAdmin],
};// Register menu item inside the plugin
plugin.addContextMenuItem(contextMenuItem);// Register plugin globally for DocSpace to find
declare global {interface Window {Plugins: any;}
}window.Plugins = window.Plugins || {};
window.Plugins.Extsearch = plugin;export default plugin;
请注意!您只能在服务器协作空间版本中上传自己的插件。
如果出现任何错误,请修复插件的源代码,然后重复构建和测试的过程。
现在您的插件已经过测试并正常运行,您可以将其添加到协作空间服务器版本并开始使用。
小结
协作空间插件为文档管理和团队协作提供了高效且便捷的解决方案。通过与用户常用平台的集成,它能有效消除协作障碍,提升各类工作流程的效率。
如果您在使用协作空间插件时有任何疑问,欢迎前往 ONLYOFFICE 论坛向我们的开发团队提问。您也可以通过 GitHub 提交问题,反馈功能需求或报告错误。