@josephyan/qingflow-app-user-mcp 1.1.15 → 1.1.17
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
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @josephyan/qingflow-app-user-mcp@1.1.
|
|
6
|
+
npm install @josephyan/qingflow-app-user-mcp@1.1.17
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @josephyan/qingflow-app-user-mcp@1.1.
|
|
12
|
+
npx -y -p @josephyan/qingflow-app-user-mcp@1.1.17 qingflow-app-user-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -1105,6 +1105,91 @@ class ViewAssociatedResourcesPatch(StrictModel):
|
|
|
1105
1105
|
)
|
|
1106
1106
|
|
|
1107
1107
|
|
|
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
|
+
|
|
1108
1193
|
class ViewUpsertPatch(StrictModel):
|
|
1109
1194
|
name: str
|
|
1110
1195
|
view_key: str | None = Field(default=None, validation_alias=AliasChoices("view_key", "viewKey"))
|
|
@@ -1116,6 +1201,8 @@ class ViewUpsertPatch(StrictModel):
|
|
|
1116
1201
|
end_field: str | None = Field(default=None, validation_alias=AliasChoices("end_field", "endField"))
|
|
1117
1202
|
title_field: str | None = Field(default=None, validation_alias=AliasChoices("title_field", "titleField"))
|
|
1118
1203
|
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"))
|
|
1119
1206
|
visibility: VisibilityPatch | None = None
|
|
1120
1207
|
query_conditions: ViewQueryConditionsPatch | None = Field(default=None, validation_alias=AliasChoices("query_conditions", "queryConditions", "query_condition", "queryCondition"))
|
|
1121
1208
|
associated_resources: ViewAssociatedResourcesPatch | None = Field(
|
|
@@ -1165,6 +1252,12 @@ class ViewUpsertPatch(StrictModel):
|
|
|
1165
1252
|
payload["associated_resources"] = payload.pop("associatedReports")
|
|
1166
1253
|
if "asosChartConfig" in payload and "associated_resources" not in payload:
|
|
1167
1254
|
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")
|
|
1168
1261
|
raw_type = payload.get("type")
|
|
1169
1262
|
if isinstance(raw_type, str):
|
|
1170
1263
|
normalized = raw_type.strip().lower()
|
|
@@ -1186,6 +1279,10 @@ class ViewUpsertPatch(StrictModel):
|
|
|
1186
1279
|
raise ValueError("board view requires group_by")
|
|
1187
1280
|
if self.type == PublicViewType.gantt and not (self.start_field and self.end_field):
|
|
1188
1281
|
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
|
|
1189
1286
|
return self
|
|
1190
1287
|
|
|
1191
1288
|
|
|
@@ -1211,6 +1308,76 @@ class ViewPartialPatch(StrictModel):
|
|
|
1211
1308
|
return self
|
|
1212
1309
|
|
|
1213
1310
|
|
|
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
|
+
|
|
1214
1381
|
class CustomButtonJudgeValuePatch(StrictModel):
|
|
1215
1382
|
id: int | str | None = None
|
|
1216
1383
|
value: Any | None = None
|
|
@@ -90,6 +90,7 @@ from .models import (
|
|
|
90
90
|
SchemaPlanRequest,
|
|
91
91
|
VisibilityPatch,
|
|
92
92
|
ViewAssociatedResourcesPatch,
|
|
93
|
+
ViewActionButtonPatch,
|
|
93
94
|
ViewButtonBindingPatch,
|
|
94
95
|
ViewPartialPatch,
|
|
95
96
|
ViewUpsertPatch,
|
|
@@ -99,6 +100,9 @@ from .models import (
|
|
|
99
100
|
ViewsPreset,
|
|
100
101
|
FlowPreset,
|
|
101
102
|
FlowNodePermissionsPatch,
|
|
103
|
+
public_view_action_button_payload,
|
|
104
|
+
public_view_partial_payload,
|
|
105
|
+
public_view_upsert_payload,
|
|
102
106
|
)
|
|
103
107
|
|
|
104
108
|
BUILDER_PORTAL_LIST_DETAIL_VERIFY_LIMIT = 50
|
|
@@ -8264,7 +8268,8 @@ class AiBuilderFacade:
|
|
|
8264
8268
|
for field in current_fields
|
|
8265
8269
|
if isinstance(field, dict) and str(field.get("name") or "")
|
|
8266
8270
|
}
|
|
8267
|
-
upsert_views = [view
|
|
8271
|
+
upsert_views = [public_view_upsert_payload(view) for view in request.upsert_views]
|
|
8272
|
+
patch_views = [public_view_partial_payload(patch) for patch in request.patch_views]
|
|
8268
8273
|
if request.preset is not None:
|
|
8269
8274
|
upsert_views = _build_views_preset(request.preset, list(field_names))
|
|
8270
8275
|
blocking_issues: list[dict[str, Any]] = []
|
|
@@ -8326,6 +8331,7 @@ class AiBuilderFacade:
|
|
|
8326
8331
|
normalized_args = {
|
|
8327
8332
|
"app_key": request.app_key,
|
|
8328
8333
|
"upsert_views": upsert_views,
|
|
8334
|
+
"patch_views": patch_views,
|
|
8329
8335
|
"remove_views": list(request.remove_views),
|
|
8330
8336
|
}
|
|
8331
8337
|
return {
|
|
@@ -8344,6 +8350,7 @@ class AiBuilderFacade:
|
|
|
8344
8350
|
"request_id": None,
|
|
8345
8351
|
"views_diff_preview": {
|
|
8346
8352
|
"upsert": [view.get("name") for view in upsert_views],
|
|
8353
|
+
"patch": [view.get("view_key") or view.get("name") for view in patch_views],
|
|
8347
8354
|
"remove": list(request.remove_views),
|
|
8348
8355
|
},
|
|
8349
8356
|
"blocking_issues": blocking_issues,
|
|
@@ -9980,6 +9987,406 @@ class AiBuilderFacade:
|
|
|
9980
9987
|
}
|
|
9981
9988
|
return finalize(self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response))
|
|
9982
9989
|
|
|
9990
|
+
def _extract_view_action_button_patch_intents(
|
|
9991
|
+
self,
|
|
9992
|
+
*,
|
|
9993
|
+
existing_by_key: dict[str, dict[str, Any]],
|
|
9994
|
+
existing_by_name: dict[str, list[dict[str, Any]]],
|
|
9995
|
+
patch_views: list[ViewPartialPatch],
|
|
9996
|
+
) -> tuple[list[ViewPartialPatch], list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]:
|
|
9997
|
+
action_keys = {"action_buttons", "actionButtons"}
|
|
9998
|
+
mode_keys = {"action_buttons_mode", "actionButtonsMode", "button_mode", "buttonMode"}
|
|
9999
|
+
sanitized: list[ViewPartialPatch] = []
|
|
10000
|
+
intents: list[dict[str, Any]] = []
|
|
10001
|
+
issues: list[dict[str, Any]] = []
|
|
10002
|
+
results: list[dict[str, Any]] = []
|
|
10003
|
+
sentinel = object()
|
|
10004
|
+
for index, patch in enumerate(patch_views):
|
|
10005
|
+
raw_set = dict(patch.set or {})
|
|
10006
|
+
action_buttons_raw: Any = sentinel
|
|
10007
|
+
for key in action_keys:
|
|
10008
|
+
if key in raw_set:
|
|
10009
|
+
action_buttons_raw = raw_set.pop(key)
|
|
10010
|
+
break
|
|
10011
|
+
mode_raw: Any = sentinel
|
|
10012
|
+
for key in mode_keys:
|
|
10013
|
+
if key in raw_set:
|
|
10014
|
+
mode_raw = raw_set.pop(key)
|
|
10015
|
+
break
|
|
10016
|
+
if action_buttons_raw is not sentinel:
|
|
10017
|
+
requires_view_write = bool(raw_set or patch.unset)
|
|
10018
|
+
if not isinstance(action_buttons_raw, list):
|
|
10019
|
+
issue = {
|
|
10020
|
+
"error_code": "INVALID_VIEW_ACTION_BUTTONS",
|
|
10021
|
+
"reason_path": f"patch_views[{index}].set.action_buttons",
|
|
10022
|
+
"message": "action_buttons must be a list",
|
|
10023
|
+
}
|
|
10024
|
+
issues.append(issue)
|
|
10025
|
+
results.append({"index": index, "status": "failed", **issue})
|
|
10026
|
+
else:
|
|
10027
|
+
mode = str(mode_raw if mode_raw is not sentinel else "merge").strip().lower() or "merge"
|
|
10028
|
+
if mode not in {"merge", "replace"}:
|
|
10029
|
+
issue = {
|
|
10030
|
+
"error_code": "INVALID_VIEW_ACTION_BUTTONS_MODE",
|
|
10031
|
+
"reason_path": f"patch_views[{index}].set.action_buttons_mode",
|
|
10032
|
+
"message": "action_buttons_mode must be merge or replace",
|
|
10033
|
+
}
|
|
10034
|
+
issues.append(issue)
|
|
10035
|
+
results.append({"index": index, "status": "failed", **issue})
|
|
10036
|
+
else:
|
|
10037
|
+
try:
|
|
10038
|
+
action_buttons = [
|
|
10039
|
+
ViewActionButtonPatch.model_validate(item)
|
|
10040
|
+
for item in action_buttons_raw
|
|
10041
|
+
]
|
|
10042
|
+
except Exception as error:
|
|
10043
|
+
issue = {
|
|
10044
|
+
"error_code": "INVALID_VIEW_ACTION_BUTTONS",
|
|
10045
|
+
"reason_path": f"patch_views[{index}].set.action_buttons",
|
|
10046
|
+
"message": str(error),
|
|
10047
|
+
}
|
|
10048
|
+
issues.append(issue)
|
|
10049
|
+
results.append({"index": index, "status": "failed", **issue})
|
|
10050
|
+
else:
|
|
10051
|
+
view_key = str(patch.view_key or "").strip()
|
|
10052
|
+
view_name = str(patch.name or "").strip()
|
|
10053
|
+
matched_view: dict[str, Any] | None = None
|
|
10054
|
+
if view_key:
|
|
10055
|
+
matched_view = existing_by_key.get(view_key)
|
|
10056
|
+
if matched_view is None:
|
|
10057
|
+
issue = {
|
|
10058
|
+
"error_code": "UNKNOWN_VIEW",
|
|
10059
|
+
"reason_path": f"patch_views[{index}].view_key",
|
|
10060
|
+
"view_key": view_key,
|
|
10061
|
+
"message": "view_key does not exist on this app",
|
|
10062
|
+
}
|
|
10063
|
+
issues.append(issue)
|
|
10064
|
+
results.append({"index": index, "status": "failed", **issue})
|
|
10065
|
+
else:
|
|
10066
|
+
matches = existing_by_name.get(view_name, [])
|
|
10067
|
+
if len(matches) != 1:
|
|
10068
|
+
issue = {
|
|
10069
|
+
"error_code": "AMBIGUOUS_VIEW" if matches else "UNKNOWN_VIEW",
|
|
10070
|
+
"reason_path": f"patch_views[{index}].name",
|
|
10071
|
+
"view_name": view_name,
|
|
10072
|
+
"matches": [
|
|
10073
|
+
{"name": _extract_view_name(view), "view_key": _extract_view_key(view), "type": _normalize_view_type_name(view.get("viewgraphType") or view.get("type"))}
|
|
10074
|
+
for view in matches
|
|
10075
|
+
],
|
|
10076
|
+
"message": "patch_views[].set.action_buttons must target a single existing view; use view_key when names are duplicated",
|
|
10077
|
+
}
|
|
10078
|
+
issues.append(issue)
|
|
10079
|
+
results.append({"index": index, "status": "failed", **issue})
|
|
10080
|
+
else:
|
|
10081
|
+
matched_view = matches[0]
|
|
10082
|
+
view_key = _extract_view_key(matched_view)
|
|
10083
|
+
if matched_view is not None or view_key:
|
|
10084
|
+
view_name = _extract_view_name(matched_view or {}) or view_name or view_key
|
|
10085
|
+
intents.append(
|
|
10086
|
+
{
|
|
10087
|
+
"source": "patch_views",
|
|
10088
|
+
"index": index,
|
|
10089
|
+
"view_key": view_key,
|
|
10090
|
+
"view_name": view_name,
|
|
10091
|
+
"action_buttons": action_buttons,
|
|
10092
|
+
"mode": mode,
|
|
10093
|
+
"requires_view_write": requires_view_write,
|
|
10094
|
+
}
|
|
10095
|
+
)
|
|
10096
|
+
results.append(
|
|
10097
|
+
{
|
|
10098
|
+
"index": index,
|
|
10099
|
+
"status": "action_buttons_extracted",
|
|
10100
|
+
"view_key": view_key,
|
|
10101
|
+
"view_name": view_name,
|
|
10102
|
+
"action_buttons_count": len(action_buttons),
|
|
10103
|
+
"action_buttons_mode": mode,
|
|
10104
|
+
}
|
|
10105
|
+
)
|
|
10106
|
+
elif mode_raw is not sentinel:
|
|
10107
|
+
issue = {
|
|
10108
|
+
"error_code": "ACTION_BUTTONS_MODE_WITHOUT_ACTION_BUTTONS",
|
|
10109
|
+
"reason_path": f"patch_views[{index}].set.action_buttons_mode",
|
|
10110
|
+
"message": "action_buttons_mode is only valid together with action_buttons",
|
|
10111
|
+
}
|
|
10112
|
+
issues.append(issue)
|
|
10113
|
+
results.append({"index": index, "status": "failed", **issue})
|
|
10114
|
+
if raw_set or patch.unset:
|
|
10115
|
+
sanitized.append(
|
|
10116
|
+
ViewPartialPatch.model_validate(
|
|
10117
|
+
{
|
|
10118
|
+
"view_key": patch.view_key,
|
|
10119
|
+
"name": patch.name,
|
|
10120
|
+
"set": raw_set,
|
|
10121
|
+
"unset": list(patch.unset or []),
|
|
10122
|
+
}
|
|
10123
|
+
)
|
|
10124
|
+
)
|
|
10125
|
+
return sanitized, intents, issues, results
|
|
10126
|
+
|
|
10127
|
+
def _view_action_button_to_custom_button_payload(self, *, button: ViewActionButtonPatch, client_key: str | None) -> dict[str, Any]:
|
|
10128
|
+
payload: dict[str, Any] = {
|
|
10129
|
+
"button_text": button.text,
|
|
10130
|
+
"trigger_action": button.action.value,
|
|
10131
|
+
"style_preset": button.style_preset or "primary_blue",
|
|
10132
|
+
}
|
|
10133
|
+
if button.button_id is not None:
|
|
10134
|
+
payload["button_id"] = button.button_id
|
|
10135
|
+
if client_key:
|
|
10136
|
+
payload["client_key"] = client_key
|
|
10137
|
+
if button.background_color:
|
|
10138
|
+
payload["background_color"] = button.background_color
|
|
10139
|
+
if button.text_color:
|
|
10140
|
+
payload["text_color"] = button.text_color
|
|
10141
|
+
if button.button_icon:
|
|
10142
|
+
payload["button_icon"] = button.button_icon
|
|
10143
|
+
if button.action == PublicButtonTriggerAction.link:
|
|
10144
|
+
payload["trigger_link_url"] = button.url
|
|
10145
|
+
elif button.action == PublicButtonTriggerAction.add_data:
|
|
10146
|
+
add_data_config: dict[str, Any] = {
|
|
10147
|
+
"related_app_key": button.target_app_key,
|
|
10148
|
+
"related_app_name": button.target_app_name,
|
|
10149
|
+
"field_mappings": deepcopy(button.field_mappings),
|
|
10150
|
+
"default_values": deepcopy(button.default_values),
|
|
10151
|
+
}
|
|
10152
|
+
payload["trigger_add_data_config"] = _compact_dict(add_data_config)
|
|
10153
|
+
elif button.action == PublicButtonTriggerAction.qrobot:
|
|
10154
|
+
payload["external_qrobot_config"] = deepcopy(button.external_qrobot_config)
|
|
10155
|
+
elif button.action == PublicButtonTriggerAction.wings:
|
|
10156
|
+
payload["trigger_wings_config"] = deepcopy(button.trigger_wings_config)
|
|
10157
|
+
return payload
|
|
10158
|
+
|
|
10159
|
+
def _view_action_button_binding_payload(self, *, button: ViewActionButtonPatch, button_ref: Any) -> dict[str, Any]:
|
|
10160
|
+
payload: dict[str, Any] = {
|
|
10161
|
+
"button_ref": button_ref,
|
|
10162
|
+
"placement": button.placement.value,
|
|
10163
|
+
"primary": button.primary,
|
|
10164
|
+
"button_limit": [
|
|
10165
|
+
[rule.model_dump(mode="json") for rule in group]
|
|
10166
|
+
for group in button.visible_when
|
|
10167
|
+
],
|
|
10168
|
+
"button_formula_type": button.button_formula_type,
|
|
10169
|
+
"print_tpls": deepcopy(button.print_tpls),
|
|
10170
|
+
}
|
|
10171
|
+
if button.button_formula:
|
|
10172
|
+
payload["button_formula"] = button.button_formula
|
|
10173
|
+
return payload
|
|
10174
|
+
|
|
10175
|
+
def _compile_view_action_buttons_request(
|
|
10176
|
+
self,
|
|
10177
|
+
*,
|
|
10178
|
+
app_key: str,
|
|
10179
|
+
intents: list[dict[str, Any]],
|
|
10180
|
+
) -> tuple[CustomButtonsApplyRequest | None, list[dict[str, Any]]]:
|
|
10181
|
+
upsert_buttons: list[CustomButtonUpsertPatch] = []
|
|
10182
|
+
view_configs: list[CustomButtonViewConfigPatch] = []
|
|
10183
|
+
issues: list[dict[str, Any]] = []
|
|
10184
|
+
seen_button_payloads: dict[str, dict[str, Any]] = {}
|
|
10185
|
+
seen_button_refs: dict[str, Any] = {}
|
|
10186
|
+
used_client_keys: set[str] = set()
|
|
10187
|
+
for intent_index, intent in enumerate(intents):
|
|
10188
|
+
view_key = str(intent.get("view_key") or "").strip()
|
|
10189
|
+
mode = str(intent.get("mode") or "merge").strip().lower() or "merge"
|
|
10190
|
+
action_buttons = [
|
|
10191
|
+
item for item in (intent.get("action_buttons") or [])
|
|
10192
|
+
if isinstance(item, ViewActionButtonPatch)
|
|
10193
|
+
]
|
|
10194
|
+
bindings: list[dict[str, Any]] = []
|
|
10195
|
+
for button_index, button in enumerate(action_buttons):
|
|
10196
|
+
identity = f"id:{button.button_id}" if button.button_id is not None else f"text:{str(button.text or '').strip()}"
|
|
10197
|
+
explicit_client_key = str(button.client_key or "").strip() or None
|
|
10198
|
+
generated_client_key = explicit_client_key or f"view_action_{_slugify(str(button.text or ''), default='button')}"
|
|
10199
|
+
client_key = generated_client_key
|
|
10200
|
+
payload = self._view_action_button_to_custom_button_payload(
|
|
10201
|
+
button=button,
|
|
10202
|
+
client_key=None if button.button_id is not None else client_key,
|
|
10203
|
+
)
|
|
10204
|
+
compare_payload = deepcopy(payload)
|
|
10205
|
+
compare_payload.pop("client_key", None)
|
|
10206
|
+
existing_payload = seen_button_payloads.get(identity)
|
|
10207
|
+
if existing_payload is not None:
|
|
10208
|
+
if existing_payload != compare_payload:
|
|
10209
|
+
issues.append(
|
|
10210
|
+
{
|
|
10211
|
+
"error_code": "DUPLICATE_ACTION_BUTTON_CONFLICT",
|
|
10212
|
+
"reason_path": f"{intent.get('source') or 'views'}[{intent.get('index', intent_index)}].action_buttons[{button_index}]",
|
|
10213
|
+
"button_text": button.text,
|
|
10214
|
+
"message": "the same app cannot declare two action_buttons with the same button text or id but different button body config in one app_views_apply call",
|
|
10215
|
+
}
|
|
10216
|
+
)
|
|
10217
|
+
continue
|
|
10218
|
+
button_ref = seen_button_refs[identity]
|
|
10219
|
+
else:
|
|
10220
|
+
seen_button_payloads[identity] = compare_payload
|
|
10221
|
+
if button.button_id is not None:
|
|
10222
|
+
button_ref = button.button_id
|
|
10223
|
+
else:
|
|
10224
|
+
unique_client_key = client_key
|
|
10225
|
+
if unique_client_key in used_client_keys:
|
|
10226
|
+
unique_client_key = f"{unique_client_key}_{len(used_client_keys) + 1}"
|
|
10227
|
+
payload["client_key"] = unique_client_key
|
|
10228
|
+
used_client_keys.add(unique_client_key)
|
|
10229
|
+
button_ref = unique_client_key
|
|
10230
|
+
payload["client_key"] = unique_client_key
|
|
10231
|
+
try:
|
|
10232
|
+
upsert_buttons.append(CustomButtonUpsertPatch.model_validate(payload))
|
|
10233
|
+
except Exception as error:
|
|
10234
|
+
issues.append(
|
|
10235
|
+
{
|
|
10236
|
+
"error_code": "INVALID_VIEW_ACTION_BUTTON",
|
|
10237
|
+
"reason_path": f"{intent.get('source') or 'views'}[{intent.get('index', intent_index)}].action_buttons[{button_index}]",
|
|
10238
|
+
"message": str(error),
|
|
10239
|
+
}
|
|
10240
|
+
)
|
|
10241
|
+
continue
|
|
10242
|
+
seen_button_refs[identity] = button_ref
|
|
10243
|
+
bindings.append(self._view_action_button_binding_payload(button=button, button_ref=button_ref))
|
|
10244
|
+
if mode == "replace" or bindings:
|
|
10245
|
+
try:
|
|
10246
|
+
view_configs.append(CustomButtonViewConfigPatch.model_validate({"view_key": view_key, "mode": mode, "buttons": bindings}))
|
|
10247
|
+
except Exception as error:
|
|
10248
|
+
issues.append(
|
|
10249
|
+
{
|
|
10250
|
+
"error_code": "INVALID_VIEW_ACTION_BUTTON_BINDING",
|
|
10251
|
+
"reason_path": f"{intent.get('source') or 'views'}[{intent.get('index', intent_index)}].action_buttons",
|
|
10252
|
+
"view_key": view_key,
|
|
10253
|
+
"message": str(error),
|
|
10254
|
+
}
|
|
10255
|
+
)
|
|
10256
|
+
if issues:
|
|
10257
|
+
return None, issues
|
|
10258
|
+
if not upsert_buttons and not view_configs:
|
|
10259
|
+
return None, []
|
|
10260
|
+
try:
|
|
10261
|
+
return CustomButtonsApplyRequest.model_validate(
|
|
10262
|
+
{
|
|
10263
|
+
"app_key": app_key,
|
|
10264
|
+
"upsert_buttons": [item.model_dump(mode="json") for item in upsert_buttons],
|
|
10265
|
+
"view_configs": [item.model_dump(mode="json") for item in view_configs],
|
|
10266
|
+
}
|
|
10267
|
+
), []
|
|
10268
|
+
except Exception as error:
|
|
10269
|
+
return None, [{"error_code": "INVALID_VIEW_ACTION_BUTTONS", "message": str(error)}]
|
|
10270
|
+
|
|
10271
|
+
def _view_action_buttons_retry_payload(self, *, profile: str, app_key: str, intents: list[dict[str, Any]]) -> dict[str, Any]:
|
|
10272
|
+
return {
|
|
10273
|
+
"tool_name": "app_views_apply",
|
|
10274
|
+
"arguments": {
|
|
10275
|
+
"profile": profile,
|
|
10276
|
+
"app_key": app_key,
|
|
10277
|
+
"publish": True,
|
|
10278
|
+
"upsert_views": [],
|
|
10279
|
+
"patch_views": [
|
|
10280
|
+
{
|
|
10281
|
+
"view_key": intent.get("view_key"),
|
|
10282
|
+
"set": {
|
|
10283
|
+
"action_buttons": [
|
|
10284
|
+
public_view_action_button_payload(button, compact=True)
|
|
10285
|
+
for button in (intent.get("action_buttons") or [])
|
|
10286
|
+
if isinstance(button, ViewActionButtonPatch)
|
|
10287
|
+
],
|
|
10288
|
+
"action_buttons_mode": intent.get("mode") or "merge",
|
|
10289
|
+
},
|
|
10290
|
+
}
|
|
10291
|
+
for intent in intents
|
|
10292
|
+
if str(intent.get("view_key") or "").strip()
|
|
10293
|
+
],
|
|
10294
|
+
"remove_views": [],
|
|
10295
|
+
},
|
|
10296
|
+
}
|
|
10297
|
+
|
|
10298
|
+
def _failed_view_action_button_intents(self, *, intents: list[dict[str, Any]], result: dict[str, Any]) -> list[dict[str, Any]]:
|
|
10299
|
+
failed_view_keys: set[str] = set()
|
|
10300
|
+
failed_button_texts: set[str] = set()
|
|
10301
|
+
for item in result.get("failed") or []:
|
|
10302
|
+
if not isinstance(item, dict):
|
|
10303
|
+
continue
|
|
10304
|
+
view_key = str(item.get("view_key") or item.get("viewgraph_key") or "").strip()
|
|
10305
|
+
if view_key:
|
|
10306
|
+
failed_view_keys.add(view_key)
|
|
10307
|
+
button_text = str(item.get("button_text") or item.get("text") or "").strip()
|
|
10308
|
+
if button_text:
|
|
10309
|
+
failed_button_texts.add(button_text)
|
|
10310
|
+
for item in result.get("view_configs") or []:
|
|
10311
|
+
if not isinstance(item, dict):
|
|
10312
|
+
continue
|
|
10313
|
+
status = str(item.get("status") or "").strip()
|
|
10314
|
+
if status and status not in {"success", "noop", "skipped"}:
|
|
10315
|
+
view_key = str(item.get("view_key") or item.get("viewgraph_key") or "").strip()
|
|
10316
|
+
if view_key:
|
|
10317
|
+
failed_view_keys.add(view_key)
|
|
10318
|
+
if failed_view_keys:
|
|
10319
|
+
return [
|
|
10320
|
+
intent
|
|
10321
|
+
for intent in intents
|
|
10322
|
+
if str(intent.get("view_key") or "").strip() in failed_view_keys
|
|
10323
|
+
]
|
|
10324
|
+
if failed_button_texts:
|
|
10325
|
+
return [
|
|
10326
|
+
intent
|
|
10327
|
+
for intent in intents
|
|
10328
|
+
if any(
|
|
10329
|
+
isinstance(button, ViewActionButtonPatch) and str(button.text or "").strip() in failed_button_texts
|
|
10330
|
+
for button in (intent.get("action_buttons") or [])
|
|
10331
|
+
)
|
|
10332
|
+
]
|
|
10333
|
+
if str(result.get("status") or "").strip() == "failed" or (result.get("write_succeeded") is False and bool(result.get("write_executed"))):
|
|
10334
|
+
return list(intents)
|
|
10335
|
+
return []
|
|
10336
|
+
|
|
10337
|
+
def _apply_view_action_buttons(
|
|
10338
|
+
self,
|
|
10339
|
+
*,
|
|
10340
|
+
profile: str,
|
|
10341
|
+
app_key: str,
|
|
10342
|
+
intents: list[dict[str, Any]],
|
|
10343
|
+
) -> dict[str, Any]:
|
|
10344
|
+
if not intents:
|
|
10345
|
+
return {
|
|
10346
|
+
"status": "success",
|
|
10347
|
+
"verified": True,
|
|
10348
|
+
"write_executed": False,
|
|
10349
|
+
"write_succeeded": False,
|
|
10350
|
+
"verification": {
|
|
10351
|
+
"action_buttons_verified": True,
|
|
10352
|
+
"view_button_bindings_verified": True,
|
|
10353
|
+
},
|
|
10354
|
+
"retry_payload": None,
|
|
10355
|
+
}
|
|
10356
|
+
request, issues = self._compile_view_action_buttons_request(app_key=app_key, intents=intents)
|
|
10357
|
+
if issues:
|
|
10358
|
+
return {
|
|
10359
|
+
"status": "failed",
|
|
10360
|
+
"error_code": "DUPLICATE_ACTION_BUTTON_CONFLICT" if any(item.get("error_code") == "DUPLICATE_ACTION_BUTTON_CONFLICT" for item in issues) else "VIEW_ACTION_BUTTONS_INVALID",
|
|
10361
|
+
"recoverable": True,
|
|
10362
|
+
"message": "view action_buttons could not be compiled; view writes may already have completed",
|
|
10363
|
+
"details": {"blocking_issues": issues},
|
|
10364
|
+
"verification": {
|
|
10365
|
+
"action_buttons_verified": False,
|
|
10366
|
+
"view_button_bindings_verified": False,
|
|
10367
|
+
},
|
|
10368
|
+
"verified": False,
|
|
10369
|
+
"write_executed": False,
|
|
10370
|
+
"write_succeeded": False,
|
|
10371
|
+
"retry_payload": self._view_action_buttons_retry_payload(profile=profile, app_key=app_key, intents=intents),
|
|
10372
|
+
}
|
|
10373
|
+
if request is None:
|
|
10374
|
+
return {
|
|
10375
|
+
"status": "success",
|
|
10376
|
+
"verified": True,
|
|
10377
|
+
"write_executed": False,
|
|
10378
|
+
"write_succeeded": False,
|
|
10379
|
+
"verification": {
|
|
10380
|
+
"action_buttons_verified": True,
|
|
10381
|
+
"view_button_bindings_verified": True,
|
|
10382
|
+
},
|
|
10383
|
+
"retry_payload": None,
|
|
10384
|
+
}
|
|
10385
|
+
result = self.app_custom_buttons_apply(profile=profile, request=request)
|
|
10386
|
+
retry_intents = [] if result.get("verified") else self._failed_view_action_button_intents(intents=intents, result=result)
|
|
10387
|
+
result["retry_payload"] = self._view_action_buttons_retry_payload(profile=profile, app_key=app_key, intents=retry_intents) if retry_intents else None
|
|
10388
|
+
return result
|
|
10389
|
+
|
|
9983
10390
|
def app_views_apply(
|
|
9984
10391
|
self,
|
|
9985
10392
|
*,
|
|
@@ -9993,11 +10400,27 @@ class AiBuilderFacade:
|
|
|
9993
10400
|
patch_views = patch_views or []
|
|
9994
10401
|
normalized_args = {
|
|
9995
10402
|
"app_key": app_key,
|
|
9996
|
-
"upsert_views": [patch
|
|
9997
|
-
"patch_views": [patch
|
|
10403
|
+
"upsert_views": [public_view_upsert_payload(patch) for patch in upsert_views],
|
|
10404
|
+
"patch_views": [public_view_partial_payload(patch) for patch in patch_views],
|
|
9998
10405
|
"remove_views": list(remove_views),
|
|
9999
10406
|
"publish": publish,
|
|
10000
10407
|
}
|
|
10408
|
+
has_action_button_intent = any(patch.action_buttons is not None for patch in upsert_views) or any(
|
|
10409
|
+
any(key in (patch.set or {}) for key in ("action_buttons", "actionButtons"))
|
|
10410
|
+
for patch in patch_views
|
|
10411
|
+
)
|
|
10412
|
+
if has_action_button_intent and not publish:
|
|
10413
|
+
return _failed(
|
|
10414
|
+
"VIEW_ACTION_BUTTONS_REQUIRE_PUBLISH",
|
|
10415
|
+
"app_views_apply action_buttons require publish=true because the underlying custom button writer may publish after successful button writes",
|
|
10416
|
+
normalized_args=normalized_args,
|
|
10417
|
+
details={
|
|
10418
|
+
"app_key": app_key,
|
|
10419
|
+
"required_publish": True,
|
|
10420
|
+
"reason": "action_buttons are compiled through app_custom_buttons_apply",
|
|
10421
|
+
},
|
|
10422
|
+
suggested_next_call={"tool_name": "app_views_apply", "arguments": {"profile": profile, **{**normalized_args, "publish": True}}},
|
|
10423
|
+
)
|
|
10001
10424
|
if not upsert_views and not patch_views and not remove_views:
|
|
10002
10425
|
response = {
|
|
10003
10426
|
"status": "success",
|
|
@@ -10104,6 +10527,28 @@ class AiBuilderFacade:
|
|
|
10104
10527
|
if name and key:
|
|
10105
10528
|
existing_by_key[key] = view
|
|
10106
10529
|
existing_by_name.setdefault(name, []).append(view)
|
|
10530
|
+
patch_action_button_intents: list[dict[str, Any]] = []
|
|
10531
|
+
action_button_patch_results: list[dict[str, Any]] = []
|
|
10532
|
+
if patch_views:
|
|
10533
|
+
sanitized_patch_views, patch_action_button_intents, action_button_patch_issues, action_button_patch_results = self._extract_view_action_button_patch_intents(
|
|
10534
|
+
existing_by_key=existing_by_key,
|
|
10535
|
+
existing_by_name=existing_by_name,
|
|
10536
|
+
patch_views=patch_views,
|
|
10537
|
+
)
|
|
10538
|
+
if action_button_patch_results:
|
|
10539
|
+
normalized_args["action_button_patch_results"] = action_button_patch_results
|
|
10540
|
+
if action_button_patch_issues:
|
|
10541
|
+
return finalize(
|
|
10542
|
+
_failed(
|
|
10543
|
+
"VIEW_ACTION_BUTTON_PATCH_FAILED",
|
|
10544
|
+
"one or more patch_views action_buttons entries could not be resolved; no write was executed",
|
|
10545
|
+
normalized_args=normalized_args,
|
|
10546
|
+
details={"patch_results": action_button_patch_results, "blocking_issues": action_button_patch_issues},
|
|
10547
|
+
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
10548
|
+
)
|
|
10549
|
+
)
|
|
10550
|
+
patch_views = sanitized_patch_views
|
|
10551
|
+
normalized_args["patch_views"] = [public_view_partial_payload(patch) for patch in patch_views]
|
|
10107
10552
|
creating_view_names = [
|
|
10108
10553
|
patch.name
|
|
10109
10554
|
for patch in upsert_views
|
|
@@ -10155,7 +10600,7 @@ class AiBuilderFacade:
|
|
|
10155
10600
|
)
|
|
10156
10601
|
)
|
|
10157
10602
|
upsert_views = [*upsert_views, *expanded_views]
|
|
10158
|
-
normalized_args["upsert_views"] = [patch
|
|
10603
|
+
normalized_args["upsert_views"] = [public_view_upsert_payload(patch) for patch in upsert_views]
|
|
10159
10604
|
normalized_args["patch_results"] = patch_results
|
|
10160
10605
|
current_fields_by_name = {
|
|
10161
10606
|
str(field.get("name") or ""): field
|
|
@@ -10250,6 +10695,22 @@ class AiBuilderFacade:
|
|
|
10250
10695
|
removed_keys: set[str] = set()
|
|
10251
10696
|
view_results: list[dict[str, Any]] = []
|
|
10252
10697
|
failed_views: list[dict[str, Any]] = []
|
|
10698
|
+
action_button_intents: list[dict[str, Any]] = list(patch_action_button_intents)
|
|
10699
|
+
|
|
10700
|
+
def record_action_button_intent(*, patch: ViewUpsertPatch, view_key: str | None, index: int) -> None:
|
|
10701
|
+
if patch.action_buttons is None:
|
|
10702
|
+
return
|
|
10703
|
+
action_button_intents.append(
|
|
10704
|
+
{
|
|
10705
|
+
"source": "upsert_views",
|
|
10706
|
+
"index": index,
|
|
10707
|
+
"view_key": str(view_key or "").strip(),
|
|
10708
|
+
"view_name": patch.name,
|
|
10709
|
+
"action_buttons": list(patch.action_buttons),
|
|
10710
|
+
"mode": patch.action_buttons_mode,
|
|
10711
|
+
}
|
|
10712
|
+
)
|
|
10713
|
+
|
|
10253
10714
|
for selector in remove_views:
|
|
10254
10715
|
selector_text = str(selector or "").strip()
|
|
10255
10716
|
if not selector_text:
|
|
@@ -10644,6 +11105,7 @@ class AiBuilderFacade:
|
|
|
10644
11105
|
"apply_columns": deepcopy(apply_columns),
|
|
10645
11106
|
}
|
|
10646
11107
|
)
|
|
11108
|
+
record_action_button_intent(patch=patch, view_key=existing_key, index=ordinal - 1)
|
|
10647
11109
|
else:
|
|
10648
11110
|
template_key = _pick_view_template_key(existing_view_list, desired_type=patch.type.value)
|
|
10649
11111
|
should_copy_template = patch.type.value == "table" and template_key and not translated_filters
|
|
@@ -10698,6 +11160,7 @@ class AiBuilderFacade:
|
|
|
10698
11160
|
"expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
|
|
10699
11161
|
}
|
|
10700
11162
|
)
|
|
11163
|
+
record_action_button_intent(patch=patch, view_key=created_key, index=ordinal - 1)
|
|
10701
11164
|
except (QingflowApiError, RuntimeError) as error:
|
|
10702
11165
|
api_error = _coerce_api_error(error)
|
|
10703
11166
|
should_retry_minimal = operation_phase != "default_view_apply_config_sync" and (
|
|
@@ -10790,6 +11253,7 @@ class AiBuilderFacade:
|
|
|
10790
11253
|
"apply_columns": deepcopy(apply_columns),
|
|
10791
11254
|
}
|
|
10792
11255
|
)
|
|
11256
|
+
record_action_button_intent(patch=patch, view_key=existing_key, index=ordinal - 1)
|
|
10793
11257
|
else:
|
|
10794
11258
|
created.append(patch.name)
|
|
10795
11259
|
view_results.append(
|
|
@@ -10807,6 +11271,7 @@ class AiBuilderFacade:
|
|
|
10807
11271
|
"apply_columns": deepcopy(apply_columns),
|
|
10808
11272
|
}
|
|
10809
11273
|
)
|
|
11274
|
+
record_action_button_intent(patch=patch, view_key=created_key, index=ordinal - 1)
|
|
10810
11275
|
continue
|
|
10811
11276
|
fallback_payload = _build_minimal_view_payload(
|
|
10812
11277
|
app_key=app_key,
|
|
@@ -10834,6 +11299,7 @@ class AiBuilderFacade:
|
|
|
10834
11299
|
"expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
|
|
10835
11300
|
}
|
|
10836
11301
|
)
|
|
11302
|
+
record_action_button_intent(patch=patch, view_key=created_key, index=ordinal - 1)
|
|
10837
11303
|
continue
|
|
10838
11304
|
except (QingflowApiError, RuntimeError) as fallback_error:
|
|
10839
11305
|
api_error = _coerce_api_error(fallback_error)
|
|
@@ -10881,6 +11347,56 @@ class AiBuilderFacade:
|
|
|
10881
11347
|
failed_views.append(failure_entry)
|
|
10882
11348
|
view_results.append(failure_entry)
|
|
10883
11349
|
continue
|
|
11350
|
+
successful_action_view_keys = {
|
|
11351
|
+
str(item.get("view_key") or "").strip()
|
|
11352
|
+
for item in view_results
|
|
11353
|
+
if str(item.get("status") or "") in {"created", "updated"} and str(item.get("view_key") or "").strip()
|
|
11354
|
+
}
|
|
11355
|
+
successful_action_view_names = {
|
|
11356
|
+
str(item.get("name") or "").strip()
|
|
11357
|
+
for item in view_results
|
|
11358
|
+
if str(item.get("status") or "") in {"created", "updated"} and str(item.get("name") or "").strip()
|
|
11359
|
+
}
|
|
11360
|
+
skipped_action_button_intents = [
|
|
11361
|
+
intent
|
|
11362
|
+
for intent in action_button_intents
|
|
11363
|
+
if bool(intent.get("requires_view_write"))
|
|
11364
|
+
and str(intent.get("view_key") or "").strip() not in successful_action_view_keys
|
|
11365
|
+
and str(intent.get("view_name") or "").strip() not in successful_action_view_names
|
|
11366
|
+
]
|
|
11367
|
+
skipped_action_button_intent_ids = {id(intent) for intent in skipped_action_button_intents}
|
|
11368
|
+
runnable_action_button_intents = [
|
|
11369
|
+
intent for intent in action_button_intents if id(intent) not in skipped_action_button_intent_ids
|
|
11370
|
+
]
|
|
11371
|
+
action_buttons_result = self._apply_view_action_buttons(
|
|
11372
|
+
profile=profile,
|
|
11373
|
+
app_key=app_key,
|
|
11374
|
+
intents=runnable_action_button_intents,
|
|
11375
|
+
)
|
|
11376
|
+
if skipped_action_button_intents:
|
|
11377
|
+
action_buttons_result.setdefault("skipped_due_to_view_write_failure", [])
|
|
11378
|
+
if isinstance(action_buttons_result["skipped_due_to_view_write_failure"], list):
|
|
11379
|
+
action_buttons_result["skipped_due_to_view_write_failure"].extend(
|
|
11380
|
+
{
|
|
11381
|
+
"source": intent.get("source"),
|
|
11382
|
+
"index": intent.get("index"),
|
|
11383
|
+
"view_key": intent.get("view_key"),
|
|
11384
|
+
"view_name": intent.get("view_name"),
|
|
11385
|
+
"action_buttons_count": len(intent.get("action_buttons") or []),
|
|
11386
|
+
}
|
|
11387
|
+
for intent in skipped_action_button_intents
|
|
11388
|
+
)
|
|
11389
|
+
action_button_write_executed = bool(action_buttons_result.get("write_executed"))
|
|
11390
|
+
action_button_write_succeeded = bool(action_buttons_result.get("write_succeeded"))
|
|
11391
|
+
action_buttons_verification = action_buttons_result.get("verification") if isinstance(action_buttons_result.get("verification"), dict) else {}
|
|
11392
|
+
action_buttons_verified = (
|
|
11393
|
+
bool(action_buttons_result.get("verified", True))
|
|
11394
|
+
and bool(action_buttons_verification.get("custom_buttons_verified", action_buttons_verification.get("action_buttons_verified", True)))
|
|
11395
|
+
and not skipped_action_button_intents
|
|
11396
|
+
)
|
|
11397
|
+
view_action_button_bindings_verified = bool(action_buttons_verification.get("view_button_bindings_verified", True)) and not skipped_action_button_intents
|
|
11398
|
+
action_buttons_failed = bool(action_button_intents) and not (action_buttons_verified and view_action_button_bindings_verified)
|
|
11399
|
+
action_buttons_retry_payload = action_buttons_result.get("retry_payload") if isinstance(action_buttons_result.get("retry_payload"), dict) else None
|
|
10884
11400
|
needs_view_list_readback = bool(created or updated)
|
|
10885
11401
|
verified_view_result: list[dict[str, Any]] | None = []
|
|
10886
11402
|
verified_views_unavailable = False
|
|
@@ -11251,9 +11767,9 @@ class AiBuilderFacade:
|
|
|
11251
11767
|
view_query_conditions_verified = verified and not query_condition_readback_pending and not query_condition_mismatches
|
|
11252
11768
|
view_associated_resources_verified = verified and not associated_resource_readback_pending and not associated_resource_mismatches
|
|
11253
11769
|
view_buttons_verified = verified and not button_readback_pending and not button_mismatches and not custom_button_readback_pending
|
|
11254
|
-
noop = not created and not updated and not removed
|
|
11770
|
+
noop = not created and not updated and not removed and not action_button_write_executed
|
|
11255
11771
|
if failed_views:
|
|
11256
|
-
successful_changes = bool(created or updated or removed)
|
|
11772
|
+
successful_changes = bool(created or updated or removed or action_button_write_succeeded)
|
|
11257
11773
|
first_failure = failed_views[0]
|
|
11258
11774
|
response = {
|
|
11259
11775
|
"status": "partial_success" if successful_changes else "failed",
|
|
@@ -11269,6 +11785,7 @@ class AiBuilderFacade:
|
|
|
11269
11785
|
"query_condition_mismatches": query_condition_mismatches,
|
|
11270
11786
|
"associated_resource_mismatches": associated_resource_mismatches,
|
|
11271
11787
|
"button_mismatches": button_mismatches,
|
|
11788
|
+
**({"action_buttons_result": action_buttons_result} if action_button_intents else {}),
|
|
11272
11789
|
**(
|
|
11273
11790
|
{"custom_button_readback_pending": deepcopy(custom_button_readback_pending_entries)}
|
|
11274
11791
|
if custom_button_readback_pending_entries
|
|
@@ -11301,6 +11818,11 @@ class AiBuilderFacade:
|
|
|
11301
11818
|
if (button_readback_pending or button_mismatches)
|
|
11302
11819
|
else []
|
|
11303
11820
|
)
|
|
11821
|
+
+ (
|
|
11822
|
+
[_warning("VIEW_ACTION_BUTTONS_UNVERIFIED", "view definitions may exist, but inline action button creation or binding is not fully verified")]
|
|
11823
|
+
if action_buttons_failed
|
|
11824
|
+
else []
|
|
11825
|
+
)
|
|
11304
11826
|
+ (
|
|
11305
11827
|
[_warning("VIEW_CUSTOM_BUTTON_READBACK_PENDING", "system buttons verified, but draft custom button bindings are not fully visible through view readback yet")]
|
|
11306
11828
|
if custom_button_readback_pending
|
|
@@ -11313,6 +11835,8 @@ class AiBuilderFacade:
|
|
|
11313
11835
|
"view_query_conditions_verified": view_query_conditions_verified,
|
|
11314
11836
|
"view_associated_resources_verified": view_associated_resources_verified,
|
|
11315
11837
|
"view_buttons_verified": view_buttons_verified,
|
|
11838
|
+
"action_buttons_verified": action_buttons_verified,
|
|
11839
|
+
"view_button_bindings_verified": view_action_button_bindings_verified,
|
|
11316
11840
|
"views_read_unavailable": verified_views_unavailable,
|
|
11317
11841
|
"by_view": verification_by_view,
|
|
11318
11842
|
"custom_button_readback_pending": custom_button_readback_pending,
|
|
@@ -11321,10 +11845,10 @@ class AiBuilderFacade:
|
|
|
11321
11845
|
"app_key": app_key,
|
|
11322
11846
|
"app_name": app_name,
|
|
11323
11847
|
"views_diff": {"created": created, "updated": updated, "removed": removed, "failed": failed_views},
|
|
11324
|
-
"verified": verified and view_filters_verified and view_query_conditions_verified and view_associated_resources_verified and view_buttons_verified,
|
|
11325
|
-
"write_executed": bool(created or updated or removed),
|
|
11326
|
-
"write_succeeded": bool(created or updated or removed),
|
|
11327
|
-
"safe_to_retry": not bool(created or updated or removed),
|
|
11848
|
+
"verified": verified and view_filters_verified and view_query_conditions_verified and view_associated_resources_verified and view_buttons_verified and action_buttons_verified and view_action_button_bindings_verified,
|
|
11849
|
+
"write_executed": bool(created or updated or removed or action_button_write_executed),
|
|
11850
|
+
"write_succeeded": bool(created or updated or removed or action_button_write_succeeded),
|
|
11851
|
+
"safe_to_retry": not bool(created or updated or removed or action_button_write_executed),
|
|
11328
11852
|
}
|
|
11329
11853
|
return finalize(self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response))
|
|
11330
11854
|
warnings: list[dict[str, Any]] = []
|
|
@@ -11336,6 +11860,8 @@ class AiBuilderFacade:
|
|
|
11336
11860
|
warnings.append(_warning("VIEW_ASSOCIATED_RESOURCES_UNVERIFIED", "view definitions were applied, but associated resource visibility is not fully verified"))
|
|
11337
11861
|
if button_readback_pending or button_mismatches:
|
|
11338
11862
|
warnings.append(_warning("VIEW_BUTTONS_UNVERIFIED", "view definitions were applied, but saved button behavior is not fully verified"))
|
|
11863
|
+
if action_buttons_failed:
|
|
11864
|
+
warnings.append(_warning("VIEW_ACTION_BUTTONS_UNVERIFIED", "view definitions were applied, but inline action button creation or binding is not fully verified"))
|
|
11339
11865
|
if custom_button_readback_pending:
|
|
11340
11866
|
warnings.append(
|
|
11341
11867
|
_warning(
|
|
@@ -11350,14 +11876,25 @@ class AiBuilderFacade:
|
|
|
11350
11876
|
"view delete was sent, but deletion readback is not fully verified; do not blindly repeat delete",
|
|
11351
11877
|
)
|
|
11352
11878
|
)
|
|
11353
|
-
all_verified =
|
|
11879
|
+
all_verified = (
|
|
11880
|
+
verified
|
|
11881
|
+
and view_filters_verified
|
|
11882
|
+
and view_query_conditions_verified
|
|
11883
|
+
and view_associated_resources_verified
|
|
11884
|
+
and view_buttons_verified
|
|
11885
|
+
and action_buttons_verified
|
|
11886
|
+
and view_action_button_bindings_verified
|
|
11887
|
+
)
|
|
11888
|
+
action_buttons_error_code = str(action_buttons_result.get("error_code") or "VIEW_ACTION_BUTTONS_APPLY_FAILED") if action_buttons_failed else None
|
|
11354
11889
|
response = {
|
|
11355
11890
|
"status": "success" if all_verified else "partial_success",
|
|
11356
|
-
"error_code": None if all_verified else ("VIEW_BUTTON_READBACK_MISMATCH" if button_mismatches else "VIEW_ASSOCIATED_RESOURCE_READBACK_MISMATCH" if associated_resource_mismatches else "VIEW_QUERY_CONDITION_READBACK_MISMATCH" if query_condition_mismatches else "VIEW_FILTER_READBACK_MISMATCH" if filter_mismatches else "VIEWS_READBACK_PENDING"),
|
|
11891
|
+
"error_code": None if all_verified else (action_buttons_error_code if action_buttons_failed else "VIEW_BUTTON_READBACK_MISMATCH" if button_mismatches else "VIEW_ASSOCIATED_RESOURCE_READBACK_MISMATCH" if associated_resource_mismatches else "VIEW_QUERY_CONDITION_READBACK_MISMATCH" if query_condition_mismatches else "VIEW_FILTER_READBACK_MISMATCH" if filter_mismatches else "VIEWS_READBACK_PENDING"),
|
|
11357
11892
|
"recoverable": not all_verified,
|
|
11358
11893
|
"message": (
|
|
11359
11894
|
"applied view patch"
|
|
11360
11895
|
if all_verified
|
|
11896
|
+
else "applied view patch; inline action buttons did not fully verify"
|
|
11897
|
+
if action_buttons_failed
|
|
11361
11898
|
else "applied view patch; buttons did not fully verify"
|
|
11362
11899
|
if button_mismatches
|
|
11363
11900
|
else "applied view patch; associated resources did not fully verify"
|
|
@@ -11376,6 +11913,7 @@ class AiBuilderFacade:
|
|
|
11376
11913
|
**({"query_condition_mismatches": query_condition_mismatches} if query_condition_mismatches else {}),
|
|
11377
11914
|
**({"associated_resource_mismatches": associated_resource_mismatches} if associated_resource_mismatches else {}),
|
|
11378
11915
|
**({"button_mismatches": button_mismatches} if button_mismatches else {}),
|
|
11916
|
+
**({"action_buttons_result": action_buttons_result} if action_button_intents else {}),
|
|
11379
11917
|
**(
|
|
11380
11918
|
{"custom_button_readback_pending": deepcopy(custom_button_readback_pending_entries)}
|
|
11381
11919
|
if custom_button_readback_pending_entries
|
|
@@ -11383,7 +11921,7 @@ class AiBuilderFacade:
|
|
|
11383
11921
|
),
|
|
11384
11922
|
},
|
|
11385
11923
|
"request_id": None,
|
|
11386
|
-
"suggested_next_call": None if all_verified else {"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
11924
|
+
"suggested_next_call": None if all_verified else (action_buttons_retry_payload if action_buttons_failed and action_buttons_retry_payload else {"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}}),
|
|
11387
11925
|
"noop": noop,
|
|
11388
11926
|
"warnings": warnings,
|
|
11389
11927
|
"verification": {
|
|
@@ -11392,6 +11930,8 @@ class AiBuilderFacade:
|
|
|
11392
11930
|
"view_query_conditions_verified": view_query_conditions_verified,
|
|
11393
11931
|
"view_associated_resources_verified": view_associated_resources_verified,
|
|
11394
11932
|
"view_buttons_verified": view_buttons_verified,
|
|
11933
|
+
"action_buttons_verified": action_buttons_verified,
|
|
11934
|
+
"view_button_bindings_verified": view_action_button_bindings_verified,
|
|
11395
11935
|
"views_read_unavailable": verified_views_unavailable,
|
|
11396
11936
|
"filter_readback_pending": filter_readback_pending,
|
|
11397
11937
|
"query_condition_readback_pending": query_condition_readback_pending,
|
|
@@ -11406,9 +11946,9 @@ class AiBuilderFacade:
|
|
|
11406
11946
|
"app_name": app_name,
|
|
11407
11947
|
"views_diff": {"created": created, "updated": updated, "removed": removed, "failed": []},
|
|
11408
11948
|
"verified": all_verified,
|
|
11409
|
-
"write_executed": bool(created or updated or removed),
|
|
11410
|
-
"write_succeeded": bool(created or updated or removed),
|
|
11411
|
-
"safe_to_retry": not bool(created or updated or removed),
|
|
11949
|
+
"write_executed": bool(created or updated or removed or action_button_write_executed),
|
|
11950
|
+
"write_succeeded": bool(created or updated or removed or action_button_write_succeeded),
|
|
11951
|
+
"safe_to_retry": not bool(created or updated or removed or action_button_write_executed),
|
|
11412
11952
|
}
|
|
11413
11953
|
return finalize(self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response))
|
|
11414
11954
|
|
|
@@ -49,6 +49,8 @@ from ..builder_facade.models import (
|
|
|
49
49
|
ViewUpsertPatch,
|
|
50
50
|
ViewsPreset,
|
|
51
51
|
ViewsPlanRequest,
|
|
52
|
+
public_view_partial_payload,
|
|
53
|
+
public_view_upsert_payload,
|
|
52
54
|
)
|
|
53
55
|
from ..builder_facade.service import AiBuilderFacade, INTEGRATION_OUTPUT_TARGET_FIELD_TYPES
|
|
54
56
|
from ..solution.compiler.icon_utils import (
|
|
@@ -2018,6 +2020,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2018
2020
|
profile: str,
|
|
2019
2021
|
app_key: str,
|
|
2020
2022
|
upsert_views: list[JSONObject] | None = None,
|
|
2023
|
+
patch_views: list[JSONObject] | None = None,
|
|
2021
2024
|
remove_views: list[str] | None = None,
|
|
2022
2025
|
preset: str | None = None,
|
|
2023
2026
|
) -> JSONObject:
|
|
@@ -2036,6 +2039,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2036
2039
|
{
|
|
2037
2040
|
"app_key": app_key,
|
|
2038
2041
|
"upsert_views": upsert_views or [],
|
|
2042
|
+
"patch_views": patch_views or [],
|
|
2039
2043
|
"remove_views": remove_views or [],
|
|
2040
2044
|
"preset": preset,
|
|
2041
2045
|
}
|
|
@@ -2052,6 +2056,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2052
2056
|
"app_key": app_key,
|
|
2053
2057
|
"preset": "default_table",
|
|
2054
2058
|
"upsert_views": [],
|
|
2059
|
+
"patch_views": [],
|
|
2055
2060
|
"remove_views": [],
|
|
2056
2061
|
},
|
|
2057
2062
|
},
|
|
@@ -2059,8 +2064,24 @@ class AiBuilderTools(ToolBase):
|
|
|
2059
2064
|
return _safe_tool_call(
|
|
2060
2065
|
lambda: self._facade.app_views_plan(profile=profile, request=request),
|
|
2061
2066
|
error_code="VIEWS_PLAN_FAILED",
|
|
2062
|
-
normalized_args=
|
|
2063
|
-
|
|
2067
|
+
normalized_args={
|
|
2068
|
+
"app_key": request.app_key,
|
|
2069
|
+
"upsert_views": [public_view_upsert_payload(view) for view in request.upsert_views],
|
|
2070
|
+
"patch_views": [public_view_partial_payload(patch) for patch in request.patch_views],
|
|
2071
|
+
"remove_views": list(request.remove_views),
|
|
2072
|
+
"preset": request.preset.value if request.preset is not None else None,
|
|
2073
|
+
},
|
|
2074
|
+
suggested_next_call={
|
|
2075
|
+
"tool_name": "app_views_plan",
|
|
2076
|
+
"arguments": {
|
|
2077
|
+
"profile": profile,
|
|
2078
|
+
"app_key": request.app_key,
|
|
2079
|
+
"upsert_views": [public_view_upsert_payload(view) for view in request.upsert_views],
|
|
2080
|
+
"patch_views": [public_view_partial_payload(patch) for patch in request.patch_views],
|
|
2081
|
+
"remove_views": list(request.remove_views),
|
|
2082
|
+
"preset": request.preset.value if request.preset is not None else None,
|
|
2083
|
+
},
|
|
2084
|
+
},
|
|
2064
2085
|
)
|
|
2065
2086
|
|
|
2066
2087
|
@tool_cn_name("应用结构应用")
|
|
@@ -2990,7 +3011,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2990
3011
|
)
|
|
2991
3012
|
if reserved_failure is not None:
|
|
2992
3013
|
return reserved_failure
|
|
2993
|
-
if patch_views:
|
|
3014
|
+
if patch_views or _view_payload_has_action_buttons(upsert_views=upsert_views, patch_views=patch_views):
|
|
2994
3015
|
try:
|
|
2995
3016
|
parsed_views = [ViewUpsertPatch.model_validate(item) for item in (upsert_views or [])]
|
|
2996
3017
|
parsed_patch_views = [ViewPartialPatch.model_validate(item) for item in patch_views]
|
|
@@ -3016,8 +3037,8 @@ class AiBuilderTools(ToolBase):
|
|
|
3016
3037
|
normalized_args = {
|
|
3017
3038
|
"app_key": app_key,
|
|
3018
3039
|
"publish": publish,
|
|
3019
|
-
"upsert_views": [view
|
|
3020
|
-
"patch_views": [patch
|
|
3040
|
+
"upsert_views": [public_view_upsert_payload(view) for view in parsed_views],
|
|
3041
|
+
"patch_views": [public_view_partial_payload(patch) for patch in parsed_patch_views],
|
|
3021
3042
|
"remove_views": list(remove_views or []),
|
|
3022
3043
|
}
|
|
3023
3044
|
return _safe_tool_call(
|
|
@@ -3072,7 +3093,8 @@ class AiBuilderTools(ToolBase):
|
|
|
3072
3093
|
normalized_args = {
|
|
3073
3094
|
"app_key": str(plan_args.get("app_key") or app_key),
|
|
3074
3095
|
"publish": publish,
|
|
3075
|
-
"upsert_views": [view
|
|
3096
|
+
"upsert_views": [public_view_upsert_payload(view) for view in parsed_views],
|
|
3097
|
+
"patch_views": [],
|
|
3076
3098
|
"remove_views": list(plan_args.get("remove_views") or remove_views),
|
|
3077
3099
|
}
|
|
3078
3100
|
return _safe_tool_call(
|
|
@@ -4278,6 +4300,23 @@ def _reserved_system_view_name_failure(
|
|
|
4278
4300
|
return None
|
|
4279
4301
|
|
|
4280
4302
|
|
|
4303
|
+
def _view_payload_has_action_buttons(
|
|
4304
|
+
*,
|
|
4305
|
+
upsert_views: list[JSONObject] | None = None,
|
|
4306
|
+
patch_views: list[JSONObject] | None = None,
|
|
4307
|
+
) -> bool:
|
|
4308
|
+
for item in upsert_views or []:
|
|
4309
|
+
if isinstance(item, dict) and any(key in item for key in ("action_buttons", "actionButtons")):
|
|
4310
|
+
return True
|
|
4311
|
+
for item in patch_views or []:
|
|
4312
|
+
if not isinstance(item, dict):
|
|
4313
|
+
continue
|
|
4314
|
+
raw_set = item.get("set")
|
|
4315
|
+
if isinstance(raw_set, dict) and any(key in raw_set for key in ("action_buttons", "actionButtons")):
|
|
4316
|
+
return True
|
|
4317
|
+
return False
|
|
4318
|
+
|
|
4319
|
+
|
|
4281
4320
|
def _config_failure(
|
|
4282
4321
|
*,
|
|
4283
4322
|
tool_name: str,
|
|
@@ -4986,6 +5025,15 @@ def _builder_view_resources(payload: JSONObject) -> list[JSONObject]:
|
|
|
4986
5025
|
message=message,
|
|
4987
5026
|
)
|
|
4988
5027
|
)
|
|
5028
|
+
details = payload.get("details") if isinstance(payload.get("details"), dict) else {}
|
|
5029
|
+
action_buttons_result = details.get("action_buttons_result") if isinstance(details, dict) else None
|
|
5030
|
+
if isinstance(action_buttons_result, dict):
|
|
5031
|
+
nested_payload = {
|
|
5032
|
+
**action_buttons_result,
|
|
5033
|
+
"app_key": payload.get("app_key"),
|
|
5034
|
+
"app_name": payload.get("app_name"),
|
|
5035
|
+
}
|
|
5036
|
+
resources.extend(_builder_button_resources(nested_payload))
|
|
4989
5037
|
return resources
|
|
4990
5038
|
|
|
4991
5039
|
|
|
@@ -6460,11 +6508,15 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6460
6508
|
"upsert_views[].columns",
|
|
6461
6509
|
"upsert_views[].filters",
|
|
6462
6510
|
"upsert_views[].buttons",
|
|
6511
|
+
"upsert_views[].action_buttons",
|
|
6512
|
+
"upsert_views[].action_buttons_mode",
|
|
6463
6513
|
"upsert_views[].visibility",
|
|
6464
6514
|
"upsert_views[].query_conditions",
|
|
6465
6515
|
"patch_views[].view_key",
|
|
6466
6516
|
"patch_views[].name",
|
|
6467
6517
|
"patch_views[].set",
|
|
6518
|
+
"patch_views[].set.action_buttons",
|
|
6519
|
+
"patch_views[].set.action_buttons_mode",
|
|
6468
6520
|
"patch_views[].unset",
|
|
6469
6521
|
],
|
|
6470
6522
|
"aliases": {
|
|
@@ -6492,6 +6544,14 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6492
6544
|
"buttons[].buttonFormula": "buttons[].button_formula",
|
|
6493
6545
|
"buttons[].buttonFormulaType": "buttons[].button_formula_type",
|
|
6494
6546
|
"buttons[].printTpls": "buttons[].print_tpls",
|
|
6547
|
+
"actionButtons": "action_buttons",
|
|
6548
|
+
"actionButtonsMode": "action_buttons_mode",
|
|
6549
|
+
"action_buttons[].button_text": "action_buttons[].text",
|
|
6550
|
+
"action_buttons[].trigger_action": "action_buttons[].action",
|
|
6551
|
+
"action_buttons[].trigger_link_url": "action_buttons[].url",
|
|
6552
|
+
"action_buttons[].visibleWhen": "action_buttons[].visible_when",
|
|
6553
|
+
"action_buttons[].fieldMappings": "action_buttons[].field_mappings",
|
|
6554
|
+
"action_buttons[].defaultValues": "action_buttons[].default_values",
|
|
6495
6555
|
},
|
|
6496
6556
|
"allowed_values": {
|
|
6497
6557
|
"preset": [member.value for member in ViewsPreset],
|
|
@@ -6499,6 +6559,9 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6499
6559
|
"view.filter.operator": [member.value for member in ViewFilterOperator],
|
|
6500
6560
|
"view.buttons.button_type": ["SYSTEM", "CUSTOM"],
|
|
6501
6561
|
"view.buttons.config_type": ["TOP", "DETAIL", "INSIDE"],
|
|
6562
|
+
"view.action_buttons.action": ["add_data", "link", "qRobot", "wings"],
|
|
6563
|
+
"view.action_buttons.placement": [member.value for member in PublicButtonPlacement],
|
|
6564
|
+
"view.action_buttons_mode": ["merge", "replace"],
|
|
6502
6565
|
**deepcopy(_VISIBILITY_ALLOWED_VALUES),
|
|
6503
6566
|
},
|
|
6504
6567
|
"execution_notes": [
|
|
@@ -6508,6 +6571,9 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6508
6571
|
"upsert_views[].query_conditions.rows is a layout matrix of query-panel supported field names; it is compiled to backend queryCondition queIds",
|
|
6509
6572
|
"do not put relation/attachment/subtable/code_block/q_linker/address fields in query_conditions; use filters for fixed filters or app_associated_resources_apply.match_mappings for current-record relation/report matching",
|
|
6510
6573
|
"use patch_views for partial parameter replacement on existing views; the tool reads current config, merges patch_views[].set/unset, then submits the backend full-save payload internally",
|
|
6574
|
+
"business buttons should be declared with action_buttons on the target view; app_views_apply writes the view first, then compiles action_buttons through the custom button writer and binds them to the resulting view_key",
|
|
6575
|
+
"action_buttons_mode defaults to merge; replace rewrites this view's custom button bindings, and replace plus action_buttons=[] clears this view's custom button bindings without deleting button bodies",
|
|
6576
|
+
"legacy upsert_views[].buttons is a compatibility path for raw full replacement of existing view button config; do not use it as the normal agent path",
|
|
6511
6577
|
"remove_views accepts a raw view_key or an exact unique view name; after DELETE the tool verifies deletion by single view_key readback, not by a full app view list",
|
|
6512
6578
|
"deleted views return verification.by_view[].delete_executed, readback_status, and safe_to_retry_delete=false; if readback is pending, do not blindly repeat the delete",
|
|
6513
6579
|
"do not create business views named 全部数据, 我的数据, 我发起的, 待办, 已办, or 抄送; these are built-in system/default views. Use business-specific names for new views, and pass the existing raw view_key or patch_views when changing a built-in view",
|
|
@@ -6538,6 +6604,27 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6538
6604
|
],
|
|
6539
6605
|
"remove_views": [],
|
|
6540
6606
|
},
|
|
6607
|
+
"action_buttons_example": {
|
|
6608
|
+
"profile": "default",
|
|
6609
|
+
"app_key": "APP_KEY",
|
|
6610
|
+
"upsert_views": [
|
|
6611
|
+
{
|
|
6612
|
+
"name": "生产工单执行视图",
|
|
6613
|
+
"type": "table",
|
|
6614
|
+
"columns": ["工单编号", "产品", "状态", "负责人"],
|
|
6615
|
+
"action_buttons": [
|
|
6616
|
+
{
|
|
6617
|
+
"text": "开始生产",
|
|
6618
|
+
"action": "link",
|
|
6619
|
+
"url": "https://example.com/start",
|
|
6620
|
+
"placement": "list",
|
|
6621
|
+
"visible_when": [{"field_name": "状态", "operator": "eq", "value": "待排产"}],
|
|
6622
|
+
}
|
|
6623
|
+
],
|
|
6624
|
+
}
|
|
6625
|
+
],
|
|
6626
|
+
"remove_views": [],
|
|
6627
|
+
},
|
|
6541
6628
|
"full_upsert_query_conditions_example": {
|
|
6542
6629
|
"profile": "default",
|
|
6543
6630
|
"app_key": "APP_KEY",
|
|
@@ -6595,11 +6682,15 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6595
6682
|
"upsert_views[].columns",
|
|
6596
6683
|
"upsert_views[].filters",
|
|
6597
6684
|
"upsert_views[].buttons",
|
|
6685
|
+
"upsert_views[].action_buttons",
|
|
6686
|
+
"upsert_views[].action_buttons_mode",
|
|
6598
6687
|
"upsert_views[].visibility",
|
|
6599
6688
|
"upsert_views[].query_conditions",
|
|
6600
6689
|
"patch_views[].view_key",
|
|
6601
6690
|
"patch_views[].name",
|
|
6602
6691
|
"patch_views[].set",
|
|
6692
|
+
"patch_views[].set.action_buttons",
|
|
6693
|
+
"patch_views[].set.action_buttons_mode",
|
|
6603
6694
|
"patch_views[].unset",
|
|
6604
6695
|
],
|
|
6605
6696
|
"aliases": {
|
|
@@ -6627,12 +6718,23 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6627
6718
|
"buttons[].buttonFormula": "buttons[].button_formula",
|
|
6628
6719
|
"buttons[].buttonFormulaType": "buttons[].button_formula_type",
|
|
6629
6720
|
"buttons[].printTpls": "buttons[].print_tpls",
|
|
6721
|
+
"actionButtons": "action_buttons",
|
|
6722
|
+
"actionButtonsMode": "action_buttons_mode",
|
|
6723
|
+
"action_buttons[].button_text": "action_buttons[].text",
|
|
6724
|
+
"action_buttons[].trigger_action": "action_buttons[].action",
|
|
6725
|
+
"action_buttons[].trigger_link_url": "action_buttons[].url",
|
|
6726
|
+
"action_buttons[].visibleWhen": "action_buttons[].visible_when",
|
|
6727
|
+
"action_buttons[].fieldMappings": "action_buttons[].field_mappings",
|
|
6728
|
+
"action_buttons[].defaultValues": "action_buttons[].default_values",
|
|
6630
6729
|
},
|
|
6631
6730
|
"allowed_values": {
|
|
6632
6731
|
"view.type": [member.value for member in PublicViewType],
|
|
6633
6732
|
"view.filter.operator": [member.value for member in ViewFilterOperator],
|
|
6634
6733
|
"view.buttons.button_type": ["SYSTEM", "CUSTOM"],
|
|
6635
6734
|
"view.buttons.config_type": ["TOP", "DETAIL", "INSIDE"],
|
|
6735
|
+
"view.action_buttons.action": ["add_data", "link", "qRobot", "wings"],
|
|
6736
|
+
"view.action_buttons.placement": [member.value for member in PublicButtonPlacement],
|
|
6737
|
+
"view.action_buttons_mode": ["merge", "replace"],
|
|
6636
6738
|
**deepcopy(_VISIBILITY_ALLOWED_VALUES),
|
|
6637
6739
|
},
|
|
6638
6740
|
"execution_notes": [
|
|
@@ -6642,6 +6744,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6642
6744
|
"read back app_get after any failed or partial view apply",
|
|
6643
6745
|
"view existence verification and saved-filter verification are separate; treat filters as unverified until verification.view_filters_verified is true",
|
|
6644
6746
|
"buttons omitted preserves existing button config; buttons=[] clears all buttons; buttons=[...] replaces the full button config",
|
|
6747
|
+
"business buttons should be declared with action_buttons on the target view; app_views_apply writes the view first, then compiles action_buttons through the custom button writer and binds them to the resulting view_key",
|
|
6748
|
+
"patch_views[].set.action_buttons can add buttons to an existing view without rewriting columns/filters; action_buttons_mode defaults to merge, replace rewrites this view's custom button bindings, and replace plus action_buttons=[] clears bindings without deleting button bodies",
|
|
6749
|
+
"publish=false with action_buttons is blocked as VIEW_ACTION_BUTTONS_REQUIRE_PUBLISH because the underlying custom button writer may publish after successful button writes",
|
|
6750
|
+
"legacy upsert_views[].buttons is a compatibility path for raw full replacement of existing view button config; do not use it as the normal agent path",
|
|
6645
6751
|
"upsert_views[].visibility may set per-view visibility; omit it to preserve an existing view's auth or default a new view to workspace/not",
|
|
6646
6752
|
"filters are saved fixed filters that apply when the view opens; query_conditions configure the frontend query panel and only apply after a user enters query values",
|
|
6647
6753
|
"upsert_views[].query_conditions.rows is a layout matrix of query-panel supported field names; it is compiled to backend queryCondition queIds",
|
|
@@ -6679,6 +6785,51 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6679
6785
|
],
|
|
6680
6786
|
"remove_views": [],
|
|
6681
6787
|
},
|
|
6788
|
+
"action_buttons_example": {
|
|
6789
|
+
"profile": "default",
|
|
6790
|
+
"app_key": "APP_KEY",
|
|
6791
|
+
"publish": True,
|
|
6792
|
+
"upsert_views": [
|
|
6793
|
+
{
|
|
6794
|
+
"name": "生产工单执行视图",
|
|
6795
|
+
"type": "table",
|
|
6796
|
+
"columns": ["工单编号", "产品", "状态", "负责人"],
|
|
6797
|
+
"action_buttons": [
|
|
6798
|
+
{
|
|
6799
|
+
"text": "开始生产",
|
|
6800
|
+
"action": "link",
|
|
6801
|
+
"url": "https://example.com/start",
|
|
6802
|
+
"placement": "list",
|
|
6803
|
+
"visible_when": [{"field_name": "状态", "operator": "eq", "value": "待排产"}],
|
|
6804
|
+
}
|
|
6805
|
+
],
|
|
6806
|
+
}
|
|
6807
|
+
],
|
|
6808
|
+
"remove_views": [],
|
|
6809
|
+
},
|
|
6810
|
+
"action_buttons_patch_example": {
|
|
6811
|
+
"profile": "default",
|
|
6812
|
+
"app_key": "APP_KEY",
|
|
6813
|
+
"publish": True,
|
|
6814
|
+
"patch_views": [
|
|
6815
|
+
{
|
|
6816
|
+
"view_key": "RAW_VIEW_KEY",
|
|
6817
|
+
"set": {
|
|
6818
|
+
"action_buttons": [
|
|
6819
|
+
{
|
|
6820
|
+
"text": "创建验收单",
|
|
6821
|
+
"action": "add_data",
|
|
6822
|
+
"target_app_key": "ARRIVAL_APP",
|
|
6823
|
+
"field_mappings": [{"source_field": "数据ID", "target_field": "关联工单"}],
|
|
6824
|
+
"default_values": {"状态": "待验收"},
|
|
6825
|
+
"placement": "detail",
|
|
6826
|
+
}
|
|
6827
|
+
]
|
|
6828
|
+
},
|
|
6829
|
+
}
|
|
6830
|
+
],
|
|
6831
|
+
"remove_views": [],
|
|
6832
|
+
},
|
|
6682
6833
|
"full_upsert_query_conditions_example": {
|
|
6683
6834
|
"profile": "default",
|
|
6684
6835
|
"app_key": "APP_KEY",
|