当前位置: 首页 > news >正文

【技术教程】如何为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:上传至协作空间

  1. 以管理员身份登录。
  2. 导航至:设置 → 集成 → 插件。
  3. 点击上传,然后选择生成的 dist/plugin.zip 文件。
  4. 如果插件按钮尚未启用,请启用。

步骤 7:测试插件

  1. 前往任意房间
  2. 右键单击​​房间
  3. 点击 Ext Search 菜单项
  4. 选择文件类型,然后点击 View Files 
  5. 系统将出现一个已筛选文件列表,每个文件都带有 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 提交问题,反馈功能需求或报告错误。

http://www.dtcms.com/a/352832.html

相关文章:

  • string类的学习及模拟
  • vue拖动排序,vue使用 HTML5 的draggable拖放 API实现内容拖并排序,并更新数组数据
  • 【无标题】淘宝直播间详情数据
  • 云原生安全架构设计与零信任实践
  • 三格电子——高频一体式工业级RFID读写器的应用
  • 核心内涵解析:销采一体化 CRM 是什么?
  • 贴片式TE卡 +北京君正+Rk瑞芯微的应用
  • 亚马逊ASIN定投广告的智能化突破:从人工苦力到数据驱动的华丽转身
  • Part 1️⃣:相机几何与单视图几何-第六章:相机模型
  • Android中点击链接跳转到对应App页面的底层原理
  • Linux 云服务器日志清理自动化方法
  • 第二阶段Winfrom-8:特性和反射,加密和解密,单例模式
  • 点评项目(Redis中间件)第一部分Redis基础
  • golang 12 package 和 module
  • SegEarth-R1: Geospatial Pixel Reasoning via Large Language Model
  • week5-[字符数组]长度和
  • GraphRAG数据可视化
  • Java中JUnit知识点
  • Qt表格组件封装与远程数据库连接:从数据展示到交互体验
  • 阿里云——应用交付与负载均衡
  • 用户体验设计 | 从UX到AX:人工智能如何重构交互范式?
  • 阿里云轻量应用服务器与ECS对比
  • 4步用代码拆解数学建模中的TOPSIS评价决策! ! !
  • 树的常见算法及Java实现
  • LeetCode算法日记 - Day 23: 外观数列、数青蛙
  • 欧洲数字化养殖平台 Herdwatch 借力 Iceberg + StarRocks 提升分析能力
  • 【Matplotlib学习】驾驭画布:Matplotlib 布局方式从入门到精通完全指南
  • 【RabbitWQ】基于 Java 实现轻量级消息队列(二)
  • 医疗AI时代的生物医学Go编程:高性能计算与精准医疗的案例分析(一)
  • 【重学 MySQL】九十、Linux下MySQL的安装与卸载指南