Skip to main content

zerolaunch_plugin_api/config/
action.rs

1use serde::{Deserialize, Serialize};
2/// 配置动作声明,描述组件对外提供的可执行动作。
3///
4/// 仅由配置管理边界和字段动作绑定引用;具体执行仍由 Configurable 实现负责。
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct ConfigActionDef {
7    /// 动作唯一标识符,如 "detect_browsers"。
8    #[serde(rename = "action", default)]
9    pub action: String,
10    /// 动作显示名称,用于设置界面按钮文本。
11    #[serde(rename = "label", default)]
12    pub label: String,
13    /// 动作描述,用于解释动作效果。
14    #[serde(rename = "description", default)]
15    pub description: String,
16}
17
18/// 字段级动作绑定,区分数据注入和用户触发的副作用动作。
19///
20/// 仅作为 Settings schema 的 UI metadata 使用,不应被 core 配置逻辑直接依赖。
21#[derive(Debug, Clone, Serialize, Deserialize)]
22#[serde(tag = "kind", content = "binding")]
23pub enum FieldAction {
24    /// 查询或检测数据,并将返回值注入字段或兄弟字段。
25    #[serde(rename = "data")]
26    Data(DataActionBinding),
27    /// 用户显式触发的轻量副作用动作,例如写入图标缓存。
28    #[serde(rename = "effect")]
29    Effect(EffectActionBinding),
30}
31
32/// 数据注入动作绑定,描述如何从 action 返回值填充设置字段。
33///
34/// 仅由 schema builder、IPC schema 和前端字段动作按钮使用。
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct DataActionBinding {
37    /// 动作标识符,对应 `ConfigActionDef.action`。
38    #[serde(rename = "action", default)]
39    pub action: String,
40    /// 动作所属组件 ID;None 表示当前组件。
41    #[serde(rename = "component", default)]
42    pub component: Option<String>,
43    /// action 返回结果中用作显示标签的字段名。
44    #[serde(rename = "labelField", default)]
45    pub label_field: String,
46    /// labelField 列的表头显示文本;labelField 在条目 schema 中无对应字段时使用。
47    #[serde(rename = "labelFieldLabel", default)]
48    pub label_field_label: String,
49    /// action 返回结果中用作字段值的字段名。
50    #[serde(rename = "valueField", default)]
51    pub value_field: String,
52    /// 字段级 data action:返回数组结果与当前字段值合并时的去重键;
53    /// 按该键匹配已存在条目,None 时整体替换当前字段值。
54    #[serde(rename = "mergeKey", default)]
55    pub merge_key: Option<String>,
56    /// 返回结果字段到设置字段的映射,格式为 source → target。
57    #[serde(rename = "fieldMapping", default)]
58    pub field_mapping: Vec<(String, String)>,
59}
60
61/// 用户触发的副作用动作绑定,描述动作参数来源和临时字段语义。
62///
63/// 仅由设置字段渲染层调用;动作必须通过 `config_execute_action` 执行,
64/// 不参与 staged/immediate 配置提交,也不修改组件 settings。
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct EffectActionBinding {
67    /// 动作标识符,对应 `ConfigActionDef.action`。
68    #[serde(rename = "action", default)]
69    pub action: String,
70    /// 动作所属组件 ID;None 表示当前组件。
71    #[serde(rename = "component", default)]
72    pub component: Option<String>,
73    /// 表单字段到 action 参数的映射,格式为 source → target;为空时传递当前表单值。
74    #[serde(rename = "fieldMapping", default)]
75    pub field_mapping: Vec<(String, String)>,
76    /// 是否只用于动作参数而不应写入持久化 settings。
77    #[serde(rename = "transient", default)]
78    pub transient: bool,
79}
80
81/// MasterDetail 详情面板的联动动作定义。
82///
83/// 仅由 MasterDetail schema 和详情预览组件使用;动作结果用于生成预览数据。
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct DetailActionDef {
86    /// 选中左侧列表项时调用的动作名。
87    #[serde(rename = "action", default)]
88    pub action: String,
89    /// 从选中项提取参数的字段名。
90    #[serde(rename = "paramField", default)]
91    pub param_field: String,
92    /// 传给 action 的参数名。
93    #[serde(rename = "paramKey", default)]
94    pub param_key: String,
95    /// 预览数据项的唯一标识字段。
96    #[serde(rename = "previewItemKey", default)]
97    pub preview_item_key: String,
98    /// 预览数据项的显示标题字段。
99    #[serde(rename = "previewItemLabel", default)]
100    pub preview_item_label: String,
101    /// 详情编辑结果写入的兄弟设置字段。
102    #[serde(rename = "targetField", default)]
103    pub target_field: String,
104    /// 用于匹配已有覆盖项的字段名。
105    #[serde(rename = "targetMatchKey", default)]
106    pub target_match_key: String,
107}
108
109#[cfg(test)]
110mod tests {
111    use super::{DataActionBinding, EffectActionBinding, FieldAction};
112
113    /// 验证字段 action 使用与 TypeScript contract 一致的 kind/binding 形状。
114    #[test]
115    fn field_action_uses_discriminated_shape() {
116        let data = serde_json::to_value(FieldAction::Data(DataActionBinding {
117            action: "search_candidates".into(),
118            component: Some("candidate-registry".into()),
119            label_field: "name".into(),
120            label_field_label: "名称".into(),
121            value_field: "target".into(),
122            merge_key: None,
123            field_mapping: vec![("iconRequestJson".into(), "icon_request_json".into())],
124        }))
125        .expect("Data action must serialize");
126        assert_eq!(data["kind"], "data");
127        assert_eq!(data["binding"]["labelField"], "name");
128        assert_eq!(data["binding"]["labelFieldLabel"], "名称");
129        assert_eq!(data["binding"]["valueField"], "target");
130        assert!(data["binding"]["mergeKey"].is_null());
131        assert_eq!(data["binding"]["fieldMapping"][0][0], "iconRequestJson");
132
133        let effect = serde_json::to_value(FieldAction::Effect(EffectActionBinding {
134            action: "apply_override".into(),
135            component: None,
136            field_mapping: vec![("custom_icon_path".into(), "custom_icon_path".into())],
137            transient: true,
138        }))
139        .expect("Effect action must serialize");
140        assert_eq!(effect["kind"], "effect");
141        assert_eq!(effect["binding"]["transient"], true);
142        assert_eq!(effect["binding"]["fieldMapping"][0][1], "custom_icon_path");
143    }
144}