@josephyan/qingflow-app-user-mcp 1.1.16 → 1.1.18

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.
@@ -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.model_dump(mode="json") for view in request.upsert_views]
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.model_dump(mode="json") for patch in upsert_views],
9997
- "patch_views": [patch.model_dump(mode="json") for patch in patch_views],
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.model_dump(mode="json") for patch in upsert_views]
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 = verified and view_filters_verified and view_query_conditions_verified and view_associated_resources_verified and view_buttons_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
 
@@ -12478,6 +13018,7 @@ class AiBuilderFacade:
12478
13018
  draft_readback_error: JSONObject | None = None
12479
13019
  live_readback_error: JSONObject | None = None
12480
13020
  update_payload: dict[str, Any] = {}
13021
+ inline_chart_results: list[dict[str, Any]] = []
12481
13022
  try:
12482
13023
  layout_diagnostics: dict[str, Any] = _empty_portal_layout_diagnostics()
12483
13024
  if creating:
@@ -12525,10 +13066,11 @@ class AiBuilderFacade:
12525
13066
  base_payload=base_payload,
12526
13067
  )
12527
13068
  if sections_requested:
12528
- component_payload, layout_metadata = self._build_portal_components_from_sections(
13069
+ component_payload, layout_metadata, inline_chart_results = self._build_portal_components_from_sections(
12529
13070
  profile=profile,
12530
13071
  sections=request.sections,
12531
13072
  layout_preset=request.layout_preset,
13073
+ inline_chart_results=inline_chart_results,
12532
13074
  )
12533
13075
  layout_diagnostics = _portal_layout_diagnostics(request.sections, component_payload, layout_metadata=layout_metadata)
12534
13076
  update_payload["components"] = component_payload
@@ -12556,11 +13098,12 @@ class AiBuilderFacade:
12556
13098
  draft_result = {}
12557
13099
  except (QingflowApiError, RuntimeError, ValueError) as error:
12558
13100
  api_error = _coerce_api_error(error) if not isinstance(error, ValueError) else None
12559
- if write_executed:
13101
+ inline_chart_write_executed = any(bool(item.get("write_executed")) for item in inline_chart_results)
13102
+ if write_executed or inline_chart_write_executed:
12560
13103
  transport_error = _transport_error_payload(api_error) if api_error is not None else None
12561
13104
  warning = _warning(
12562
13105
  "PORTAL_WRITE_INCOMPLETE_AFTER_PARTIAL_WRITE",
12563
- "one or more portal write steps executed before a later write step failed",
13106
+ "one or more portal or inline-chart write steps executed before a later write step failed",
12564
13107
  )
12565
13108
  if transport_error is not None:
12566
13109
  for key in ("backend_code", "http_status", "request_id"):
@@ -12581,6 +13124,7 @@ class AiBuilderFacade:
12581
13124
  if api_error is not None
12582
13125
  else {"message": str(error)}
12583
13126
  ),
13127
+ **({"inline_chart_results": deepcopy(inline_chart_results)} if inline_chart_results else {}),
12584
13128
  },
12585
13129
  "request_id": api_error.request_id if api_error else None,
12586
13130
  "backend_code": api_error.backend_code if api_error else None,
@@ -12596,6 +13140,7 @@ class AiBuilderFacade:
12596
13140
  "published": False,
12597
13141
  "publish_failed": False,
12598
13142
  "write_incomplete": True,
13143
+ "inline_charts_verified": False if inline_chart_results else None,
12599
13144
  "readback_unavailable": False,
12600
13145
  "metadata_unverified": True,
12601
13146
  },
@@ -12607,6 +13152,7 @@ class AiBuilderFacade:
12607
13152
  "verified": False,
12608
13153
  "write_executed": True,
12609
13154
  "safe_to_retry": False,
13155
+ "inline_chart_results": deepcopy(inline_chart_results),
12610
13156
  })
12611
13157
  return _failed(
12612
13158
  "PORTAL_APPLY_FAILED",
@@ -12718,6 +13264,7 @@ class AiBuilderFacade:
12718
13264
  "draft": draft_meta_mismatches,
12719
13265
  "live": live_meta_mismatches,
12720
13266
  },
13267
+ **({"inline_chart_results": deepcopy(inline_chart_results)} if inline_chart_results else {}),
12721
13268
  **({"publish_error": publish_error} if publish_error is not None else {}),
12722
13269
  **({"draft_readback_error": draft_readback_error} if draft_readback_error is not None else {}),
12723
13270
  **({"live_readback_error": live_readback_error} if live_readback_error is not None else {}),
@@ -12755,6 +13302,7 @@ class AiBuilderFacade:
12755
13302
  "noop": False,
12756
13303
  "warnings": warnings,
12757
13304
  "layout_diagnostics": layout_diagnostics,
13305
+ "inline_chart_results": deepcopy(inline_chart_results),
12758
13306
  "verification": {
12759
13307
  "draft_verified": draft_verified,
12760
13308
  "draft_metadata_verified": draft_meta_verified,
@@ -12762,6 +13310,7 @@ class AiBuilderFacade:
12762
13310
  "live_metadata_verified": live_meta_verified,
12763
13311
  "published": published,
12764
13312
  "publish_failed": publish_failed,
13313
+ "inline_charts_verified": all(str(item.get("status") or "") in {"created", "updated"} and str(item.get("chart_id") or "").strip() for item in inline_chart_results),
12765
13314
  "readback_unavailable": draft_readback_error is not None or live_readback_error is not None,
12766
13315
  "metadata_unverified": draft_readback_error is not None or live_readback_error is not None,
12767
13316
  },
@@ -13515,9 +14064,11 @@ class AiBuilderFacade:
13515
14064
  profile: str,
13516
14065
  sections: list[PortalSectionPatch],
13517
14066
  layout_preset: str | None = None,
13518
- ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
14067
+ inline_chart_results: list[dict[str, Any]] | None = None,
14068
+ ) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]:
13519
14069
  resolved_components: list[dict[str, Any]] = []
13520
14070
  layout_metadata: list[dict[str, Any]] = []
14071
+ inline_chart_results = inline_chart_results if inline_chart_results is not None else []
13521
14072
  pc_x = 0
13522
14073
  pc_y = 0
13523
14074
  pc_row_height = 0
@@ -13544,11 +14095,53 @@ class AiBuilderFacade:
13544
14095
  dash_style = deepcopy(section.dash_style_config) if isinstance(section.dash_style_config, dict) else None
13545
14096
  component: dict[str, Any]
13546
14097
  if section.source_type == "chart":
13547
- resolved_chart = _resolve_chart_reference(
13548
- charts=self.charts,
13549
- profile=profile,
13550
- ref=section.chart_ref,
13551
- )
14098
+ if section.chart is not None:
14099
+ inline_chart_payload = section.chart.model_dump(mode="json", exclude={"app_key"})
14100
+ chart_request = ChartApplyRequest(
14101
+ app_key=section.chart.app_key,
14102
+ upsert_charts=[ChartUpsertPatch.model_validate(inline_chart_payload)],
14103
+ patch_charts=[],
14104
+ remove_chart_ids=[],
14105
+ reorder_chart_ids=[],
14106
+ )
14107
+ chart_apply = self.chart_apply(profile=profile, request=chart_request)
14108
+ chart_items = chart_apply.get("chart_results") if isinstance(chart_apply.get("chart_results"), list) else []
14109
+ successful_chart = next(
14110
+ (
14111
+ item
14112
+ for item in chart_items
14113
+ if isinstance(item, dict)
14114
+ and str(item.get("status") or "") in {"created", "updated"}
14115
+ and str(item.get("chart_id") or "").strip()
14116
+ ),
14117
+ None,
14118
+ )
14119
+ inline_chart_results.append({
14120
+ "title": section.title,
14121
+ "app_key": section.chart.app_key,
14122
+ "chart_name": section.chart.name,
14123
+ "chart_type": section.chart.chart_type.value,
14124
+ "status": str(successful_chart.get("status") or "") if isinstance(successful_chart, dict) else "failed",
14125
+ "chart_id": str(successful_chart.get("chart_id") or "") if isinstance(successful_chart, dict) else "",
14126
+ "chart_apply_status": chart_apply.get("status"),
14127
+ "write_executed": bool(chart_apply.get("write_executed")),
14128
+ **({"error_code": chart_apply.get("error_code")} if chart_apply.get("error_code") else {}),
14129
+ **({"message": chart_apply.get("message")} if chart_apply.get("message") else {}),
14130
+ })
14131
+ if not isinstance(successful_chart, dict):
14132
+ raise ValueError(f"inline chart '{section.chart.name}' could not be created or updated for portal section '{section.title}'")
14133
+ resolved_chart = {
14134
+ "chart_id": str(successful_chart.get("chart_id") or "").strip(),
14135
+ "chart_name": str(successful_chart.get("name") or section.chart.name).strip(),
14136
+ "app_key": section.chart.app_key,
14137
+ "chart_type": section.chart.chart_type.value,
14138
+ }
14139
+ else:
14140
+ resolved_chart = _resolve_chart_reference(
14141
+ charts=self.charts,
14142
+ profile=profile,
14143
+ ref=section.chart_ref,
14144
+ )
13552
14145
  chart_type = str(
13553
14146
  resolved_chart.get("chart_type")
13554
14147
  or _public_chart_type_from_backend(section.config.get("chartType") or section.config.get("chart_type"))
@@ -13608,7 +14201,7 @@ class AiBuilderFacade:
13608
14201
  if dash_style is not None:
13609
14202
  component["dashStyleConfigBO"] = dash_style
13610
14203
  resolved_components.append(component)
13611
- return resolved_components, layout_metadata
14204
+ return resolved_components, layout_metadata, inline_chart_results
13612
14205
 
13613
14206
  def _resolve_current_user_identity(self, *, profile: str) -> JSONObject:
13614
14207
  session_profile = self.apps.sessions.get_profile(profile)
@@ -17254,12 +17847,12 @@ def _portal_layout_diagnostics(
17254
17847
  if source_type == "chart" and role in {"metric", "metrics", "indicator", "kpi"} and not is_metric_chart:
17255
17848
  warnings.append(_warning(
17256
17849
  "PORTAL_METRIC_SECTION_CHART_TYPE_MISMATCH",
17257
- "metric portal section must reference a target/indicator chart; create the missing metric chart before assembling the portal",
17850
+ "metric portal section must reference a target/indicator chart; use inline_chart to create the missing metric chart while assembling the portal",
17258
17851
  section_index=index,
17259
17852
  title=title,
17260
17853
  role=role,
17261
17854
  chart_type=chart_type or None,
17262
- fix_hint="Use app_charts_apply with chart_type=target and metric='count(*)' or another metric expression, then reference that chart.",
17855
+ fix_hint="Use section.inline_chart with chart_type=target or indicator and a metric expression, or change role away from metric.",
17263
17856
  ))
17264
17857
  if section is not None and section.position is not None and not bool(getattr(section.position, "mobile_provided", False)):
17265
17858
  warnings.append(_warning(
@@ -17319,7 +17912,7 @@ def _append_portal_standard_count_warnings(
17319
17912
  "standard portal metric area should contain 4-6 metric cards",
17320
17913
  actual_count=metric_count,
17321
17914
  expected_count="4-6",
17322
- fix_hint="Create missing target/indicator charts first, or keep 4 metric cards in one row with pc.cols=6, pc.rows=5.",
17915
+ fix_hint="Use inline_chart for missing target/indicator metric cards, or keep 4 metric cards in one row with pc.cols=6, pc.rows=5.",
17323
17916
  ))
17324
17917
  if (bi_count or require_complete_standard) and not 2 <= bi_count <= 3:
17325
17918
  warnings.append(_warning(