@josephyan/qingflow-app-builder-mcp 1.1.30 → 1.1.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +6 -4
  2. package/docs/local-agent-install.md +57 -6
  3. package/entry_point.py +1 -1
  4. package/npm/bin/qingflow-app-builder-mcp.mjs +1 -1
  5. package/npm/bin/qingflow-skills.mjs +5 -0
  6. package/npm/lib/runtime.mjs +20 -59
  7. package/npm/scripts/postinstall.mjs +1 -10
  8. package/package.json +3 -2
  9. package/pyproject.toml +1 -1
  10. package/skills/qingflow-app-builder/SKILL.md +111 -55
  11. package/skills/qingflow-app-builder/references/complete-system-development-guide.md +169 -0
  12. package/skills/qingflow-app-builder/references/create-app.md +31 -24
  13. package/skills/qingflow-app-builder/references/environments.md +1 -1
  14. package/skills/qingflow-app-builder/references/gotchas.md +40 -14
  15. package/skills/qingflow-app-builder/references/match-rules.md +196 -85
  16. package/skills/qingflow-app-builder/references/public-surface-sync.md +2 -2
  17. package/skills/qingflow-app-builder/references/single-app-development-guide.md +84 -0
  18. package/skills/qingflow-app-builder/references/solution-playbooks.md +10 -0
  19. package/skills/qingflow-app-builder/references/tool-selection.md +40 -25
  20. package/skills/qingflow-app-builder/references/update-flow.md +27 -52
  21. package/skills/qingflow-app-builder/references/update-schema.md +14 -0
  22. package/skills/qingflow-app-builder/references/update-views.md +101 -32
  23. package/skills/qingflow-app-builder/scripts/validate_system_build_summary.py +124 -0
  24. package/skills/qingflow-app-builder-code-integrations/SKILL.md +5 -3
  25. package/skills/qingflow-app-builder-code-integrations/references/code-block.md +1 -1
  26. package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +1 -1
  27. package/skills/qingflow-mcp-setup/SKILL.md +13 -9
  28. package/skills/qingflow-mcp-setup/references/claude-desktop.md +1 -1
  29. package/skills/qingflow-mcp-setup/references/environments.md +2 -1
  30. package/skills/qingflow-mcp-setup/references/generic-stdio.md +6 -5
  31. package/skills/qingflow-workflow-builder/SKILL.md +9 -7
  32. package/skills/qingflow-workflow-builder/references/01-overview.md +4 -2
  33. package/skills/qingflow-workflow-builder/references/03-flow-patterns.md +2 -0
  34. package/skills/qingflow-workflow-builder/references/06-stage3-build-spec.md +59 -7
  35. package/skills/qingflow-workflow-builder/references/07-stage4-validate-spec.md +14 -5
  36. package/skills/qingflow-workflow-builder/references/08-stage5-apply-verify.md +19 -3
  37. package/skills/qingflow-workflow-builder/references/11-troubleshooting.md +2 -0
  38. package/src/qingflow_mcp/__main__.py +6 -2
  39. package/src/qingflow_mcp/builder_facade/models.py +473 -138
  40. package/src/qingflow_mcp/builder_facade/service.py +6356 -1866
  41. package/src/qingflow_mcp/cli/commands/builder.py +358 -355
  42. package/src/qingflow_mcp/cli/commands/chart.py +1 -1
  43. package/src/qingflow_mcp/cli/commands/common.py +12 -3
  44. package/src/qingflow_mcp/cli/commands/exports.py +2 -2
  45. package/src/qingflow_mcp/cli/commands/imports.py +3 -3
  46. package/src/qingflow_mcp/cli/commands/portal.py +2 -2
  47. package/src/qingflow_mcp/cli/commands/record.py +101 -27
  48. package/src/qingflow_mcp/cli/commands/task.py +28 -47
  49. package/src/qingflow_mcp/cli/commands/view.py +1 -1
  50. package/src/qingflow_mcp/cli/context.py +0 -3
  51. package/src/qingflow_mcp/cli/formatters.py +784 -16
  52. package/src/qingflow_mcp/cli/main.py +120 -41
  53. package/src/qingflow_mcp/errors.py +43 -2
  54. package/src/qingflow_mcp/public_surface.py +26 -17
  55. package/src/qingflow_mcp/response_trim.py +81 -17
  56. package/src/qingflow_mcp/server.py +14 -12
  57. package/src/qingflow_mcp/server_app_builder.py +83 -39
  58. package/src/qingflow_mcp/server_app_user.py +22 -16
  59. package/src/qingflow_mcp/session_store.py +11 -7
  60. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
  61. package/src/qingflow_mcp/solution/executor.py +114 -15
  62. package/src/qingflow_mcp/tools/ai_builder_tools.py +3571 -1473
  63. package/src/qingflow_mcp/tools/app_tools.py +184 -43
  64. package/src/qingflow_mcp/tools/approval_tools.py +197 -35
  65. package/src/qingflow_mcp/tools/auth_tools.py +92 -16
  66. package/src/qingflow_mcp/tools/code_block_tools.py +298 -40
  67. package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
  68. package/src/qingflow_mcp/tools/directory_tools.py +236 -72
  69. package/src/qingflow_mcp/tools/export_tools.py +244 -34
  70. package/src/qingflow_mcp/tools/feedback_tools.py +9 -0
  71. package/src/qingflow_mcp/tools/file_tools.py +9 -3
  72. package/src/qingflow_mcp/tools/import_tools.py +336 -49
  73. package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
  74. package/src/qingflow_mcp/tools/package_tools.py +118 -6
  75. package/src/qingflow_mcp/tools/portal_tools.py +39 -3
  76. package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
  77. package/src/qingflow_mcp/tools/record_tools.py +1141 -356
  78. package/src/qingflow_mcp/tools/resource_read_tools.py +188 -39
  79. package/src/qingflow_mcp/tools/role_tools.py +80 -9
  80. package/src/qingflow_mcp/tools/solution_tools.py +57 -15
  81. package/src/qingflow_mcp/tools/task_context_tools.py +687 -159
  82. package/src/qingflow_mcp/tools/task_tools.py +113 -29
  83. package/src/qingflow_mcp/tools/view_tools.py +106 -3
  84. package/src/qingflow_mcp/tools/workflow_tools.py +17 -1
  85. package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
  86. package/src/qingflow_mcp/version.py +0 -2
  87. package/skills/qingflow-app-builder/references/build-complete-system.md +0 -234
  88. package/skills/qingflow-app-builder/references/build-single-app.md +0 -138
@@ -71,20 +71,36 @@ class PublicExternalVisibilityMode(str, Enum):
71
71
 
72
72
 
73
73
  FIELD_TYPE_ALIASES: dict[str, PublicFieldType] = {
74
- "textarea": PublicFieldType.long_text,
75
74
  "multiline": PublicFieldType.long_text,
76
- "multi_line": PublicFieldType.long_text,
77
75
  "multiline_text": PublicFieldType.long_text,
76
+ "multi_line": PublicFieldType.long_text,
77
+ "multi_line_text": PublicFieldType.long_text,
78
+ "textarea": PublicFieldType.long_text,
79
+ "longtext": PublicFieldType.long_text,
80
+ "long-text": PublicFieldType.long_text,
78
81
  "amount": PublicFieldType.amount,
79
82
  "currency": PublicFieldType.amount,
80
83
  "mobile": PublicFieldType.phone,
81
84
  "user": PublicFieldType.member,
82
85
  "users": PublicFieldType.member,
83
86
  "select": PublicFieldType.single_select,
87
+ "single_choice": PublicFieldType.single_select,
88
+ "single-choice": PublicFieldType.single_select,
89
+ "single choice": PublicFieldType.single_select,
90
+ "choice": PublicFieldType.single_select,
91
+ "dropdown": PublicFieldType.single_select,
84
92
  "radio": PublicFieldType.single_select,
85
93
  "checkbox": PublicFieldType.multi_select,
86
94
  "multi_select": PublicFieldType.multi_select,
87
95
  "multi-select": PublicFieldType.multi_select,
96
+ "multi select": PublicFieldType.multi_select,
97
+ "multiselect": PublicFieldType.multi_select,
98
+ "multi_choice": PublicFieldType.multi_select,
99
+ "multi-choice": PublicFieldType.multi_select,
100
+ "multi choice": PublicFieldType.multi_select,
101
+ "multiple_choice": PublicFieldType.multi_select,
102
+ "multiple-choice": PublicFieldType.multi_select,
103
+ "multiple choice": PublicFieldType.multi_select,
88
104
  "departments": PublicFieldType.department,
89
105
  "qlinker": PublicFieldType.q_linker,
90
106
  "q_linker": PublicFieldType.q_linker,
@@ -187,6 +203,10 @@ class LayoutPreset(str, Enum):
187
203
  single_section = "single_section"
188
204
 
189
205
 
206
+ class FlowPreset(str, Enum):
207
+ basic_approval = "basic_approval"
208
+ basic_fill_then_approve = "basic_fill_then_approve"
209
+
190
210
 
191
211
  class ViewsPreset(str, Enum):
192
212
  default_table = "default_table"
@@ -227,6 +247,116 @@ class ViewFilterOperator(str, Enum):
227
247
  not_empty = "not_empty"
228
248
 
229
249
 
250
+ _PUBLIC_CONDITION_OPERATOR_ALIASES: dict[str, str] = {
251
+ "eq": "eq",
252
+ "equal": "eq",
253
+ "equals": "eq",
254
+ "=": "eq",
255
+ "==": "eq",
256
+ "neq": "neq",
257
+ "ne": "neq",
258
+ "not_eq": "neq",
259
+ "not_equal": "neq",
260
+ "not_equals": "neq",
261
+ "noteq": "neq",
262
+ "notequal": "neq",
263
+ "notequals": "neq",
264
+ "!=": "neq",
265
+ "in": "in",
266
+ "any": "in",
267
+ "any_of": "in",
268
+ "anyof": "in",
269
+ "one_of": "in",
270
+ "oneof": "in",
271
+ "between_any": "in",
272
+ "betweenany": "in",
273
+ "equal_any": "in",
274
+ "equalany": "in",
275
+ "any_match": "in",
276
+ "anymatch": "in",
277
+ "contains": "contains",
278
+ "contain": "contains",
279
+ "include": "contains",
280
+ "includes": "contains",
281
+ "like": "contains",
282
+ "fuzzy": "contains",
283
+ "fuzzy_match": "contains",
284
+ "fuzzymatch": "contains",
285
+ "gte": "gte",
286
+ "ge": "gte",
287
+ ">=": "gte",
288
+ "greater_or_equal": "gte",
289
+ "greaterorequal": "gte",
290
+ "greater_than_or_equal": "gte",
291
+ "greaterthanorequal": "gte",
292
+ "lte": "lte",
293
+ "le": "lte",
294
+ "<=": "lte",
295
+ "less_or_equal": "lte",
296
+ "lessorequal": "lte",
297
+ "less_than_or_equal": "lte",
298
+ "lessthanorequal": "lte",
299
+ "is_empty": "is_empty",
300
+ "isempty": "is_empty",
301
+ "empty": "is_empty",
302
+ "blank": "is_empty",
303
+ "is_blank": "is_empty",
304
+ "isblank": "is_empty",
305
+ "null": "is_empty",
306
+ "is_null": "is_empty",
307
+ "isnull": "is_empty",
308
+ "not_empty": "not_empty",
309
+ "notempty": "not_empty",
310
+ "is_not_empty": "not_empty",
311
+ "isnotempty": "not_empty",
312
+ "not_blank": "not_empty",
313
+ "notblank": "not_empty",
314
+ "not_null": "not_empty",
315
+ "notnull": "not_empty",
316
+ "is_not_null": "not_empty",
317
+ "isnotnull": "not_empty",
318
+ }
319
+
320
+
321
+ def normalize_public_condition_operator(raw_operator: Any, *, extra_aliases: dict[str, str] | None = None) -> Any:
322
+ if not isinstance(raw_operator, str):
323
+ return raw_operator
324
+ text = raw_operator.strip().lower()
325
+ if not text:
326
+ return text
327
+ lookup = dict(_PUBLIC_CONDITION_OPERATOR_ALIASES)
328
+ if extra_aliases:
329
+ lookup.update(extra_aliases)
330
+ candidates = [
331
+ text,
332
+ re.sub(r"[\s-]+", "_", text),
333
+ re.sub(r"[\s_-]+", "", text),
334
+ ]
335
+ for candidate in candidates:
336
+ if candidate in lookup:
337
+ return lookup[candidate]
338
+ return text
339
+
340
+
341
+ def _normalize_public_condition_payload(value: Any) -> Any:
342
+ if not isinstance(value, dict):
343
+ return value
344
+ payload = dict(value)
345
+ if "value" in payload and "values" not in payload:
346
+ raw_value = payload.pop("value")
347
+ payload["values"] = list(raw_value) if isinstance(raw_value, list) else [raw_value]
348
+ elif "values" in payload and not isinstance(payload.get("values"), list):
349
+ payload["values"] = [payload.get("values")]
350
+ raw_operator = payload.get("operator", payload.get("op"))
351
+ if raw_operator is None:
352
+ payload["operator"] = "eq"
353
+ raw_operator = "eq"
354
+ if isinstance(raw_operator, str):
355
+ payload["operator"] = normalize_public_condition_operator(raw_operator)
356
+ payload.pop("op", None)
357
+ return payload
358
+
359
+
230
360
  class FlowAssigneePatch(StrictModel):
231
361
  role_ids: list[int] = Field(default_factory=list)
232
362
  role_names: list[str] = Field(default_factory=list)
@@ -242,45 +372,13 @@ class FlowNodePermissionsPatch(StrictModel):
242
372
 
243
373
  class FlowConditionRulePatch(StrictModel):
244
374
  field_name: str = Field(validation_alias=AliasChoices("field_name", "fieldName", "field", "name"))
245
- operator: FlowConditionOperator = Field(validation_alias=AliasChoices("operator", "op"))
375
+ operator: FlowConditionOperator = Field(default=FlowConditionOperator.eq, validation_alias=AliasChoices("operator", "op"))
246
376
  values: list[Any] = Field(default_factory=list)
247
377
 
248
378
  @model_validator(mode="before")
249
379
  @classmethod
250
380
  def normalize_aliases(cls, value: Any) -> Any:
251
- if not isinstance(value, dict):
252
- return value
253
- payload = dict(value)
254
- if "value" in payload and "values" not in payload:
255
- raw_value = payload.pop("value")
256
- payload["values"] = list(raw_value) if isinstance(raw_value, list) else [raw_value]
257
- raw_operator = payload.get("operator", payload.get("op"))
258
- if isinstance(raw_operator, str):
259
- normalized = raw_operator.strip().lower()
260
- operator_aliases = {
261
- "equals": FlowConditionOperator.eq.value,
262
- "equal": FlowConditionOperator.eq.value,
263
- "=": FlowConditionOperator.eq.value,
264
- "not_equals": FlowConditionOperator.neq.value,
265
- "not_equal": FlowConditionOperator.neq.value,
266
- "!=": FlowConditionOperator.neq.value,
267
- ">=": FlowConditionOperator.gte.value,
268
- "<=": FlowConditionOperator.lte.value,
269
- "any_of": FlowConditionOperator.in_.value,
270
- "one_of": FlowConditionOperator.in_.value,
271
- "between_any": FlowConditionOperator.in_.value,
272
- "empty": FlowConditionOperator.is_empty.value,
273
- "is blank": FlowConditionOperator.is_empty.value,
274
- "blank": FlowConditionOperator.is_empty.value,
275
- "not_empty": FlowConditionOperator.not_empty.value,
276
- "not blank": FlowConditionOperator.not_empty.value,
277
- }
278
- if normalized in operator_aliases:
279
- payload["operator"] = operator_aliases[normalized]
280
- elif "operator" not in payload:
281
- payload["operator"] = normalized
282
- payload.pop("op", None)
283
- return payload
381
+ return _normalize_public_condition_payload(value)
284
382
 
285
383
  @model_validator(mode="after")
286
384
  def validate_shape(self) -> "FlowConditionRulePatch":
@@ -294,45 +392,13 @@ class FlowConditionRulePatch(StrictModel):
294
392
 
295
393
  class ViewFilterRulePatch(StrictModel):
296
394
  field_name: str = Field(validation_alias=AliasChoices("field_name", "fieldName", "field", "name"))
297
- operator: ViewFilterOperator = Field(validation_alias=AliasChoices("operator", "op"))
395
+ operator: ViewFilterOperator = Field(default=ViewFilterOperator.eq, validation_alias=AliasChoices("operator", "op"))
298
396
  values: list[Any] = Field(default_factory=list)
299
397
 
300
398
  @model_validator(mode="before")
301
399
  @classmethod
302
400
  def normalize_aliases(cls, value: Any) -> Any:
303
- if not isinstance(value, dict):
304
- return value
305
- payload = dict(value)
306
- if "value" in payload and "values" not in payload:
307
- raw_value = payload.pop("value")
308
- payload["values"] = list(raw_value) if isinstance(raw_value, list) else [raw_value]
309
- raw_operator = payload.get("operator", payload.get("op"))
310
- if isinstance(raw_operator, str):
311
- normalized = raw_operator.strip().lower()
312
- operator_aliases = {
313
- "equals": ViewFilterOperator.eq.value,
314
- "equal": ViewFilterOperator.eq.value,
315
- "=": ViewFilterOperator.eq.value,
316
- "not_equals": ViewFilterOperator.neq.value,
317
- "not_equal": ViewFilterOperator.neq.value,
318
- "!=": ViewFilterOperator.neq.value,
319
- ">=": ViewFilterOperator.gte.value,
320
- "<=": ViewFilterOperator.lte.value,
321
- "any_of": ViewFilterOperator.in_.value,
322
- "one_of": ViewFilterOperator.in_.value,
323
- "between_any": ViewFilterOperator.in_.value,
324
- "empty": ViewFilterOperator.is_empty.value,
325
- "is blank": ViewFilterOperator.is_empty.value,
326
- "blank": ViewFilterOperator.is_empty.value,
327
- "not_empty": ViewFilterOperator.not_empty.value,
328
- "not blank": ViewFilterOperator.not_empty.value,
329
- }
330
- if normalized in operator_aliases:
331
- payload["operator"] = operator_aliases[normalized]
332
- elif "operator" not in payload:
333
- payload["operator"] = normalized
334
- payload.pop("op", None)
335
- return payload
401
+ return _normalize_public_condition_payload(value)
336
402
 
337
403
  @model_validator(mode="after")
338
404
  def validate_shape(self) -> "ViewFilterRulePatch":
@@ -1085,6 +1151,91 @@ class ViewAssociatedResourcesPatch(StrictModel):
1085
1151
  )
1086
1152
 
1087
1153
 
1154
+ class ViewActionButtonPatch(StrictModel):
1155
+ button_id: int | None = Field(default=None, validation_alias=AliasChoices("button_id", "buttonId", "id"))
1156
+ client_key: str | None = Field(default=None, validation_alias=AliasChoices("client_key", "clientKey"))
1157
+ text: str = Field(validation_alias=AliasChoices("text", "button_text", "buttonText", "name"))
1158
+ action: PublicButtonTriggerAction = Field(validation_alias=AliasChoices("action", "trigger_action", "triggerAction"))
1159
+ url: str | None = Field(default=None, validation_alias=AliasChoices("url", "link_url", "linkUrl", "trigger_link_url", "triggerLinkUrl"))
1160
+ target_app_key: str | None = Field(default=None, validation_alias=AliasChoices("target_app_key", "targetAppKey", "related_app_key", "relatedAppKey"))
1161
+ target_app_name: str | None = Field(default=None, validation_alias=AliasChoices("target_app_name", "targetAppName", "related_app_name", "relatedAppName"))
1162
+ field_mappings: list[dict[str, Any]] = Field(default_factory=list, validation_alias=AliasChoices("field_mappings", "fieldMappings", "mappings"))
1163
+ default_values: dict[str, Any] = Field(default_factory=dict, validation_alias=AliasChoices("default_values", "defaultValues", "defaults"))
1164
+ placement: PublicButtonPlacement = Field(default=PublicButtonPlacement.detail, validation_alias=AliasChoices("placement", "position"))
1165
+ primary: bool = Field(default=False, validation_alias=AliasChoices("primary", "being_main", "beingMain"))
1166
+ visible_when: list[list[ViewFilterRulePatch]] = Field(
1167
+ default_factory=list,
1168
+ validation_alias=AliasChoices("visible_when", "visibleWhen", "button_limit", "buttonLimit"),
1169
+ )
1170
+ button_formula: str | None = Field(default=None, validation_alias=AliasChoices("button_formula", "buttonFormula"))
1171
+ button_formula_type: int = Field(default=1, validation_alias=AliasChoices("button_formula_type", "buttonFormulaType"))
1172
+ print_tpls: list[Any] = Field(default_factory=list, validation_alias=AliasChoices("print_tpls", "printTpls"))
1173
+ style_preset: str | None = Field(default=None, validation_alias=AliasChoices("style_preset", "stylePreset"))
1174
+ background_color: str | None = Field(default=None, validation_alias=AliasChoices("background_color", "backgroundColor"))
1175
+ text_color: str | None = Field(default=None, validation_alias=AliasChoices("text_color", "textColor"))
1176
+ button_icon: str | None = Field(default=None, validation_alias=AliasChoices("button_icon", "buttonIcon"))
1177
+ external_qrobot_config: dict[str, Any] | None = Field(
1178
+ default=None,
1179
+ validation_alias=AliasChoices(
1180
+ "external_qrobot_config",
1181
+ "externalQrobotConfig",
1182
+ "externalQRobotConfig",
1183
+ "custom_button_external_qrobot_relation_vo",
1184
+ "customButtonExternalQRobotRelationVO",
1185
+ ),
1186
+ )
1187
+ trigger_wings_config: dict[str, Any] | None = Field(default=None, validation_alias=AliasChoices("trigger_wings_config", "triggerWingsConfig"))
1188
+
1189
+ @model_validator(mode="before")
1190
+ @classmethod
1191
+ def normalize_aliases(cls, value: Any) -> Any:
1192
+ if not isinstance(value, dict):
1193
+ return value
1194
+ payload = dict(value)
1195
+ raw_action = payload.get("action", payload.get("trigger_action", payload.get("triggerAction")))
1196
+ if isinstance(raw_action, str):
1197
+ normalized = raw_action.strip()
1198
+ aliases = {
1199
+ "add_data": PublicButtonTriggerAction.add_data.value,
1200
+ "add-data": PublicButtonTriggerAction.add_data.value,
1201
+ "adddata": PublicButtonTriggerAction.add_data.value,
1202
+ "addData": PublicButtonTriggerAction.add_data.value,
1203
+ "link": PublicButtonTriggerAction.link.value,
1204
+ "url": PublicButtonTriggerAction.link.value,
1205
+ "qrobot": PublicButtonTriggerAction.qrobot.value,
1206
+ "qRobot": PublicButtonTriggerAction.qrobot.value,
1207
+ "wings": PublicButtonTriggerAction.wings.value,
1208
+ }
1209
+ payload["action"] = aliases.get(normalized, aliases.get(normalized.lower(), normalized))
1210
+ raw_placement = payload.get("placement", payload.get("position", payload.get("config_type", payload.get("configType"))))
1211
+ if isinstance(raw_placement, str):
1212
+ normalized = raw_placement.strip().lower()
1213
+ if normalized in {"top", "header"}:
1214
+ payload["placement"] = PublicButtonPlacement.header.value
1215
+ elif normalized in {"detail", "data_detail"}:
1216
+ payload["placement"] = PublicButtonPlacement.detail.value
1217
+ elif normalized in {"list", "row", "row_action", "inside"}:
1218
+ payload["placement"] = PublicButtonPlacement.list.value
1219
+ raw_limits = payload.get("visible_when", payload.get("visibleWhen", payload.get("button_limit", payload.get("buttonLimit"))))
1220
+ if isinstance(raw_limits, list) and raw_limits and all(isinstance(item, dict) for item in raw_limits):
1221
+ payload["visible_when"] = [raw_limits]
1222
+ return payload
1223
+
1224
+ @model_validator(mode="after")
1225
+ def validate_shape(self) -> "ViewActionButtonPatch":
1226
+ if not str(self.text or "").strip():
1227
+ raise ValueError("action_buttons[].text is required")
1228
+ if self.action == PublicButtonTriggerAction.link and not str(self.url or "").strip():
1229
+ raise ValueError("link action_buttons require url")
1230
+ if self.action == PublicButtonTriggerAction.add_data and not str(self.target_app_key or "").strip():
1231
+ raise ValueError("add_data action_buttons require target_app_key")
1232
+ if self.action == PublicButtonTriggerAction.qrobot and self.external_qrobot_config is None:
1233
+ raise ValueError("qRobot action_buttons require external_qrobot_config")
1234
+ if self.action == PublicButtonTriggerAction.wings and self.trigger_wings_config is None:
1235
+ raise ValueError("wings action_buttons require trigger_wings_config")
1236
+ return self
1237
+
1238
+
1088
1239
  class ViewUpsertPatch(StrictModel):
1089
1240
  name: str
1090
1241
  view_key: str | None = Field(default=None, validation_alias=AliasChoices("view_key", "viewKey"))
@@ -1096,6 +1247,8 @@ class ViewUpsertPatch(StrictModel):
1096
1247
  end_field: str | None = Field(default=None, validation_alias=AliasChoices("end_field", "endField"))
1097
1248
  title_field: str | None = Field(default=None, validation_alias=AliasChoices("title_field", "titleField"))
1098
1249
  buttons: list["ViewButtonBindingPatch"] | None = None
1250
+ action_buttons: list[ViewActionButtonPatch] | None = Field(default=None, validation_alias=AliasChoices("action_buttons", "actionButtons"))
1251
+ action_buttons_mode: str = Field(default="merge", validation_alias=AliasChoices("action_buttons_mode", "actionButtonsMode", "button_mode", "buttonMode"))
1099
1252
  visibility: VisibilityPatch | None = None
1100
1253
  query_conditions: ViewQueryConditionsPatch | None = Field(default=None, validation_alias=AliasChoices("query_conditions", "queryConditions", "query_condition", "queryCondition"))
1101
1254
  associated_resources: ViewAssociatedResourcesPatch | None = Field(
@@ -1145,6 +1298,12 @@ class ViewUpsertPatch(StrictModel):
1145
1298
  payload["associated_resources"] = payload.pop("associatedReports")
1146
1299
  if "asosChartConfig" in payload and "associated_resources" not in payload:
1147
1300
  payload["associated_resources"] = payload.pop("asosChartConfig")
1301
+ if "actionButtons" in payload and "action_buttons" not in payload:
1302
+ payload["action_buttons"] = payload.pop("actionButtons")
1303
+ if "actionButtonsMode" in payload and "action_buttons_mode" not in payload:
1304
+ payload["action_buttons_mode"] = payload.pop("actionButtonsMode")
1305
+ if "buttonMode" in payload and "action_buttons_mode" not in payload:
1306
+ payload["action_buttons_mode"] = payload.pop("buttonMode")
1148
1307
  raw_type = payload.get("type")
1149
1308
  if isinstance(raw_type, str):
1150
1309
  normalized = raw_type.strip().lower()
@@ -1166,6 +1325,10 @@ class ViewUpsertPatch(StrictModel):
1166
1325
  raise ValueError("board view requires group_by")
1167
1326
  if self.type == PublicViewType.gantt and not (self.start_field and self.end_field):
1168
1327
  raise ValueError("gantt view requires start_field and end_field")
1328
+ normalized_mode = str(self.action_buttons_mode or "merge").strip().lower()
1329
+ if normalized_mode not in {"merge", "replace"}:
1330
+ raise ValueError("action_buttons_mode must be merge or replace")
1331
+ self.action_buttons_mode = normalized_mode
1169
1332
  return self
1170
1333
 
1171
1334
 
@@ -1191,6 +1354,76 @@ class ViewPartialPatch(StrictModel):
1191
1354
  return self
1192
1355
 
1193
1356
 
1357
+ def public_button_trigger_action_value(value: Any) -> str:
1358
+ raw = value.value if isinstance(value, PublicButtonTriggerAction) else str(value or "").strip()
1359
+ normalized = raw.lower()
1360
+ if raw == PublicButtonTriggerAction.add_data.value or normalized in {"add_data", "add-data", "adddata"}:
1361
+ return "add_data"
1362
+ if normalized in {PublicButtonTriggerAction.link.value, "url"}:
1363
+ return "link"
1364
+ if raw == PublicButtonTriggerAction.qrobot.value or normalized == "qrobot":
1365
+ return "qRobot"
1366
+ if normalized == PublicButtonTriggerAction.wings.value:
1367
+ return "wings"
1368
+ return raw
1369
+
1370
+
1371
+ def _drop_empty_public_payload_values(payload: dict[str, Any]) -> dict[str, Any]:
1372
+ compact: dict[str, Any] = {}
1373
+ for key, item in payload.items():
1374
+ if item is None or item == "" or item == [] or item == {}:
1375
+ continue
1376
+ if key == "primary" and item is False:
1377
+ continue
1378
+ compact[key] = item
1379
+ return compact
1380
+
1381
+
1382
+ def public_view_action_button_payload(button: Any, *, compact: bool = False) -> dict[str, Any]:
1383
+ if isinstance(button, ViewActionButtonPatch):
1384
+ payload = button.model_dump(mode="json")
1385
+ elif isinstance(button, dict):
1386
+ payload = dict(button)
1387
+ else:
1388
+ return {}
1389
+ raw_action = payload.get("action", payload.get("trigger_action", payload.get("triggerAction")))
1390
+ if raw_action is not None:
1391
+ payload["action"] = public_button_trigger_action_value(raw_action)
1392
+ payload.pop("trigger_action", None)
1393
+ payload.pop("triggerAction", None)
1394
+ if compact:
1395
+ return _drop_empty_public_payload_values(payload)
1396
+ return payload
1397
+
1398
+
1399
+ def public_view_upsert_payload(view: ViewUpsertPatch) -> dict[str, Any]:
1400
+ payload = view.model_dump(mode="json")
1401
+ if view.action_buttons is not None:
1402
+ payload["action_buttons"] = [
1403
+ public_view_action_button_payload(button, compact=True)
1404
+ for button in view.action_buttons
1405
+ ]
1406
+ return payload
1407
+
1408
+
1409
+ def public_view_partial_payload(patch: ViewPartialPatch) -> dict[str, Any]:
1410
+ payload = patch.model_dump(mode="json")
1411
+ patch_set = payload.get("set")
1412
+ if isinstance(patch_set, dict):
1413
+ raw_buttons = patch_set.get("action_buttons")
1414
+ if raw_buttons is None:
1415
+ raw_buttons = patch_set.get("actionButtons")
1416
+ if isinstance(raw_buttons, list):
1417
+ patch_set = dict(patch_set)
1418
+ patch_set.pop("actionButtons", None)
1419
+ patch_set["action_buttons"] = [
1420
+ public_view_action_button_payload(button, compact=True)
1421
+ for button in raw_buttons
1422
+ ]
1423
+ payload["set"] = patch_set
1424
+ return payload
1425
+
1426
+
1194
1427
  class CustomButtonJudgeValuePatch(StrictModel):
1195
1428
  id: int | str | None = None
1196
1429
  value: Any | None = None
@@ -1259,13 +1492,39 @@ class CustomButtonFieldMappingPatch(StrictModel):
1259
1492
 
1260
1493
 
1261
1494
  class FieldMatchMappingPatch(StrictModel):
1262
- target_field: Any = Field(validation_alias=AliasChoices("target_field", "targetField", "target", "field"))
1495
+ target_field: Any = Field(validation_alias=AliasChoices("target_field", "targetField", "target", "field", "field_name", "fieldName", "name"))
1263
1496
  source_field: Any | None = Field(default=None, validation_alias=AliasChoices("source_field", "sourceField", "source"))
1264
- value: Any | None = Field(default=None, validation_alias=AliasChoices("value", "static_value", "staticValue"))
1497
+ value: Any | None = Field(default=None, validation_alias=AliasChoices("value", "values", "static_value", "staticValue"))
1265
1498
  operator: str = Field(default="eq", validation_alias=AliasChoices("operator", "op", "judge_type", "judgeType"))
1266
1499
 
1500
+ @model_validator(mode="before")
1501
+ @classmethod
1502
+ def normalize_aliases(cls, value: Any) -> Any:
1503
+ if not isinstance(value, dict):
1504
+ return value
1505
+ payload = dict(value)
1506
+ if "values" in payload and "value" not in payload:
1507
+ raw_values = payload.pop("values")
1508
+ if isinstance(raw_values, list) and len(raw_values) == 1:
1509
+ payload["value"] = raw_values[0]
1510
+ else:
1511
+ payload["value"] = raw_values
1512
+ raw_operator = payload.get("operator", payload.get("op", payload.get("judge_type", payload.get("judgeType"))))
1513
+ if isinstance(raw_operator, str):
1514
+ payload["operator"] = normalize_public_condition_operator(
1515
+ raw_operator,
1516
+ extra_aliases={"include_any": "include_any", "includes_any": "include_any", "includeany": "include_any"},
1517
+ )
1518
+ for alias in ("op", "judge_type", "judgeType"):
1519
+ payload.pop(alias, None)
1520
+ return payload
1521
+
1267
1522
  @model_validator(mode="after")
1268
1523
  def validate_shape(self) -> "FieldMatchMappingPatch":
1524
+ if str(self.operator or "").strip().lower() in {"is_empty", "not_empty"}:
1525
+ self.source_field = None
1526
+ self.value = None
1527
+ return self
1269
1528
  has_source = self.source_field is not None and str(self.source_field).strip() != ""
1270
1529
  has_value = self.value is not None
1271
1530
  if has_source == has_value:
@@ -1459,9 +1718,6 @@ class CustomButtonViewButtonBindingPatch(StrictModel):
1459
1718
  raw_limits = payload.get("button_limit", payload.get("buttonLimit", payload.get("visible_when", payload.get("visibleWhen"))))
1460
1719
  if isinstance(raw_limits, list) and raw_limits and all(isinstance(item, dict) for item in raw_limits):
1461
1720
  payload["button_limit"] = [raw_limits]
1462
- for alias in ("buttonLimit", "visible_when", "visibleWhen"):
1463
- if alias in payload and alias != "button_limit":
1464
- payload.pop(alias, None)
1465
1721
  return payload
1466
1722
 
1467
1723
 
@@ -1613,6 +1869,7 @@ class AssociatedResourcesApplyRequest(StrictModel):
1613
1869
  if not isinstance(value, dict):
1614
1870
  return value
1615
1871
  payload = dict(value)
1872
+ default_target_app_key = str(payload.get("app_key", payload.get("appKey", "")) or "").strip()
1616
1873
  if "upsertResources" in payload and "upsert_resources" not in payload:
1617
1874
  payload["upsert_resources"] = payload.pop("upsertResources")
1618
1875
  if "patchResources" in payload and "patch_resources" not in payload:
@@ -1625,6 +1882,16 @@ class AssociatedResourcesApplyRequest(StrictModel):
1625
1882
  payload["reorder_associated_item_ids"] = payload.pop("reorderAssociatedItemIds")
1626
1883
  if "viewConfigs" in payload and "view_configs" not in payload:
1627
1884
  payload["view_configs"] = payload.pop("viewConfigs")
1885
+ if default_target_app_key and isinstance(payload.get("upsert_resources"), list):
1886
+ normalized_resources = []
1887
+ for item in payload["upsert_resources"]:
1888
+ if isinstance(item, dict) and not any(
1889
+ str(item.get(key) or "").strip()
1890
+ for key in ("target_app_key", "targetAppKey", "app_key", "appKey")
1891
+ ):
1892
+ item = {**item, "target_app_key": default_target_app_key}
1893
+ normalized_resources.append(item)
1894
+ payload["upsert_resources"] = normalized_resources
1628
1895
  return payload
1629
1896
 
1630
1897
  @model_validator(mode="after")
@@ -1648,7 +1915,7 @@ class ViewButtonBindingPatch(StrictModel):
1648
1915
  being_main: bool = Field(default=False, validation_alias=AliasChoices("being_main", "beingMain"))
1649
1916
  button_limit: list[list[ViewFilterRulePatch]] = Field(
1650
1917
  default_factory=list,
1651
- validation_alias=AliasChoices("button_limit", "buttonLimit", "visible_when", "visibleWhen"),
1918
+ validation_alias=AliasChoices("button_limit", "buttonLimit"),
1652
1919
  )
1653
1920
  button_formula: str | None = Field(default=None, validation_alias=AliasChoices("button_formula", "buttonFormula"))
1654
1921
  button_formula_type: int = Field(default=1, validation_alias=AliasChoices("button_formula_type", "buttonFormulaType"))
@@ -1677,12 +1944,9 @@ class ViewButtonBindingPatch(StrictModel):
1677
1944
  payload["config_type"] = "DETAIL"
1678
1945
  elif normalized_config in {"inside", "list", "row", "row_action"}:
1679
1946
  payload["config_type"] = "INSIDE"
1680
- raw_limits = payload.get("button_limit", payload.get("buttonLimit", payload.get("visible_when", payload.get("visibleWhen"))))
1947
+ raw_limits = payload.get("button_limit", payload.get("buttonLimit"))
1681
1948
  if isinstance(raw_limits, list) and raw_limits and all(isinstance(item, dict) for item in raw_limits):
1682
1949
  payload["button_limit"] = [raw_limits]
1683
- for alias in ("buttonLimit", "visible_when", "visibleWhen"):
1684
- if alias in payload and alias != "button_limit":
1685
- payload.pop(alias, None)
1686
1950
  return payload
1687
1951
 
1688
1952
  @model_validator(mode="after")
@@ -1722,45 +1986,13 @@ class ViewButtonBindingPatch(StrictModel):
1722
1986
 
1723
1987
  class ChartFilterRulePatch(StrictModel):
1724
1988
  field_name: str = Field(validation_alias=AliasChoices("field_name", "fieldName", "field", "name"))
1725
- operator: ViewFilterOperator = Field(validation_alias=AliasChoices("operator", "op"))
1989
+ operator: ViewFilterOperator = Field(default=ViewFilterOperator.eq, validation_alias=AliasChoices("operator", "op"))
1726
1990
  values: list[Any] = Field(default_factory=list)
1727
1991
 
1728
1992
  @model_validator(mode="before")
1729
1993
  @classmethod
1730
1994
  def normalize_aliases(cls, value: Any) -> Any:
1731
- if not isinstance(value, dict):
1732
- return value
1733
- payload = dict(value)
1734
- if "value" in payload and "values" not in payload:
1735
- raw_value = payload.pop("value")
1736
- payload["values"] = list(raw_value) if isinstance(raw_value, list) else [raw_value]
1737
- raw_operator = payload.get("operator", payload.get("op"))
1738
- if isinstance(raw_operator, str):
1739
- normalized = raw_operator.strip().lower()
1740
- operator_aliases = {
1741
- "equals": ViewFilterOperator.eq.value,
1742
- "equal": ViewFilterOperator.eq.value,
1743
- "=": ViewFilterOperator.eq.value,
1744
- "not_equals": ViewFilterOperator.neq.value,
1745
- "not_equal": ViewFilterOperator.neq.value,
1746
- "!=": ViewFilterOperator.neq.value,
1747
- ">=": ViewFilterOperator.gte.value,
1748
- "<=": ViewFilterOperator.lte.value,
1749
- "any_of": ViewFilterOperator.in_.value,
1750
- "one_of": ViewFilterOperator.in_.value,
1751
- "between_any": ViewFilterOperator.in_.value,
1752
- "empty": ViewFilterOperator.is_empty.value,
1753
- "is blank": ViewFilterOperator.is_empty.value,
1754
- "blank": ViewFilterOperator.is_empty.value,
1755
- "not_empty": ViewFilterOperator.not_empty.value,
1756
- "not blank": ViewFilterOperator.not_empty.value,
1757
- }
1758
- if normalized in operator_aliases:
1759
- payload["operator"] = operator_aliases[normalized]
1760
- elif "operator" not in payload:
1761
- payload["operator"] = normalized
1762
- payload.pop("op", None)
1763
- return payload
1995
+ return _normalize_public_condition_payload(value)
1764
1996
 
1765
1997
  @model_validator(mode="after")
1766
1998
  def validate_shape(self) -> "ChartFilterRulePatch":
@@ -2049,6 +2281,22 @@ class ChartApplyRequest(StrictModel):
2049
2281
  return self
2050
2282
 
2051
2283
 
2284
+ class PortalInlineChartPatch(ChartUpsertPatch):
2285
+ app_key: str = Field(validation_alias=AliasChoices("app_key", "appKey"))
2286
+
2287
+ @model_validator(mode="before")
2288
+ @classmethod
2289
+ def normalize_inline_aliases(cls, value: Any) -> Any:
2290
+ if not isinstance(value, dict):
2291
+ return value
2292
+ payload = dict(value)
2293
+ if "chart_name" in payload and "name" not in payload:
2294
+ payload["name"] = payload.pop("chart_name")
2295
+ if "chartName" in payload and "name" not in payload:
2296
+ payload["name"] = payload.pop("chartName")
2297
+ return payload
2298
+
2299
+
2052
2300
  class PortalComponentPositionPatch(StrictModel):
2053
2301
  pc_x: int = Field(default=0, validation_alias=AliasChoices("pc_x", "pcX", "x"))
2054
2302
  pc_y: int = Field(default=0, validation_alias=AliasChoices("pc_y", "pcY", "y"))
@@ -2095,7 +2343,6 @@ class PortalComponentPositionPatch(StrictModel):
2095
2343
  class PortalChartRefPatch(StrictModel):
2096
2344
  app_key: str | None = None
2097
2345
  chart_id: str | None = None
2098
- chart_key: str | None = None
2099
2346
  chart_name: str | None = None
2100
2347
 
2101
2348
  @model_validator(mode="before")
@@ -2104,24 +2351,26 @@ class PortalChartRefPatch(StrictModel):
2104
2351
  if not isinstance(value, dict):
2105
2352
  return value
2106
2353
  payload = dict(value)
2107
- if "chartKey" in payload and "chart_key" not in payload:
2108
- payload["chart_key"] = payload.pop("chartKey")
2109
2354
  if "chartId" in payload and "chart_id" not in payload:
2110
2355
  payload["chart_id"] = payload.pop("chartId")
2111
- if "biChartId" in payload and "chart_id" not in payload:
2112
- payload["chart_id"] = payload.pop("biChartId")
2356
+ if "chart_key" in payload and "chart_id" not in payload:
2357
+ payload["chart_id"] = payload.pop("chart_key")
2358
+ if "chartKey" in payload and "chart_id" not in payload:
2359
+ payload["chart_id"] = payload.pop("chartKey")
2113
2360
  if "chartName" in payload and "chart_name" not in payload:
2114
2361
  payload["chart_name"] = payload.pop("chartName")
2362
+ if "name" in payload and "chart_name" not in payload:
2363
+ payload["chart_name"] = payload.pop("name")
2115
2364
  if "appKey" in payload and "app_key" not in payload:
2116
2365
  payload["app_key"] = payload.pop("appKey")
2117
2366
  return payload
2118
2367
 
2119
2368
  @model_validator(mode="after")
2120
2369
  def validate_target(self) -> "PortalChartRefPatch":
2121
- if not (self.chart_id or self.chart_key or self.chart_name):
2122
- raise ValueError("chart_ref requires chart_id, chart_key, or chart_name")
2123
- if self.chart_name and not self.app_key and not (self.chart_id or self.chart_key):
2124
- raise ValueError("chart_ref with chart_name requires app_key unless chart_id/chart_key is also provided")
2370
+ if not (self.chart_id or self.chart_name):
2371
+ raise ValueError("chart_ref requires chart_id or chart_name")
2372
+ if self.chart_name and not self.chart_id and not self.app_key:
2373
+ raise ValueError("chart_ref.app_key is required when resolving by chart_name")
2125
2374
  return self
2126
2375
 
2127
2376
 
@@ -2147,12 +2396,12 @@ class PortalViewRefPatch(StrictModel):
2147
2396
  class PortalSectionPatch(StrictModel):
2148
2397
  title: str
2149
2398
  source_type: str = Field(validation_alias=AliasChoices("source_type", "sourceType"))
2150
- role: str | None = None
2399
+ role: str | None = Field(default=None, validation_alias=AliasChoices("role", "zone", "section_role", "sectionRole"))
2151
2400
  position: PortalComponentPositionPatch | None = None
2152
2401
  dash_style_config: dict[str, Any] | None = Field(default=None, validation_alias=AliasChoices("dash_style_config", "dashStyleConfigBO"))
2153
2402
  config: dict[str, Any] = Field(default_factory=dict)
2154
- chart: dict[str, Any] | None = None
2155
2403
  chart_ref: PortalChartRefPatch | None = None
2404
+ chart: PortalInlineChartPatch | None = Field(default=None, validation_alias=AliasChoices("chart", "inline_chart", "inlineChart", "create_chart", "createChart"))
2156
2405
  view_ref: PortalViewRefPatch | None = None
2157
2406
  text: str | None = None
2158
2407
  url: str | None = None
@@ -2166,8 +2415,32 @@ class PortalSectionPatch(StrictModel):
2166
2415
  raw_type = payload.get("source_type", payload.get("sourceType"))
2167
2416
  if isinstance(raw_type, str):
2168
2417
  payload["source_type"] = raw_type.strip().lower()
2418
+ raw_role = payload.get("role", payload.get("zone", payload.get("section_role", payload.get("sectionRole"))))
2419
+ if isinstance(raw_role, str):
2420
+ payload["role"] = raw_role.strip().lower()
2169
2421
  if "chartRef" in payload and "chart_ref" not in payload:
2170
2422
  payload["chart_ref"] = payload.pop("chartRef")
2423
+ if "inlineChart" in payload and "chart" not in payload:
2424
+ payload["chart"] = payload.pop("inlineChart")
2425
+ if "inline_chart" in payload and "chart" not in payload:
2426
+ payload["chart"] = payload.pop("inline_chart")
2427
+ if "createChart" in payload and "chart" not in payload:
2428
+ payload["chart"] = payload.pop("createChart")
2429
+ if "create_chart" in payload and "chart" not in payload:
2430
+ payload["chart"] = payload.pop("create_chart")
2431
+ raw_chart = payload.get("chart")
2432
+ if isinstance(raw_chart, dict):
2433
+ chart_payload = dict(raw_chart)
2434
+ has_ref_id = any(key in chart_payload for key in ("chart_id", "chartId", "chart_key", "chartKey"))
2435
+ has_chart_type = any(key in chart_payload for key in ("chart_type", "chartType"))
2436
+ has_create_config = has_chart_type or any(
2437
+ key in chart_payload
2438
+ for key in ("metric", "metrics", "group_by", "groupBy", "where", "filters", "dimensions")
2439
+ )
2440
+ has_ref_name = any(key in chart_payload for key in ("chart_name", "chartName", "name"))
2441
+ if has_ref_id or (has_ref_name and not has_create_config):
2442
+ payload["chart_ref"] = chart_payload
2443
+ payload.pop("chart", None)
2171
2444
  if "viewRef" in payload and "view_ref" not in payload:
2172
2445
  payload["view_ref"] = payload.pop("viewRef")
2173
2446
  if "dashStyleConfigBO" in payload and "dash_style_config" not in payload:
@@ -2179,18 +2452,8 @@ class PortalSectionPatch(StrictModel):
2179
2452
  supported = {"chart", "view", "grid", "filter", "text", "link"}
2180
2453
  if self.source_type not in supported:
2181
2454
  raise ValueError(f"unsupported portal source_type '{self.source_type}'")
2182
- if self.role is not None:
2183
- normalized_role = str(self.role or "").strip().lower()
2184
- if normalized_role not in {"metric"}:
2185
- raise ValueError(f"unsupported portal section role '{self.role}'")
2186
- self.role = normalized_role
2187
- if self.source_type != "chart":
2188
- raise ValueError("portal section role is only supported for chart sections")
2189
- if self.source_type == "chart":
2190
- if self.chart is not None and self.chart_ref is not None:
2191
- raise ValueError("chart section accepts either chart or chart_ref, not both")
2192
- if self.chart is None and self.chart_ref is None:
2193
- raise ValueError("chart section requires chart or chart_ref")
2455
+ if self.source_type == "chart" and self.chart_ref is None and self.chart is None:
2456
+ raise ValueError("chart section requires chart_ref or chart")
2194
2457
  if self.source_type == "view" and self.view_ref is None:
2195
2458
  raise ValueError("view section requires view_ref")
2196
2459
  if self.source_type == "text" and self.text is None:
@@ -2327,6 +2590,7 @@ AppReadSummaryResponse = AppGetResponse
2327
2590
  AppFieldsReadResponse = AppGetFieldsResponse
2328
2591
  AppLayoutReadResponse = AppGetLayoutResponse
2329
2592
  AppViewsReadResponse = AppGetViewsResponse
2593
+ AppFlowReadResponse = AppGetFlowResponse
2330
2594
  AppChartsReadResponse = AppGetChartsResponse
2331
2595
 
2332
2596
 
@@ -2393,7 +2657,6 @@ class SchemaPlanRequest(StrictModel):
2393
2657
  icon: str | None = None
2394
2658
  color: str | None = None
2395
2659
  visibility: VisibilityPatch | None = None
2396
- create_if_missing: bool = False
2397
2660
  add_fields: list[FieldPatch] = Field(default_factory=list)
2398
2661
  update_fields: list[FieldUpdatePatch] = Field(default_factory=list)
2399
2662
  remove_fields: list[FieldRemovePatch] = Field(default_factory=list)
@@ -2416,6 +2679,40 @@ class LayoutPlanRequest(StrictModel):
2416
2679
  return payload
2417
2680
 
2418
2681
 
2682
+ class FlowPlanRequest(StrictModel):
2683
+ app_key: str
2684
+ mode: str = "replace"
2685
+ nodes: list[FlowNodePatch] = Field(default_factory=list)
2686
+ transitions: list[FlowTransitionPatch] = Field(default_factory=list)
2687
+ preset: FlowPreset | None = None
2688
+
2689
+ @model_validator(mode="before")
2690
+ @classmethod
2691
+ def normalize_mode_alias(cls, value: Any) -> Any:
2692
+ if not isinstance(value, dict):
2693
+ return value
2694
+ payload = dict(value)
2695
+ if str(payload.get("mode") or "").strip().lower() == "overwrite":
2696
+ payload["mode"] = "replace"
2697
+ raw_preset = payload.get("preset")
2698
+ if raw_preset is None and isinstance(payload.get("base_preset"), str):
2699
+ raw_preset = payload["base_preset"]
2700
+ payload["preset"] = raw_preset
2701
+ if isinstance(raw_preset, str):
2702
+ normalized_preset = raw_preset.strip().lower()
2703
+ preset_aliases = {
2704
+ "default_approval": FlowPreset.basic_approval.value,
2705
+ "approval": FlowPreset.basic_approval.value,
2706
+ "basic approval": FlowPreset.basic_approval.value,
2707
+ "default_fill_then_approve": FlowPreset.basic_fill_then_approve.value,
2708
+ "default-fill-then-approve": FlowPreset.basic_fill_then_approve.value,
2709
+ "fill_then_approve": FlowPreset.basic_fill_then_approve.value,
2710
+ }
2711
+ if normalized_preset in preset_aliases:
2712
+ payload["preset"] = preset_aliases[normalized_preset]
2713
+ return payload
2714
+
2715
+
2419
2716
  class ViewsPlanRequest(StrictModel):
2420
2717
  app_key: str
2421
2718
  upsert_views: list[ViewUpsertPatch] = Field(default_factory=list)
@@ -2443,8 +2740,18 @@ def _normalize_field_payload(value: Any) -> Any:
2443
2740
  if not isinstance(value, dict):
2444
2741
  return value
2445
2742
  payload = dict(value)
2743
+ if "data_title" in payload and "as_data_title" not in payload and "asDataTitle" not in payload:
2744
+ payload["as_data_title"] = payload.pop("data_title")
2745
+ if "dataTitle" in payload and "as_data_title" not in payload and "asDataTitle" not in payload:
2746
+ payload["as_data_title"] = payload.pop("dataTitle")
2747
+ if "data_cover" in payload and "as_data_cover" not in payload and "asDataCover" not in payload:
2748
+ payload["as_data_cover"] = payload.pop("data_cover")
2749
+ if "dataCover" in payload and "as_data_cover" not in payload and "asDataCover" not in payload:
2750
+ payload["as_data_cover"] = payload.pop("dataCover")
2446
2751
  if "fields" in payload and "subfields" not in payload:
2447
2752
  payload["subfields"] = payload.pop("fields")
2753
+ if "options" in payload:
2754
+ payload["options"] = _normalize_field_options(payload.get("options"))
2448
2755
  raw_type = payload.get("type")
2449
2756
  if isinstance(raw_type, int):
2450
2757
  normalized_from_id = FIELD_TYPE_ID_ALIASES.get(raw_type)
@@ -2477,6 +2784,34 @@ def _normalize_field_payload(value: Any) -> Any:
2477
2784
  return payload
2478
2785
 
2479
2786
 
2787
+ def _normalize_field_options(value: Any) -> Any:
2788
+ if not isinstance(value, list):
2789
+ return value
2790
+ normalized: list[str] = []
2791
+ for item in value:
2792
+ if isinstance(item, str):
2793
+ normalized.append(item)
2794
+ continue
2795
+ if isinstance(item, (int, float, bool)):
2796
+ normalized.append(str(item))
2797
+ continue
2798
+ if isinstance(item, dict):
2799
+ label = (
2800
+ item.get("label")
2801
+ or item.get("name")
2802
+ or item.get("title")
2803
+ or item.get("value")
2804
+ or item.get("text")
2805
+ or item.get("optValue")
2806
+ or item.get("optName")
2807
+ )
2808
+ if label is not None:
2809
+ normalized.append(str(label))
2810
+ continue
2811
+ normalized.append(str(item))
2812
+ return normalized
2813
+
2814
+
2480
2815
  def _slugify_title(title: str) -> str:
2481
2816
  normalized = "".join(ch.lower() if ch.isalnum() else "_" for ch in str(title or ""))
2482
2817
  collapsed = "_".join(part for part in normalized.split("_") if part)