From 472bf244d8db2d50a7a6a5018e40076835bf0575 Mon Sep 17 00:00:00 2001 From: iven Date: Thu, 16 Apr 2026 12:28:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(plugin):=20=E6=89=A9=E5=B1=95=20manifest?= =?UTF-8?q?=20schema=20=E6=94=AF=E6=8C=81=20searchable/filterable/visible?= =?UTF-8?q?=5Fwhen=20=E5=92=8C=20tagged=20enum=20=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PluginField 新增 searchable/filterable/sortable/visible_when 字段 - PluginPage 替换为 tagged enum PluginPageType(crud/tree/detail/tabs) - 新增 PluginSection enum(fields/crud 区段) - 新增 validate_pages 递归验证页面配置 - 更新现有测试适配新 TOML 格式 - 新增 3 个测试覆盖新页面类型解析和验证 --- crates/erp-plugin/src/manifest.rs | 259 ++++++++++++++++++++++++++++-- 1 file changed, 245 insertions(+), 14 deletions(-) diff --git a/crates/erp-plugin/src/manifest.rs b/crates/erp-plugin/src/manifest.rs index ed6a4e4..fca72d4 100644 --- a/crates/erp-plugin/src/manifest.rs +++ b/crates/erp-plugin/src/manifest.rs @@ -58,6 +58,14 @@ pub struct PluginField { pub display_name: Option, pub ui_widget: Option, pub options: Option>, + #[serde(default)] + pub searchable: Option, + #[serde(default)] + pub filterable: Option, + #[serde(default)] + pub sortable: Option, + #[serde(default)] + pub visible_when: Option, } /// 字段类型 @@ -93,19 +101,67 @@ pub struct PluginEvents { /// UI 页面配置 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PluginUi { - pub pages: Vec, + pub pages: Vec, } -/// 插件页面定义 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PluginPage { - pub route: String, - pub entity: String, - pub display_name: String, - #[serde(default)] - pub icon: String, - #[serde(default)] - pub menu_group: Option, +/// 插件页面类型(tagged enum,TOML 中通过 type 字段区分) +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(tag = "type")] +pub enum PluginPageType { + #[serde(rename = "crud")] + Crud { + entity: String, + label: String, + #[serde(default)] + icon: Option, + #[serde(default)] + enable_search: Option, + #[serde(default)] + enable_views: Option>, + }, + #[serde(rename = "tree")] + Tree { + entity: String, + label: String, + #[serde(default)] + icon: Option, + id_field: String, + parent_field: String, + label_field: String, + }, + #[serde(rename = "detail")] + Detail { + entity: String, + label: String, + sections: Vec, + }, + #[serde(rename = "tabs")] + Tabs { + label: String, + #[serde(default)] + icon: Option, + tabs: Vec, + }, +} + +/// 插件页面区段(用于 detail 页面类型) +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(tag = "type")] +pub enum PluginSection { + #[serde(rename = "fields")] + Fields { + label: String, + fields: Vec, + }, + #[serde(rename = "crud")] + Crud { + label: String, + entity: String, + #[serde(default)] + filter_field: Option, + #[serde(default)] + enable_views: Option>, + }, } /// 权限定义 @@ -154,9 +210,64 @@ pub fn parse_manifest(toml_str: &str) -> PluginResult { } } + // 验证页面类型配置 + if let Some(ui) = &manifest.ui { + validate_pages(&ui.pages)?; + } + Ok(manifest) } +/// 递归验证页面配置 +fn validate_pages(pages: &[PluginPageType]) -> PluginResult<()> { + for page in pages { + match page { + PluginPageType::Crud { entity, .. } => { + if entity.is_empty() { + return Err(PluginError::InvalidManifest( + "crud page 的 entity 不能为空".into(), + )); + } + } + PluginPageType::Tree { + entity: _, + label: _, + icon: _, + id_field, + parent_field, + label_field, + } => { + if id_field.is_empty() || parent_field.is_empty() || label_field.is_empty() { + return Err(PluginError::InvalidManifest( + "tree page 的 id_field/parent_field/label_field 不能为空".into(), + )); + } + } + PluginPageType::Detail { entity, sections, .. } => { + if entity.is_empty() { + return Err(PluginError::InvalidManifest( + "detail page 的 entity 不能为空".into(), + )); + } + if sections.is_empty() { + return Err(PluginError::InvalidManifest( + "detail page 的 sections 不能为空".into(), + )); + } + } + PluginPageType::Tabs { tabs, .. } => { + if tabs.is_empty() { + return Err(PluginError::InvalidManifest( + "tabs page 的 tabs 不能为空".into(), + )); + } + validate_pages(tabs)?; + } + } + } + Ok(()) +} + #[cfg(test)] mod tests { use super::*; @@ -208,11 +319,10 @@ subscribe = ["workflow.task.completed", "order.*"] [ui] [[ui.pages]] -route = "/products" +type = "crud" entity = "product" -display_name = "商品管理" +label = "商品管理" icon = "ShoppingOutlined" -menu_group = "进销存" [[permissions]] code = "product.list" @@ -229,6 +339,14 @@ description = "查看商品列表" assert_eq!(events.subscribe.len(), 2); let ui = manifest.ui.unwrap(); assert_eq!(ui.pages.len(), 1); + // 验证新格式解析正确 + match &ui.pages[0] { + PluginPageType::Crud { entity, label, .. } => { + assert_eq!(entity, "product"); + assert_eq!(label, "商品管理"); + } + _ => panic!("Expected Crud page type"), + } } #[test] @@ -255,6 +373,119 @@ version = "0.1.0" [[schema.entities]] name = "my-table" display_name = "表格" +"#; + let result = parse_manifest(toml); + assert!(result.is_err()); + } + + #[test] + fn parse_manifest_with_new_fields_and_page_types() { + let toml = r#" +[metadata] +id = "test-plugin" +name = "Test" +version = "0.1.0" +description = "Test" +author = "Test" +min_platform_version = "0.1.0" + +[[schema.entities]] +name = "customer" +display_name = "客户" + +[[schema.entities.fields]] +name = "code" +field_type = "string" +required = true +display_name = "编码" +unique = true +searchable = true +filterable = true +visible_when = "customer_type == 'enterprise'" + +[[ui.pages]] +type = "tabs" +label = "客户管理" +icon = "team" + +[[ui.pages.tabs]] +label = "客户列表" +type = "crud" +entity = "customer" +enable_search = true +enable_views = ["table", "timeline"] + +[[ui.pages]] +type = "detail" +entity = "customer" +label = "客户详情" + +[[ui.pages.sections]] +type = "fields" +label = "基本信息" +fields = ["code", "name"] +"#; + let manifest = parse_manifest(toml).expect("should parse"); + let field = &manifest.schema.as_ref().unwrap().entities[0].fields[0]; + assert_eq!(field.searchable, Some(true)); + assert_eq!(field.filterable, Some(true)); + assert_eq!( + field.visible_when.as_deref(), + Some("customer_type == 'enterprise'") + ); + // 验证页面类型解析 + let ui = manifest.ui.as_ref().unwrap(); + assert_eq!(ui.pages.len(), 2); + // tabs 页面 + match &ui.pages[0] { + PluginPageType::Tabs { label, tabs, .. } => { + assert_eq!(label, "客户管理"); + assert_eq!(tabs.len(), 1); + } + _ => panic!("Expected Tabs page type"), + } + // detail 页面 + match &ui.pages[1] { + PluginPageType::Detail { + entity, sections, .. + } => { + assert_eq!(entity, "customer"); + assert_eq!(sections.len(), 1); + } + _ => panic!("Expected Detail page type"), + } + } + + #[test] + fn reject_empty_entity_in_crud_page() { + let toml = r#" +[metadata] +id = "test" +name = "Test" +version = "0.1.0" + +[ui] +[[ui.pages]] +type = "crud" +entity = "" +label = "测试" +"#; + let result = parse_manifest(toml); + assert!(result.is_err()); + } + + #[test] + fn reject_empty_tabs_in_tabs_page() { + let toml = r#" +[metadata] +id = "test" +name = "Test" +version = "0.1.0" + +[ui] +[[ui.pages]] +type = "tabs" +label = "空标签页" "#; let result = parse_manifest(toml); assert!(result.is_err());