feat(plugin): 扩展 manifest schema 支持 searchable/filterable/visible_when 和 tagged enum 页面类型
- PluginField 新增 searchable/filterable/sortable/visible_when 字段 - PluginPage 替换为 tagged enum PluginPageType(crud/tree/detail/tabs) - 新增 PluginSection enum(fields/crud 区段) - 新增 validate_pages 递归验证页面配置 - 更新现有测试适配新 TOML 格式 - 新增 3 个测试覆盖新页面类型解析和验证
This commit is contained in:
@@ -58,6 +58,14 @@ pub struct PluginField {
|
|||||||
pub display_name: Option<String>,
|
pub display_name: Option<String>,
|
||||||
pub ui_widget: Option<String>,
|
pub ui_widget: Option<String>,
|
||||||
pub options: Option<Vec<serde_json::Value>>,
|
pub options: Option<Vec<serde_json::Value>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub searchable: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub filterable: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub sortable: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub visible_when: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 字段类型
|
/// 字段类型
|
||||||
@@ -93,19 +101,67 @@ pub struct PluginEvents {
|
|||||||
/// UI 页面配置
|
/// UI 页面配置
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct PluginUi {
|
pub struct PluginUi {
|
||||||
pub pages: Vec<PluginPage>,
|
pub pages: Vec<PluginPageType>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 插件页面定义
|
/// 插件页面类型(tagged enum,TOML 中通过 type 字段区分)
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
pub struct PluginPage {
|
#[serde(tag = "type")]
|
||||||
pub route: String,
|
pub enum PluginPageType {
|
||||||
pub entity: String,
|
#[serde(rename = "crud")]
|
||||||
pub display_name: String,
|
Crud {
|
||||||
#[serde(default)]
|
entity: String,
|
||||||
pub icon: String,
|
label: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub menu_group: Option<String>,
|
icon: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
enable_search: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
enable_views: Option<Vec<String>>,
|
||||||
|
},
|
||||||
|
#[serde(rename = "tree")]
|
||||||
|
Tree {
|
||||||
|
entity: String,
|
||||||
|
label: String,
|
||||||
|
#[serde(default)]
|
||||||
|
icon: Option<String>,
|
||||||
|
id_field: String,
|
||||||
|
parent_field: String,
|
||||||
|
label_field: String,
|
||||||
|
},
|
||||||
|
#[serde(rename = "detail")]
|
||||||
|
Detail {
|
||||||
|
entity: String,
|
||||||
|
label: String,
|
||||||
|
sections: Vec<PluginSection>,
|
||||||
|
},
|
||||||
|
#[serde(rename = "tabs")]
|
||||||
|
Tabs {
|
||||||
|
label: String,
|
||||||
|
#[serde(default)]
|
||||||
|
icon: Option<String>,
|
||||||
|
tabs: Vec<PluginPageType>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 插件页面区段(用于 detail 页面类型)
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
pub enum PluginSection {
|
||||||
|
#[serde(rename = "fields")]
|
||||||
|
Fields {
|
||||||
|
label: String,
|
||||||
|
fields: Vec<String>,
|
||||||
|
},
|
||||||
|
#[serde(rename = "crud")]
|
||||||
|
Crud {
|
||||||
|
label: String,
|
||||||
|
entity: String,
|
||||||
|
#[serde(default)]
|
||||||
|
filter_field: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
enable_views: Option<Vec<String>>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 权限定义
|
/// 权限定义
|
||||||
@@ -154,9 +210,64 @@ pub fn parse_manifest(toml_str: &str) -> PluginResult<PluginManifest> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 验证页面类型配置
|
||||||
|
if let Some(ui) = &manifest.ui {
|
||||||
|
validate_pages(&ui.pages)?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(manifest)
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -208,11 +319,10 @@ subscribe = ["workflow.task.completed", "order.*"]
|
|||||||
|
|
||||||
[ui]
|
[ui]
|
||||||
[[ui.pages]]
|
[[ui.pages]]
|
||||||
route = "/products"
|
type = "crud"
|
||||||
entity = "product"
|
entity = "product"
|
||||||
display_name = "商品管理"
|
label = "商品管理"
|
||||||
icon = "ShoppingOutlined"
|
icon = "ShoppingOutlined"
|
||||||
menu_group = "进销存"
|
|
||||||
|
|
||||||
[[permissions]]
|
[[permissions]]
|
||||||
code = "product.list"
|
code = "product.list"
|
||||||
@@ -229,6 +339,14 @@ description = "查看商品列表"
|
|||||||
assert_eq!(events.subscribe.len(), 2);
|
assert_eq!(events.subscribe.len(), 2);
|
||||||
let ui = manifest.ui.unwrap();
|
let ui = manifest.ui.unwrap();
|
||||||
assert_eq!(ui.pages.len(), 1);
|
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]
|
#[test]
|
||||||
@@ -255,6 +373,119 @@ version = "0.1.0"
|
|||||||
[[schema.entities]]
|
[[schema.entities]]
|
||||||
name = "my-table"
|
name = "my-table"
|
||||||
display_name = "表格"
|
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);
|
let result = parse_manifest(toml);
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
|
|||||||
Reference in New Issue
Block a user