@josephyan/qingflow-app-builder-mcp 1.1.25 → 1.1.26
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.
- package/README.md +5 -6
- package/docs/local-agent-install.md +6 -57
- package/entry_point.py +1 -1
- package/npm/bin/qingflow-app-builder-mcp.mjs +33 -2
- package/npm/lib/runtime.mjs +101 -21
- package/npm/scripts/postinstall.mjs +10 -1
- package/package.json +2 -3
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-builder/SKILL.md +42 -104
- package/skills/qingflow-app-builder/references/build-complete-system.md +428 -0
- package/skills/qingflow-app-builder/references/build-single-app.md +530 -0
- package/skills/qingflow-app-builder/references/create-app.md +11 -30
- package/skills/qingflow-app-builder/references/environments.md +1 -1
- package/skills/qingflow-app-builder/references/gotchas.md +9 -31
- package/skills/qingflow-app-builder/references/match-rules.md +0 -15
- package/skills/qingflow-app-builder/references/solution-playbooks.md +0 -10
- package/skills/qingflow-app-builder/references/tool-selection.md +23 -38
- package/skills/qingflow-app-builder/references/update-flow.md +38 -22
- package/skills/qingflow-app-builder/references/update-schema.md +0 -14
- package/skills/qingflow-app-builder/references/update-views.md +14 -83
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +3 -5
- package/skills/qingflow-app-builder-code-integrations/references/code-block.md +1 -1
- package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +1 -1
- package/skills/qingflow-mcp-setup/SKILL.md +111 -0
- package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
- package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
- package/skills/qingflow-mcp-setup/references/environments.md +61 -0
- package/skills/qingflow-mcp-setup/references/generic-stdio.md +31 -0
- package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
- package/skills/qingflow-workflow-builder/SKILL.md +96 -0
- package/skills/qingflow-workflow-builder/manifest.yaml +8 -0
- package/skills/qingflow-workflow-builder/references/01-overview.md +45 -0
- package/skills/qingflow-workflow-builder/references/02-update-mode.md +53 -0
- package/skills/qingflow-workflow-builder/references/03-flow-patterns.md +57 -0
- package/skills/qingflow-workflow-builder/references/04-stage1-business-modeling.md +131 -0
- package/skills/qingflow-workflow-builder/references/05-stage2-members-roles.md +29 -0
- package/skills/qingflow-workflow-builder/references/06-stage3-build-spec.md +165 -0
- package/skills/qingflow-workflow-builder/references/07-stage4-validate-spec.md +33 -0
- package/skills/qingflow-workflow-builder/references/08-stage5-apply-verify.md +51 -0
- package/skills/qingflow-workflow-builder/references/09-stage6-summary.md +88 -0
- package/skills/qingflow-workflow-builder/references/10-node-config-reference.md +93 -0
- package/skills/qingflow-workflow-builder/references/11-troubleshooting.md +15 -0
- package/skills/qingflow-workflow-builder/scripts/diff_flow_spec.py +275 -0
- package/skills/qingflow-workflow-builder/scripts/validate_flow_spec.py +605 -0
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/__main__.py +2 -6
- package/src/qingflow_mcp/builder_facade/models.py +108 -485
- package/src/qingflow_mcp/builder_facade/service.py +1686 -6674
- package/src/qingflow_mcp/cli/commands/builder.py +364 -322
- package/src/qingflow_mcp/cli/commands/chart.py +1 -1
- package/src/qingflow_mcp/cli/commands/common.py +3 -12
- package/src/qingflow_mcp/cli/commands/exports.py +2 -2
- package/src/qingflow_mcp/cli/commands/imports.py +3 -3
- package/src/qingflow_mcp/cli/commands/portal.py +2 -2
- package/src/qingflow_mcp/cli/commands/record.py +27 -101
- package/src/qingflow_mcp/cli/commands/task.py +47 -28
- package/src/qingflow_mcp/cli/commands/view.py +1 -1
- package/src/qingflow_mcp/cli/context.py +3 -0
- package/src/qingflow_mcp/cli/formatters.py +16 -784
- package/src/qingflow_mcp/cli/main.py +33 -117
- package/src/qingflow_mcp/errors.py +2 -43
- package/src/qingflow_mcp/public_surface.py +17 -26
- package/src/qingflow_mcp/response_trim.py +17 -81
- package/src/qingflow_mcp/server.py +12 -14
- package/src/qingflow_mcp/server_app_builder.py +36 -79
- package/src/qingflow_mcp/server_app_user.py +16 -22
- package/src/qingflow_mcp/session_store.py +7 -11
- package/src/qingflow_mcp/solution/compiler/__init__.py +1 -3
- package/src/qingflow_mcp/solution/executor.py +18 -245
- package/src/qingflow_mcp/tools/ai_builder_tools.py +1396 -3103
- package/src/qingflow_mcp/tools/app_tools.py +43 -184
- package/src/qingflow_mcp/tools/approval_tools.py +35 -197
- package/src/qingflow_mcp/tools/auth_tools.py +16 -92
- package/src/qingflow_mcp/tools/code_block_tools.py +40 -298
- package/src/qingflow_mcp/tools/custom_button_tools.py +10 -64
- package/src/qingflow_mcp/tools/directory_tools.py +72 -236
- package/src/qingflow_mcp/tools/export_tools.py +34 -244
- package/src/qingflow_mcp/tools/feedback_tools.py +0 -9
- package/src/qingflow_mcp/tools/file_tools.py +3 -9
- package/src/qingflow_mcp/tools/import_tools.py +49 -336
- package/src/qingflow_mcp/tools/navigation_tools.py +12 -91
- package/src/qingflow_mcp/tools/package_tools.py +6 -118
- package/src/qingflow_mcp/tools/portal_tools.py +3 -39
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +7 -116
- package/src/qingflow_mcp/tools/record_tools.py +360 -1145
- package/src/qingflow_mcp/tools/resource_read_tools.py +39 -188
- package/src/qingflow_mcp/tools/role_tools.py +9 -80
- package/src/qingflow_mcp/tools/solution_tools.py +45 -59
- package/src/qingflow_mcp/tools/task_context_tools.py +158 -662
- package/src/qingflow_mcp/tools/task_tools.py +29 -113
- package/src/qingflow_mcp/tools/view_tools.py +3 -106
- package/src/qingflow_mcp/tools/workflow_tools.py +4 -48
- package/src/qingflow_mcp/tools/workspace_tools.py +3 -71
- package/npm/bin/qingflow-skills.mjs +0 -5
- package/skills/qingflow-app-builder/references/complete-system-development-guide.md +0 -172
- package/skills/qingflow-app-builder/references/single-app-development-guide.md +0 -87
- package/skills/qingflow-app-builder/scripts/validate_system_build_summary.py +0 -124
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +0 -173
|
@@ -71,36 +71,17 @@ class PublicExternalVisibilityMode(str, Enum):
|
|
|
71
71
|
|
|
72
72
|
|
|
73
73
|
FIELD_TYPE_ALIASES: dict[str, PublicFieldType] = {
|
|
74
|
-
"multiline": PublicFieldType.long_text,
|
|
75
|
-
"multiline_text": PublicFieldType.long_text,
|
|
76
|
-
"multi_line": PublicFieldType.long_text,
|
|
77
|
-
"multi_line_text": PublicFieldType.long_text,
|
|
78
74
|
"textarea": PublicFieldType.long_text,
|
|
79
|
-
"longtext": PublicFieldType.long_text,
|
|
80
|
-
"long-text": PublicFieldType.long_text,
|
|
81
75
|
"amount": PublicFieldType.amount,
|
|
82
76
|
"currency": PublicFieldType.amount,
|
|
83
77
|
"mobile": PublicFieldType.phone,
|
|
84
78
|
"user": PublicFieldType.member,
|
|
85
79
|
"users": PublicFieldType.member,
|
|
86
80
|
"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,
|
|
92
81
|
"radio": PublicFieldType.single_select,
|
|
93
82
|
"checkbox": PublicFieldType.multi_select,
|
|
94
83
|
"multi_select": PublicFieldType.multi_select,
|
|
95
84
|
"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,
|
|
104
85
|
"departments": PublicFieldType.department,
|
|
105
86
|
"qlinker": PublicFieldType.q_linker,
|
|
106
87
|
"q_linker": PublicFieldType.q_linker,
|
|
@@ -203,10 +184,6 @@ class LayoutPreset(str, Enum):
|
|
|
203
184
|
single_section = "single_section"
|
|
204
185
|
|
|
205
186
|
|
|
206
|
-
class FlowPreset(str, Enum):
|
|
207
|
-
basic_approval = "basic_approval"
|
|
208
|
-
basic_fill_then_approve = "basic_fill_then_approve"
|
|
209
|
-
|
|
210
187
|
|
|
211
188
|
class ViewsPreset(str, Enum):
|
|
212
189
|
default_table = "default_table"
|
|
@@ -1105,91 +1082,6 @@ class ViewAssociatedResourcesPatch(StrictModel):
|
|
|
1105
1082
|
)
|
|
1106
1083
|
|
|
1107
1084
|
|
|
1108
|
-
class ViewActionButtonPatch(StrictModel):
|
|
1109
|
-
button_id: int | None = Field(default=None, validation_alias=AliasChoices("button_id", "buttonId", "id"))
|
|
1110
|
-
client_key: str | None = Field(default=None, validation_alias=AliasChoices("client_key", "clientKey"))
|
|
1111
|
-
text: str = Field(validation_alias=AliasChoices("text", "button_text", "buttonText", "name"))
|
|
1112
|
-
action: PublicButtonTriggerAction = Field(validation_alias=AliasChoices("action", "trigger_action", "triggerAction"))
|
|
1113
|
-
url: str | None = Field(default=None, validation_alias=AliasChoices("url", "link_url", "linkUrl", "trigger_link_url", "triggerLinkUrl"))
|
|
1114
|
-
target_app_key: str | None = Field(default=None, validation_alias=AliasChoices("target_app_key", "targetAppKey", "related_app_key", "relatedAppKey"))
|
|
1115
|
-
target_app_name: str | None = Field(default=None, validation_alias=AliasChoices("target_app_name", "targetAppName", "related_app_name", "relatedAppName"))
|
|
1116
|
-
field_mappings: list[dict[str, Any]] = Field(default_factory=list, validation_alias=AliasChoices("field_mappings", "fieldMappings", "mappings"))
|
|
1117
|
-
default_values: dict[str, Any] = Field(default_factory=dict, validation_alias=AliasChoices("default_values", "defaultValues", "defaults"))
|
|
1118
|
-
placement: PublicButtonPlacement = Field(default=PublicButtonPlacement.detail, validation_alias=AliasChoices("placement", "position"))
|
|
1119
|
-
primary: bool = Field(default=False, validation_alias=AliasChoices("primary", "being_main", "beingMain"))
|
|
1120
|
-
visible_when: list[list[ViewFilterRulePatch]] = Field(
|
|
1121
|
-
default_factory=list,
|
|
1122
|
-
validation_alias=AliasChoices("visible_when", "visibleWhen", "button_limit", "buttonLimit"),
|
|
1123
|
-
)
|
|
1124
|
-
button_formula: str | None = Field(default=None, validation_alias=AliasChoices("button_formula", "buttonFormula"))
|
|
1125
|
-
button_formula_type: int = Field(default=1, validation_alias=AliasChoices("button_formula_type", "buttonFormulaType"))
|
|
1126
|
-
print_tpls: list[Any] = Field(default_factory=list, validation_alias=AliasChoices("print_tpls", "printTpls"))
|
|
1127
|
-
style_preset: str | None = Field(default=None, validation_alias=AliasChoices("style_preset", "stylePreset"))
|
|
1128
|
-
background_color: str | None = Field(default=None, validation_alias=AliasChoices("background_color", "backgroundColor"))
|
|
1129
|
-
text_color: str | None = Field(default=None, validation_alias=AliasChoices("text_color", "textColor"))
|
|
1130
|
-
button_icon: str | None = Field(default=None, validation_alias=AliasChoices("button_icon", "buttonIcon"))
|
|
1131
|
-
external_qrobot_config: dict[str, Any] | None = Field(
|
|
1132
|
-
default=None,
|
|
1133
|
-
validation_alias=AliasChoices(
|
|
1134
|
-
"external_qrobot_config",
|
|
1135
|
-
"externalQrobotConfig",
|
|
1136
|
-
"externalQRobotConfig",
|
|
1137
|
-
"custom_button_external_qrobot_relation_vo",
|
|
1138
|
-
"customButtonExternalQRobotRelationVO",
|
|
1139
|
-
),
|
|
1140
|
-
)
|
|
1141
|
-
trigger_wings_config: dict[str, Any] | None = Field(default=None, validation_alias=AliasChoices("trigger_wings_config", "triggerWingsConfig"))
|
|
1142
|
-
|
|
1143
|
-
@model_validator(mode="before")
|
|
1144
|
-
@classmethod
|
|
1145
|
-
def normalize_aliases(cls, value: Any) -> Any:
|
|
1146
|
-
if not isinstance(value, dict):
|
|
1147
|
-
return value
|
|
1148
|
-
payload = dict(value)
|
|
1149
|
-
raw_action = payload.get("action", payload.get("trigger_action", payload.get("triggerAction")))
|
|
1150
|
-
if isinstance(raw_action, str):
|
|
1151
|
-
normalized = raw_action.strip()
|
|
1152
|
-
aliases = {
|
|
1153
|
-
"add_data": PublicButtonTriggerAction.add_data.value,
|
|
1154
|
-
"add-data": PublicButtonTriggerAction.add_data.value,
|
|
1155
|
-
"adddata": PublicButtonTriggerAction.add_data.value,
|
|
1156
|
-
"addData": PublicButtonTriggerAction.add_data.value,
|
|
1157
|
-
"link": PublicButtonTriggerAction.link.value,
|
|
1158
|
-
"url": PublicButtonTriggerAction.link.value,
|
|
1159
|
-
"qrobot": PublicButtonTriggerAction.qrobot.value,
|
|
1160
|
-
"qRobot": PublicButtonTriggerAction.qrobot.value,
|
|
1161
|
-
"wings": PublicButtonTriggerAction.wings.value,
|
|
1162
|
-
}
|
|
1163
|
-
payload["action"] = aliases.get(normalized, aliases.get(normalized.lower(), normalized))
|
|
1164
|
-
raw_placement = payload.get("placement", payload.get("position", payload.get("config_type", payload.get("configType"))))
|
|
1165
|
-
if isinstance(raw_placement, str):
|
|
1166
|
-
normalized = raw_placement.strip().lower()
|
|
1167
|
-
if normalized in {"top", "header"}:
|
|
1168
|
-
payload["placement"] = PublicButtonPlacement.header.value
|
|
1169
|
-
elif normalized in {"detail", "data_detail"}:
|
|
1170
|
-
payload["placement"] = PublicButtonPlacement.detail.value
|
|
1171
|
-
elif normalized in {"list", "row", "row_action", "inside"}:
|
|
1172
|
-
payload["placement"] = PublicButtonPlacement.list.value
|
|
1173
|
-
raw_limits = payload.get("visible_when", payload.get("visibleWhen", payload.get("button_limit", payload.get("buttonLimit"))))
|
|
1174
|
-
if isinstance(raw_limits, list) and raw_limits and all(isinstance(item, dict) for item in raw_limits):
|
|
1175
|
-
payload["visible_when"] = [raw_limits]
|
|
1176
|
-
return payload
|
|
1177
|
-
|
|
1178
|
-
@model_validator(mode="after")
|
|
1179
|
-
def validate_shape(self) -> "ViewActionButtonPatch":
|
|
1180
|
-
if not str(self.text or "").strip():
|
|
1181
|
-
raise ValueError("action_buttons[].text is required")
|
|
1182
|
-
if self.action == PublicButtonTriggerAction.link and not str(self.url or "").strip():
|
|
1183
|
-
raise ValueError("link action_buttons require url")
|
|
1184
|
-
if self.action == PublicButtonTriggerAction.add_data and not str(self.target_app_key or "").strip():
|
|
1185
|
-
raise ValueError("add_data action_buttons require target_app_key")
|
|
1186
|
-
if self.action == PublicButtonTriggerAction.qrobot and self.external_qrobot_config is None:
|
|
1187
|
-
raise ValueError("qRobot action_buttons require external_qrobot_config")
|
|
1188
|
-
if self.action == PublicButtonTriggerAction.wings and self.trigger_wings_config is None:
|
|
1189
|
-
raise ValueError("wings action_buttons require trigger_wings_config")
|
|
1190
|
-
return self
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
1085
|
class ViewUpsertPatch(StrictModel):
|
|
1194
1086
|
name: str
|
|
1195
1087
|
view_key: str | None = Field(default=None, validation_alias=AliasChoices("view_key", "viewKey"))
|
|
@@ -1201,8 +1093,6 @@ class ViewUpsertPatch(StrictModel):
|
|
|
1201
1093
|
end_field: str | None = Field(default=None, validation_alias=AliasChoices("end_field", "endField"))
|
|
1202
1094
|
title_field: str | None = Field(default=None, validation_alias=AliasChoices("title_field", "titleField"))
|
|
1203
1095
|
buttons: list["ViewButtonBindingPatch"] | None = None
|
|
1204
|
-
action_buttons: list[ViewActionButtonPatch] | None = Field(default=None, validation_alias=AliasChoices("action_buttons", "actionButtons"))
|
|
1205
|
-
action_buttons_mode: str = Field(default="merge", validation_alias=AliasChoices("action_buttons_mode", "actionButtonsMode", "button_mode", "buttonMode"))
|
|
1206
1096
|
visibility: VisibilityPatch | None = None
|
|
1207
1097
|
query_conditions: ViewQueryConditionsPatch | None = Field(default=None, validation_alias=AliasChoices("query_conditions", "queryConditions", "query_condition", "queryCondition"))
|
|
1208
1098
|
associated_resources: ViewAssociatedResourcesPatch | None = Field(
|
|
@@ -1252,12 +1142,6 @@ class ViewUpsertPatch(StrictModel):
|
|
|
1252
1142
|
payload["associated_resources"] = payload.pop("associatedReports")
|
|
1253
1143
|
if "asosChartConfig" in payload and "associated_resources" not in payload:
|
|
1254
1144
|
payload["associated_resources"] = payload.pop("asosChartConfig")
|
|
1255
|
-
if "actionButtons" in payload and "action_buttons" not in payload:
|
|
1256
|
-
payload["action_buttons"] = payload.pop("actionButtons")
|
|
1257
|
-
if "actionButtonsMode" in payload and "action_buttons_mode" not in payload:
|
|
1258
|
-
payload["action_buttons_mode"] = payload.pop("actionButtonsMode")
|
|
1259
|
-
if "buttonMode" in payload and "action_buttons_mode" not in payload:
|
|
1260
|
-
payload["action_buttons_mode"] = payload.pop("buttonMode")
|
|
1261
1145
|
raw_type = payload.get("type")
|
|
1262
1146
|
if isinstance(raw_type, str):
|
|
1263
1147
|
normalized = raw_type.strip().lower()
|
|
@@ -1279,10 +1163,6 @@ class ViewUpsertPatch(StrictModel):
|
|
|
1279
1163
|
raise ValueError("board view requires group_by")
|
|
1280
1164
|
if self.type == PublicViewType.gantt and not (self.start_field and self.end_field):
|
|
1281
1165
|
raise ValueError("gantt view requires start_field and end_field")
|
|
1282
|
-
normalized_mode = str(self.action_buttons_mode or "merge").strip().lower()
|
|
1283
|
-
if normalized_mode not in {"merge", "replace"}:
|
|
1284
|
-
raise ValueError("action_buttons_mode must be merge or replace")
|
|
1285
|
-
self.action_buttons_mode = normalized_mode
|
|
1286
1166
|
return self
|
|
1287
1167
|
|
|
1288
1168
|
|
|
@@ -1308,76 +1188,6 @@ class ViewPartialPatch(StrictModel):
|
|
|
1308
1188
|
return self
|
|
1309
1189
|
|
|
1310
1190
|
|
|
1311
|
-
def public_button_trigger_action_value(value: Any) -> str:
|
|
1312
|
-
raw = value.value if isinstance(value, PublicButtonTriggerAction) else str(value or "").strip()
|
|
1313
|
-
normalized = raw.lower()
|
|
1314
|
-
if raw == PublicButtonTriggerAction.add_data.value or normalized in {"add_data", "add-data", "adddata"}:
|
|
1315
|
-
return "add_data"
|
|
1316
|
-
if normalized in {PublicButtonTriggerAction.link.value, "url"}:
|
|
1317
|
-
return "link"
|
|
1318
|
-
if raw == PublicButtonTriggerAction.qrobot.value or normalized == "qrobot":
|
|
1319
|
-
return "qRobot"
|
|
1320
|
-
if normalized == PublicButtonTriggerAction.wings.value:
|
|
1321
|
-
return "wings"
|
|
1322
|
-
return raw
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
def _drop_empty_public_payload_values(payload: dict[str, Any]) -> dict[str, Any]:
|
|
1326
|
-
compact: dict[str, Any] = {}
|
|
1327
|
-
for key, item in payload.items():
|
|
1328
|
-
if item is None or item == "" or item == [] or item == {}:
|
|
1329
|
-
continue
|
|
1330
|
-
if key == "primary" and item is False:
|
|
1331
|
-
continue
|
|
1332
|
-
compact[key] = item
|
|
1333
|
-
return compact
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
def public_view_action_button_payload(button: Any, *, compact: bool = False) -> dict[str, Any]:
|
|
1337
|
-
if isinstance(button, ViewActionButtonPatch):
|
|
1338
|
-
payload = button.model_dump(mode="json")
|
|
1339
|
-
elif isinstance(button, dict):
|
|
1340
|
-
payload = dict(button)
|
|
1341
|
-
else:
|
|
1342
|
-
return {}
|
|
1343
|
-
raw_action = payload.get("action", payload.get("trigger_action", payload.get("triggerAction")))
|
|
1344
|
-
if raw_action is not None:
|
|
1345
|
-
payload["action"] = public_button_trigger_action_value(raw_action)
|
|
1346
|
-
payload.pop("trigger_action", None)
|
|
1347
|
-
payload.pop("triggerAction", None)
|
|
1348
|
-
if compact:
|
|
1349
|
-
return _drop_empty_public_payload_values(payload)
|
|
1350
|
-
return payload
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
def public_view_upsert_payload(view: ViewUpsertPatch) -> dict[str, Any]:
|
|
1354
|
-
payload = view.model_dump(mode="json")
|
|
1355
|
-
if view.action_buttons is not None:
|
|
1356
|
-
payload["action_buttons"] = [
|
|
1357
|
-
public_view_action_button_payload(button, compact=True)
|
|
1358
|
-
for button in view.action_buttons
|
|
1359
|
-
]
|
|
1360
|
-
return payload
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
def public_view_partial_payload(patch: ViewPartialPatch) -> dict[str, Any]:
|
|
1364
|
-
payload = patch.model_dump(mode="json")
|
|
1365
|
-
patch_set = payload.get("set")
|
|
1366
|
-
if isinstance(patch_set, dict):
|
|
1367
|
-
raw_buttons = patch_set.get("action_buttons")
|
|
1368
|
-
if raw_buttons is None:
|
|
1369
|
-
raw_buttons = patch_set.get("actionButtons")
|
|
1370
|
-
if isinstance(raw_buttons, list):
|
|
1371
|
-
patch_set = dict(patch_set)
|
|
1372
|
-
patch_set.pop("actionButtons", None)
|
|
1373
|
-
patch_set["action_buttons"] = [
|
|
1374
|
-
public_view_action_button_payload(button, compact=True)
|
|
1375
|
-
for button in raw_buttons
|
|
1376
|
-
]
|
|
1377
|
-
payload["set"] = patch_set
|
|
1378
|
-
return payload
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
1191
|
class CustomButtonJudgeValuePatch(StrictModel):
|
|
1382
1192
|
id: int | str | None = None
|
|
1383
1193
|
value: Any | None = None
|
|
@@ -1446,30 +1256,11 @@ class CustomButtonFieldMappingPatch(StrictModel):
|
|
|
1446
1256
|
|
|
1447
1257
|
|
|
1448
1258
|
class FieldMatchMappingPatch(StrictModel):
|
|
1449
|
-
target_field: Any = Field(
|
|
1450
|
-
validation_alias=AliasChoices("target_field", "targetField", "target", "field", "field_name", "fieldName")
|
|
1451
|
-
)
|
|
1259
|
+
target_field: Any = Field(validation_alias=AliasChoices("target_field", "targetField", "target", "field"))
|
|
1452
1260
|
source_field: Any | None = Field(default=None, validation_alias=AliasChoices("source_field", "sourceField", "source"))
|
|
1453
1261
|
value: Any | None = Field(default=None, validation_alias=AliasChoices("value", "static_value", "staticValue"))
|
|
1454
1262
|
operator: str = Field(default="eq", validation_alias=AliasChoices("operator", "op", "judge_type", "judgeType"))
|
|
1455
1263
|
|
|
1456
|
-
@model_validator(mode="before")
|
|
1457
|
-
@classmethod
|
|
1458
|
-
def normalize_semantic_aliases(cls, value: Any) -> Any:
|
|
1459
|
-
if not isinstance(value, dict):
|
|
1460
|
-
return value
|
|
1461
|
-
payload = dict(value)
|
|
1462
|
-
has_static_value = any(key in payload for key in ("value", "static_value", "staticValue"))
|
|
1463
|
-
if "values" in payload:
|
|
1464
|
-
values = payload.pop("values")
|
|
1465
|
-
if has_static_value:
|
|
1466
|
-
return payload
|
|
1467
|
-
if isinstance(values, list) and len(values) == 1:
|
|
1468
|
-
payload["value"] = values[0]
|
|
1469
|
-
else:
|
|
1470
|
-
payload["value"] = values
|
|
1471
|
-
return payload
|
|
1472
|
-
|
|
1473
1264
|
@model_validator(mode="after")
|
|
1474
1265
|
def validate_shape(self) -> "FieldMatchMappingPatch":
|
|
1475
1266
|
has_source = self.source_field is not None and str(self.source_field).strip() != ""
|
|
@@ -1816,7 +1607,6 @@ class AssociatedResourcesApplyRequest(StrictModel):
|
|
|
1816
1607
|
if not isinstance(value, dict):
|
|
1817
1608
|
return value
|
|
1818
1609
|
payload = dict(value)
|
|
1819
|
-
default_target_app_key = str(payload.get("app_key", payload.get("appKey", "")) or "").strip()
|
|
1820
1610
|
if "upsertResources" in payload and "upsert_resources" not in payload:
|
|
1821
1611
|
payload["upsert_resources"] = payload.pop("upsertResources")
|
|
1822
1612
|
if "patchResources" in payload and "patch_resources" not in payload:
|
|
@@ -1829,16 +1619,6 @@ class AssociatedResourcesApplyRequest(StrictModel):
|
|
|
1829
1619
|
payload["reorder_associated_item_ids"] = payload.pop("reorderAssociatedItemIds")
|
|
1830
1620
|
if "viewConfigs" in payload and "view_configs" not in payload:
|
|
1831
1621
|
payload["view_configs"] = payload.pop("viewConfigs")
|
|
1832
|
-
if default_target_app_key and isinstance(payload.get("upsert_resources"), list):
|
|
1833
|
-
normalized_resources = []
|
|
1834
|
-
for item in payload["upsert_resources"]:
|
|
1835
|
-
if isinstance(item, dict) and not any(
|
|
1836
|
-
str(item.get(key) or "").strip()
|
|
1837
|
-
for key in ("target_app_key", "targetAppKey", "app_key", "appKey")
|
|
1838
|
-
):
|
|
1839
|
-
item = {**item, "target_app_key": default_target_app_key}
|
|
1840
|
-
normalized_resources.append(item)
|
|
1841
|
-
payload["upsert_resources"] = normalized_resources
|
|
1842
1622
|
return payload
|
|
1843
1623
|
|
|
1844
1624
|
@model_validator(mode="after")
|
|
@@ -1983,63 +1763,68 @@ class ChartFilterRulePatch(StrictModel):
|
|
|
1983
1763
|
return self
|
|
1984
1764
|
|
|
1985
1765
|
|
|
1986
|
-
|
|
1987
|
-
op: str = "count"
|
|
1988
|
-
field_name: str | None = Field(default=None, validation_alias=AliasChoices("field", "field_name", "fieldName", "name"))
|
|
1989
|
-
alias: str | None = None
|
|
1766
|
+
_CHART_METRIC_RE = re.compile(r"^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\((.*)\)\s*$")
|
|
1990
1767
|
|
|
1991
|
-
@model_validator(mode="before")
|
|
1992
|
-
@classmethod
|
|
1993
|
-
def normalize_metric(cls, value: Any) -> Any:
|
|
1994
|
-
if isinstance(value, str):
|
|
1995
|
-
raw = value.strip()
|
|
1996
|
-
match = re.fullmatch(r"([A-Za-z_][A-Za-z0-9_]*)\s*\(\s*(.*?)\s*\)", raw)
|
|
1997
|
-
if match:
|
|
1998
|
-
op = match.group(1).strip().lower()
|
|
1999
|
-
field = match.group(2).strip()
|
|
2000
|
-
payload: dict[str, Any] = {"op": op}
|
|
2001
|
-
if field and field != "*":
|
|
2002
|
-
payload["field_name"] = field
|
|
2003
|
-
return payload
|
|
2004
|
-
if raw:
|
|
2005
|
-
return {"op": "sum", "field_name": raw}
|
|
2006
|
-
return {"op": "count"}
|
|
2007
|
-
if not isinstance(value, dict):
|
|
2008
|
-
return value
|
|
2009
|
-
payload = dict(value)
|
|
2010
|
-
if "operation" in payload and "op" not in payload:
|
|
2011
|
-
payload["op"] = payload.pop("operation")
|
|
2012
|
-
if "agg" in payload and "op" not in payload:
|
|
2013
|
-
payload["op"] = payload.pop("agg")
|
|
2014
|
-
if "aggregate" in payload and "op" not in payload:
|
|
2015
|
-
payload["op"] = payload.pop("aggregate")
|
|
2016
|
-
if "aggregation" in payload and "op" not in payload:
|
|
2017
|
-
payload["op"] = payload.pop("aggregation")
|
|
2018
|
-
return payload
|
|
2019
1768
|
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
1769
|
+
def _normalize_chart_string_list(value: Any) -> list[str]:
|
|
1770
|
+
if value is None:
|
|
1771
|
+
return []
|
|
1772
|
+
if isinstance(value, (list, tuple)):
|
|
1773
|
+
return [str(item).strip() for item in value if item is not None and str(item).strip()]
|
|
1774
|
+
raw = str(value).strip()
|
|
1775
|
+
return [raw] if raw else []
|
|
1776
|
+
|
|
1777
|
+
|
|
1778
|
+
def _parse_chart_metric_item(value: Any) -> tuple[list[str], str | None]:
|
|
1779
|
+
if value is None:
|
|
1780
|
+
return [], None
|
|
1781
|
+
if isinstance(value, dict):
|
|
1782
|
+
op = str(value.get("op") or value.get("agg") or value.get("aggregate") or value.get("aggregation") or "").strip().lower()
|
|
1783
|
+
field = value.get("field", value.get("field_name", value.get("name", value.get("selector"))))
|
|
1784
|
+
selectors = _normalize_chart_string_list(field)
|
|
1785
|
+
return selectors, op or ("count" if not selectors else None)
|
|
1786
|
+
raw = str(value).strip()
|
|
1787
|
+
if not raw:
|
|
1788
|
+
return [], None
|
|
1789
|
+
matched = _CHART_METRIC_RE.match(raw)
|
|
1790
|
+
if matched:
|
|
1791
|
+
op = matched.group(1).strip().lower()
|
|
1792
|
+
field = matched.group(2).strip()
|
|
1793
|
+
if field in {"*", ""}:
|
|
1794
|
+
return [], op
|
|
1795
|
+
return [field], op
|
|
1796
|
+
if raw.lower() in {"count", "count(*)"}:
|
|
1797
|
+
return [], "count"
|
|
1798
|
+
return [raw], None
|
|
1799
|
+
|
|
1800
|
+
|
|
1801
|
+
def _apply_semantic_chart_metric_aliases(payload: dict[str, Any]) -> None:
|
|
1802
|
+
raw_metrics: list[Any] = []
|
|
1803
|
+
if "metric" in payload:
|
|
1804
|
+
raw_metrics.append(payload.pop("metric"))
|
|
1805
|
+
if "metrics" in payload:
|
|
1806
|
+
metrics = payload.pop("metrics")
|
|
1807
|
+
raw_metrics.extend(metrics if isinstance(metrics, list) else [metrics])
|
|
1808
|
+
if not raw_metrics:
|
|
1809
|
+
return
|
|
1810
|
+
selectors: list[str] = []
|
|
1811
|
+
aggregates: list[str] = []
|
|
1812
|
+
for metric in raw_metrics:
|
|
1813
|
+
metric_selectors, aggregate = _parse_chart_metric_item(metric)
|
|
1814
|
+
selectors.extend(metric_selectors)
|
|
1815
|
+
if aggregate:
|
|
1816
|
+
aggregates.append(aggregate)
|
|
1817
|
+
if selectors and "indicator_field_ids" not in payload:
|
|
1818
|
+
payload["indicator_field_ids"] = selectors
|
|
1819
|
+
if aggregates:
|
|
1820
|
+
normalized_aggregate = aggregates[0]
|
|
1821
|
+
config = payload.get("config")
|
|
1822
|
+
if not isinstance(config, dict):
|
|
1823
|
+
config = {}
|
|
1824
|
+
else:
|
|
1825
|
+
config = dict(config)
|
|
1826
|
+
config.setdefault("aggregate", normalized_aggregate)
|
|
1827
|
+
payload["config"] = config
|
|
2043
1828
|
|
|
2044
1829
|
|
|
2045
1830
|
class ChartUpsertPatch(StrictModel):
|
|
@@ -2049,17 +1834,6 @@ class ChartUpsertPatch(StrictModel):
|
|
|
2049
1834
|
dimension_field_ids: list[str] = Field(default_factory=list)
|
|
2050
1835
|
indicator_field_ids: list[str] = Field(default_factory=list)
|
|
2051
1836
|
filters: list[ChartFilterRulePatch] = Field(default_factory=list)
|
|
2052
|
-
group_by: list[str] = Field(default_factory=list)
|
|
2053
|
-
rows: list[str] = Field(default_factory=list)
|
|
2054
|
-
columns: list[str] = Field(default_factory=list)
|
|
2055
|
-
metric: ChartMetricPatch | None = None
|
|
2056
|
-
metrics: list[ChartMetricPatch] = Field(default_factory=list)
|
|
2057
|
-
x_metric: ChartMetricPatch | None = None
|
|
2058
|
-
y_metric: ChartMetricPatch | None = None
|
|
2059
|
-
left_metric: ChartMetricPatch | None = None
|
|
2060
|
-
right_metric: ChartMetricPatch | None = None
|
|
2061
|
-
value_metric: ChartMetricPatch | None = None
|
|
2062
|
-
target_metric: ChartMetricPatch | None = None
|
|
2063
1837
|
question_config: list[dict[str, Any]] = Field(default_factory=list)
|
|
2064
1838
|
user_config: list[dict[str, Any]] = Field(default_factory=list)
|
|
2065
1839
|
config: dict[str, Any] = Field(default_factory=dict)
|
|
@@ -2077,74 +1851,19 @@ class ChartUpsertPatch(StrictModel):
|
|
|
2077
1851
|
payload["chart_type"] = payload.pop("type")
|
|
2078
1852
|
if "dimension_fields" in payload and "dimension_field_ids" not in payload:
|
|
2079
1853
|
payload["dimension_field_ids"] = payload.pop("dimension_fields")
|
|
2080
|
-
if "dimensions" in payload and "dimension_field_ids" not in payload and "group_by" not in payload:
|
|
2081
|
-
payload["group_by"] = payload.pop("dimensions")
|
|
2082
|
-
if "groupBy" in payload and "group_by" not in payload:
|
|
2083
|
-
payload["group_by"] = payload.pop("groupBy")
|
|
2084
|
-
if "where" in payload and "filters" not in payload:
|
|
2085
|
-
payload["filters"] = payload.pop("where")
|
|
2086
|
-
if "filter_rules" in payload and "filters" not in payload:
|
|
2087
|
-
payload["filters"] = payload.pop("filter_rules")
|
|
2088
|
-
if "filterRules" in payload and "filters" not in payload:
|
|
2089
|
-
payload["filters"] = payload.pop("filterRules")
|
|
2090
1854
|
if "indicator_fields" in payload and "indicator_field_ids" not in payload:
|
|
2091
1855
|
payload["indicator_field_ids"] = payload.pop("indicator_fields")
|
|
2092
1856
|
if "metric_field_ids" in payload and "indicator_field_ids" not in payload:
|
|
2093
1857
|
payload["indicator_field_ids"] = payload.pop("metric_field_ids")
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
"
|
|
2098
|
-
|
|
2099
|
-
"
|
|
2100
|
-
|
|
2101
|
-
"
|
|
2102
|
-
|
|
2103
|
-
"right_metric",
|
|
2104
|
-
"rightMetric",
|
|
2105
|
-
"value_metric",
|
|
2106
|
-
"valueMetric",
|
|
2107
|
-
"target_metric",
|
|
2108
|
-
"targetMetric",
|
|
2109
|
-
)
|
|
2110
|
-
|
|
2111
|
-
def has_metric_value(key: str) -> bool:
|
|
2112
|
-
entry = payload.get(key)
|
|
2113
|
-
return entry is not None and entry != "" and entry != []
|
|
2114
|
-
|
|
2115
|
-
if any(has_metric_value(key) for key in generic_metric_keys) and any(has_metric_value(key) for key in axis_metric_keys):
|
|
2116
|
-
raise ValueError(
|
|
2117
|
-
"chart metric input is ambiguous: use either metric/metrics or axis-specific "
|
|
2118
|
-
"x_metric/y_metric, left_metric/right_metric, value_metric/target_metric, not both"
|
|
2119
|
-
)
|
|
2120
|
-
if "metric" in payload and "metrics" not in payload:
|
|
2121
|
-
payload["metrics"] = [payload["metric"]]
|
|
2122
|
-
for key in ("x_metric", "xMetric", "left_metric", "leftMetric", "value_metric", "valueMetric"):
|
|
2123
|
-
if key in payload:
|
|
2124
|
-
slot_value = payload.get(key)
|
|
2125
|
-
if slot_value is not None:
|
|
2126
|
-
metric_slots.append(slot_value)
|
|
2127
|
-
canonical = {
|
|
2128
|
-
"xMetric": "x_metric",
|
|
2129
|
-
"leftMetric": "left_metric",
|
|
2130
|
-
"valueMetric": "value_metric",
|
|
2131
|
-
}.get(key)
|
|
2132
|
-
if canonical and canonical not in payload:
|
|
2133
|
-
payload[canonical] = payload.pop(key)
|
|
2134
|
-
for key in ("y_metric", "yMetric", "right_metric", "rightMetric", "target_metric", "targetMetric"):
|
|
2135
|
-
if key in payload:
|
|
2136
|
-
slot_value = payload.get(key)
|
|
2137
|
-
if slot_value is not None:
|
|
2138
|
-
metric_slots.append(slot_value)
|
|
2139
|
-
canonical = {
|
|
2140
|
-
"yMetric": "y_metric",
|
|
2141
|
-
"rightMetric": "right_metric",
|
|
2142
|
-
"targetMetric": "target_metric",
|
|
2143
|
-
}.get(key)
|
|
2144
|
-
if canonical and canonical not in payload:
|
|
2145
|
-
payload[canonical] = payload.pop(key)
|
|
2146
|
-
if metric_slots and "metrics" not in payload:
|
|
2147
|
-
payload["metrics"] = metric_slots
|
|
1858
|
+
if "group_by" in payload and "dimension_field_ids" not in payload:
|
|
1859
|
+
payload["dimension_field_ids"] = _normalize_chart_string_list(payload.pop("group_by"))
|
|
1860
|
+
if "groupBy" in payload and "dimension_field_ids" not in payload:
|
|
1861
|
+
payload["dimension_field_ids"] = _normalize_chart_string_list(payload.pop("groupBy"))
|
|
1862
|
+
if "where" in payload and "filters" not in payload:
|
|
1863
|
+
payload["filters"] = payload.pop("where")
|
|
1864
|
+
else:
|
|
1865
|
+
payload.pop("where", None)
|
|
1866
|
+
_apply_semantic_chart_metric_aliases(payload)
|
|
2148
1867
|
raw_type = payload.get("chart_type")
|
|
2149
1868
|
if isinstance(raw_type, str):
|
|
2150
1869
|
normalized = raw_type.strip().lower()
|
|
@@ -2171,31 +1890,8 @@ class ChartUpsertPatch(StrictModel):
|
|
|
2171
1890
|
payload["dimension_field_ids"] = [str(item) for item in payload["dimension_field_ids"] if item is not None and str(item).strip()]
|
|
2172
1891
|
if isinstance(payload.get("indicator_field_ids"), list):
|
|
2173
1892
|
payload["indicator_field_ids"] = [str(item) for item in payload["indicator_field_ids"] if item is not None and str(item).strip()]
|
|
2174
|
-
for key in ("group_by", "rows", "columns"):
|
|
2175
|
-
if isinstance(payload.get(key), str):
|
|
2176
|
-
payload[key] = [payload[key]]
|
|
2177
|
-
if isinstance(payload.get(key), list):
|
|
2178
|
-
payload[key] = [str(item) for item in payload[key] if item is not None and str(item).strip()]
|
|
2179
1893
|
return payload
|
|
2180
1894
|
|
|
2181
|
-
@model_validator(mode="after")
|
|
2182
|
-
def apply_semantic_chart_fields(self) -> "ChartUpsertPatch":
|
|
2183
|
-
if self.group_by and not self.dimension_field_ids:
|
|
2184
|
-
self.dimension_field_ids = list(self.group_by)
|
|
2185
|
-
if self.rows and not self.dimension_field_ids:
|
|
2186
|
-
self.dimension_field_ids = list(self.rows)
|
|
2187
|
-
semantic_metrics: list[ChartMetricPatch] = []
|
|
2188
|
-
if self.metrics:
|
|
2189
|
-
semantic_metrics.extend(self.metrics)
|
|
2190
|
-
for metric in (self.x_metric, self.y_metric, self.left_metric, self.right_metric, self.value_metric, self.target_metric):
|
|
2191
|
-
if metric is not None and metric not in semantic_metrics:
|
|
2192
|
-
semantic_metrics.append(metric)
|
|
2193
|
-
if semantic_metrics and not self.metrics:
|
|
2194
|
-
self.metrics = semantic_metrics
|
|
2195
|
-
if self.metrics and not self.metric:
|
|
2196
|
-
self.metric = self.metrics[0]
|
|
2197
|
-
return self
|
|
2198
|
-
|
|
2199
1895
|
|
|
2200
1896
|
class ChartPartialPatch(StrictModel):
|
|
2201
1897
|
chart_id: str | None = None
|
|
@@ -2260,22 +1956,6 @@ class ChartApplyRequest(StrictModel):
|
|
|
2260
1956
|
return self
|
|
2261
1957
|
|
|
2262
1958
|
|
|
2263
|
-
class PortalInlineChartPatch(ChartUpsertPatch):
|
|
2264
|
-
app_key: str = Field(validation_alias=AliasChoices("app_key", "appKey"))
|
|
2265
|
-
|
|
2266
|
-
@model_validator(mode="before")
|
|
2267
|
-
@classmethod
|
|
2268
|
-
def normalize_inline_aliases(cls, value: Any) -> Any:
|
|
2269
|
-
if not isinstance(value, dict):
|
|
2270
|
-
return value
|
|
2271
|
-
payload = dict(value)
|
|
2272
|
-
if "chart_name" in payload and "name" not in payload:
|
|
2273
|
-
payload["name"] = payload.pop("chart_name")
|
|
2274
|
-
if "chartName" in payload and "name" not in payload:
|
|
2275
|
-
payload["name"] = payload.pop("chartName")
|
|
2276
|
-
return payload
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
1959
|
class PortalComponentPositionPatch(StrictModel):
|
|
2280
1960
|
pc_x: int = Field(default=0, validation_alias=AliasChoices("pc_x", "pcX", "x"))
|
|
2281
1961
|
pc_y: int = Field(default=0, validation_alias=AliasChoices("pc_y", "pcY", "y"))
|
|
@@ -2322,14 +2002,33 @@ class PortalComponentPositionPatch(StrictModel):
|
|
|
2322
2002
|
class PortalChartRefPatch(StrictModel):
|
|
2323
2003
|
app_key: str | None = None
|
|
2324
2004
|
chart_id: str | None = None
|
|
2005
|
+
chart_key: str | None = None
|
|
2325
2006
|
chart_name: str | None = None
|
|
2326
2007
|
|
|
2008
|
+
@model_validator(mode="before")
|
|
2009
|
+
@classmethod
|
|
2010
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
2011
|
+
if not isinstance(value, dict):
|
|
2012
|
+
return value
|
|
2013
|
+
payload = dict(value)
|
|
2014
|
+
if "chartKey" in payload and "chart_key" not in payload:
|
|
2015
|
+
payload["chart_key"] = payload.pop("chartKey")
|
|
2016
|
+
if "chartId" in payload and "chart_id" not in payload:
|
|
2017
|
+
payload["chart_id"] = payload.pop("chartId")
|
|
2018
|
+
if "biChartId" in payload and "chart_id" not in payload:
|
|
2019
|
+
payload["chart_id"] = payload.pop("biChartId")
|
|
2020
|
+
if "chartName" in payload and "chart_name" not in payload:
|
|
2021
|
+
payload["chart_name"] = payload.pop("chartName")
|
|
2022
|
+
if "appKey" in payload and "app_key" not in payload:
|
|
2023
|
+
payload["app_key"] = payload.pop("appKey")
|
|
2024
|
+
return payload
|
|
2025
|
+
|
|
2327
2026
|
@model_validator(mode="after")
|
|
2328
2027
|
def validate_target(self) -> "PortalChartRefPatch":
|
|
2329
|
-
if not (self.chart_id or self.chart_name):
|
|
2330
|
-
raise ValueError("chart_ref requires chart_id or chart_name")
|
|
2331
|
-
if self.chart_name and not self.
|
|
2332
|
-
raise ValueError("chart_ref
|
|
2028
|
+
if not (self.chart_id or self.chart_key or self.chart_name):
|
|
2029
|
+
raise ValueError("chart_ref requires chart_id, chart_key, or chart_name")
|
|
2030
|
+
if self.chart_name and not self.app_key and not (self.chart_id or self.chart_key):
|
|
2031
|
+
raise ValueError("chart_ref with chart_name requires app_key unless chart_id/chart_key is also provided")
|
|
2333
2032
|
return self
|
|
2334
2033
|
|
|
2335
2034
|
|
|
@@ -2355,12 +2054,12 @@ class PortalViewRefPatch(StrictModel):
|
|
|
2355
2054
|
class PortalSectionPatch(StrictModel):
|
|
2356
2055
|
title: str
|
|
2357
2056
|
source_type: str = Field(validation_alias=AliasChoices("source_type", "sourceType"))
|
|
2358
|
-
role: str | None =
|
|
2057
|
+
role: str | None = None
|
|
2359
2058
|
position: PortalComponentPositionPatch | None = None
|
|
2360
2059
|
dash_style_config: dict[str, Any] | None = Field(default=None, validation_alias=AliasChoices("dash_style_config", "dashStyleConfigBO"))
|
|
2361
2060
|
config: dict[str, Any] = Field(default_factory=dict)
|
|
2061
|
+
chart: dict[str, Any] | None = None
|
|
2362
2062
|
chart_ref: PortalChartRefPatch | None = None
|
|
2363
|
-
chart: PortalInlineChartPatch | None = Field(default=None, validation_alias=AliasChoices("chart", "inline_chart", "inlineChart", "create_chart", "createChart"))
|
|
2364
2063
|
view_ref: PortalViewRefPatch | None = None
|
|
2365
2064
|
text: str | None = None
|
|
2366
2065
|
url: str | None = None
|
|
@@ -2374,19 +2073,8 @@ class PortalSectionPatch(StrictModel):
|
|
|
2374
2073
|
raw_type = payload.get("source_type", payload.get("sourceType"))
|
|
2375
2074
|
if isinstance(raw_type, str):
|
|
2376
2075
|
payload["source_type"] = raw_type.strip().lower()
|
|
2377
|
-
raw_role = payload.get("role", payload.get("zone", payload.get("section_role", payload.get("sectionRole"))))
|
|
2378
|
-
if isinstance(raw_role, str):
|
|
2379
|
-
payload["role"] = raw_role.strip().lower()
|
|
2380
2076
|
if "chartRef" in payload and "chart_ref" not in payload:
|
|
2381
2077
|
payload["chart_ref"] = payload.pop("chartRef")
|
|
2382
|
-
if "inlineChart" in payload and "chart" not in payload:
|
|
2383
|
-
payload["chart"] = payload.pop("inlineChart")
|
|
2384
|
-
if "inline_chart" in payload and "chart" not in payload:
|
|
2385
|
-
payload["chart"] = payload.pop("inline_chart")
|
|
2386
|
-
if "createChart" in payload and "chart" not in payload:
|
|
2387
|
-
payload["chart"] = payload.pop("createChart")
|
|
2388
|
-
if "create_chart" in payload and "chart" not in payload:
|
|
2389
|
-
payload["chart"] = payload.pop("create_chart")
|
|
2390
2078
|
if "viewRef" in payload and "view_ref" not in payload:
|
|
2391
2079
|
payload["view_ref"] = payload.pop("viewRef")
|
|
2392
2080
|
if "dashStyleConfigBO" in payload and "dash_style_config" not in payload:
|
|
@@ -2398,8 +2086,18 @@ class PortalSectionPatch(StrictModel):
|
|
|
2398
2086
|
supported = {"chart", "view", "grid", "filter", "text", "link"}
|
|
2399
2087
|
if self.source_type not in supported:
|
|
2400
2088
|
raise ValueError(f"unsupported portal source_type '{self.source_type}'")
|
|
2401
|
-
if self.
|
|
2402
|
-
|
|
2089
|
+
if self.role is not None:
|
|
2090
|
+
normalized_role = str(self.role or "").strip().lower()
|
|
2091
|
+
if normalized_role not in {"metric"}:
|
|
2092
|
+
raise ValueError(f"unsupported portal section role '{self.role}'")
|
|
2093
|
+
self.role = normalized_role
|
|
2094
|
+
if self.source_type != "chart":
|
|
2095
|
+
raise ValueError("portal section role is only supported for chart sections")
|
|
2096
|
+
if self.source_type == "chart":
|
|
2097
|
+
if self.chart is not None and self.chart_ref is not None:
|
|
2098
|
+
raise ValueError("chart section accepts either chart or chart_ref, not both")
|
|
2099
|
+
if self.chart is None and self.chart_ref is None:
|
|
2100
|
+
raise ValueError("chart section requires chart or chart_ref")
|
|
2403
2101
|
if self.source_type == "view" and self.view_ref is None:
|
|
2404
2102
|
raise ValueError("view section requires view_ref")
|
|
2405
2103
|
if self.source_type == "text" and self.text is None:
|
|
@@ -2536,7 +2234,6 @@ AppReadSummaryResponse = AppGetResponse
|
|
|
2536
2234
|
AppFieldsReadResponse = AppGetFieldsResponse
|
|
2537
2235
|
AppLayoutReadResponse = AppGetLayoutResponse
|
|
2538
2236
|
AppViewsReadResponse = AppGetViewsResponse
|
|
2539
|
-
AppFlowReadResponse = AppGetFlowResponse
|
|
2540
2237
|
AppChartsReadResponse = AppGetChartsResponse
|
|
2541
2238
|
|
|
2542
2239
|
|
|
@@ -2590,9 +2287,6 @@ class ChartGetResponse(StrictModel):
|
|
|
2590
2287
|
chart_id: str
|
|
2591
2288
|
base: dict[str, Any] = Field(default_factory=dict)
|
|
2592
2289
|
visibility: dict[str, Any] = Field(default_factory=dict)
|
|
2593
|
-
filters: list[list[dict[str, Any]]] = Field(default_factory=list)
|
|
2594
|
-
group_by: list[str] = Field(default_factory=list)
|
|
2595
|
-
metrics: list[dict[str, Any]] = Field(default_factory=list)
|
|
2596
2290
|
config: dict[str, Any] = Field(default_factory=dict)
|
|
2597
2291
|
|
|
2598
2292
|
|
|
@@ -2603,6 +2297,7 @@ class SchemaPlanRequest(StrictModel):
|
|
|
2603
2297
|
icon: str | None = None
|
|
2604
2298
|
color: str | None = None
|
|
2605
2299
|
visibility: VisibilityPatch | None = None
|
|
2300
|
+
create_if_missing: bool = False
|
|
2606
2301
|
add_fields: list[FieldPatch] = Field(default_factory=list)
|
|
2607
2302
|
update_fields: list[FieldUpdatePatch] = Field(default_factory=list)
|
|
2608
2303
|
remove_fields: list[FieldRemovePatch] = Field(default_factory=list)
|
|
@@ -2625,40 +2320,6 @@ class LayoutPlanRequest(StrictModel):
|
|
|
2625
2320
|
return payload
|
|
2626
2321
|
|
|
2627
2322
|
|
|
2628
|
-
class FlowPlanRequest(StrictModel):
|
|
2629
|
-
app_key: str
|
|
2630
|
-
mode: str = "replace"
|
|
2631
|
-
nodes: list[FlowNodePatch] = Field(default_factory=list)
|
|
2632
|
-
transitions: list[FlowTransitionPatch] = Field(default_factory=list)
|
|
2633
|
-
preset: FlowPreset | None = None
|
|
2634
|
-
|
|
2635
|
-
@model_validator(mode="before")
|
|
2636
|
-
@classmethod
|
|
2637
|
-
def normalize_mode_alias(cls, value: Any) -> Any:
|
|
2638
|
-
if not isinstance(value, dict):
|
|
2639
|
-
return value
|
|
2640
|
-
payload = dict(value)
|
|
2641
|
-
if str(payload.get("mode") or "").strip().lower() == "overwrite":
|
|
2642
|
-
payload["mode"] = "replace"
|
|
2643
|
-
raw_preset = payload.get("preset")
|
|
2644
|
-
if raw_preset is None and isinstance(payload.get("base_preset"), str):
|
|
2645
|
-
raw_preset = payload["base_preset"]
|
|
2646
|
-
payload["preset"] = raw_preset
|
|
2647
|
-
if isinstance(raw_preset, str):
|
|
2648
|
-
normalized_preset = raw_preset.strip().lower()
|
|
2649
|
-
preset_aliases = {
|
|
2650
|
-
"default_approval": FlowPreset.basic_approval.value,
|
|
2651
|
-
"approval": FlowPreset.basic_approval.value,
|
|
2652
|
-
"basic approval": FlowPreset.basic_approval.value,
|
|
2653
|
-
"default_fill_then_approve": FlowPreset.basic_fill_then_approve.value,
|
|
2654
|
-
"default-fill-then-approve": FlowPreset.basic_fill_then_approve.value,
|
|
2655
|
-
"fill_then_approve": FlowPreset.basic_fill_then_approve.value,
|
|
2656
|
-
}
|
|
2657
|
-
if normalized_preset in preset_aliases:
|
|
2658
|
-
payload["preset"] = preset_aliases[normalized_preset]
|
|
2659
|
-
return payload
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
2323
|
class ViewsPlanRequest(StrictModel):
|
|
2663
2324
|
app_key: str
|
|
2664
2325
|
upsert_views: list[ViewUpsertPatch] = Field(default_factory=list)
|
|
@@ -2686,18 +2347,8 @@ def _normalize_field_payload(value: Any) -> Any:
|
|
|
2686
2347
|
if not isinstance(value, dict):
|
|
2687
2348
|
return value
|
|
2688
2349
|
payload = dict(value)
|
|
2689
|
-
if "data_title" in payload and "as_data_title" not in payload and "asDataTitle" not in payload:
|
|
2690
|
-
payload["as_data_title"] = payload.pop("data_title")
|
|
2691
|
-
if "dataTitle" in payload and "as_data_title" not in payload and "asDataTitle" not in payload:
|
|
2692
|
-
payload["as_data_title"] = payload.pop("dataTitle")
|
|
2693
|
-
if "data_cover" in payload and "as_data_cover" not in payload and "asDataCover" not in payload:
|
|
2694
|
-
payload["as_data_cover"] = payload.pop("data_cover")
|
|
2695
|
-
if "dataCover" in payload and "as_data_cover" not in payload and "asDataCover" not in payload:
|
|
2696
|
-
payload["as_data_cover"] = payload.pop("dataCover")
|
|
2697
2350
|
if "fields" in payload and "subfields" not in payload:
|
|
2698
2351
|
payload["subfields"] = payload.pop("fields")
|
|
2699
|
-
if "options" in payload:
|
|
2700
|
-
payload["options"] = _normalize_field_options(payload.get("options"))
|
|
2701
2352
|
raw_type = payload.get("type")
|
|
2702
2353
|
if isinstance(raw_type, int):
|
|
2703
2354
|
normalized_from_id = FIELD_TYPE_ID_ALIASES.get(raw_type)
|
|
@@ -2730,34 +2381,6 @@ def _normalize_field_payload(value: Any) -> Any:
|
|
|
2730
2381
|
return payload
|
|
2731
2382
|
|
|
2732
2383
|
|
|
2733
|
-
def _normalize_field_options(value: Any) -> Any:
|
|
2734
|
-
if not isinstance(value, list):
|
|
2735
|
-
return value
|
|
2736
|
-
normalized: list[str] = []
|
|
2737
|
-
for item in value:
|
|
2738
|
-
if isinstance(item, str):
|
|
2739
|
-
normalized.append(item)
|
|
2740
|
-
continue
|
|
2741
|
-
if isinstance(item, (int, float, bool)):
|
|
2742
|
-
normalized.append(str(item))
|
|
2743
|
-
continue
|
|
2744
|
-
if isinstance(item, dict):
|
|
2745
|
-
label = (
|
|
2746
|
-
item.get("label")
|
|
2747
|
-
or item.get("name")
|
|
2748
|
-
or item.get("title")
|
|
2749
|
-
or item.get("value")
|
|
2750
|
-
or item.get("text")
|
|
2751
|
-
or item.get("optValue")
|
|
2752
|
-
or item.get("optName")
|
|
2753
|
-
)
|
|
2754
|
-
if label is not None:
|
|
2755
|
-
normalized.append(str(label))
|
|
2756
|
-
continue
|
|
2757
|
-
normalized.append(str(item))
|
|
2758
|
-
return normalized
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
2384
|
def _slugify_title(title: str) -> str:
|
|
2762
2385
|
normalized = "".join(ch.lower() if ch.isalnum() else "_" for ch in str(title or ""))
|
|
2763
2386
|
collapsed = "_".join(part for part in normalized.split("_") if part)
|