@qingflow-tech/qingflow-app-builder-mcp 1.0.7 → 1.0.9
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 +2 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-builder/SKILL.md +44 -14
- package/skills/qingflow-app-builder/references/gotchas.md +32 -1
- package/skills/qingflow-app-builder/references/match-rules.md +114 -0
- package/skills/qingflow-app-builder/references/tool-selection.md +10 -6
- package/skills/qingflow-app-builder/references/update-views.md +19 -19
- package/src/qingflow_mcp/builder_facade/models.py +205 -11
- package/src/qingflow_mcp/builder_facade/service.py +2303 -159
- package/src/qingflow_mcp/cli/commands/builder.py +8 -0
- package/src/qingflow_mcp/cli/commands/record.py +55 -1
- package/src/qingflow_mcp/public_surface.py +2 -2
- package/src/qingflow_mcp/response_trim.py +14 -0
- package/src/qingflow_mcp/server.py +1 -0
- package/src/qingflow_mcp/server_app_builder.py +13 -2
- package/src/qingflow_mcp/server_app_user.py +1 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +199 -10
|
@@ -44,17 +44,21 @@ from .models import (
|
|
|
44
44
|
AppReadSummaryResponse,
|
|
45
45
|
AppViewsReadResponse,
|
|
46
46
|
AssociatedResourcesApplyRequest,
|
|
47
|
+
AssociatedResourcePartialPatch,
|
|
47
48
|
AssociatedResourceUpsertPatch,
|
|
48
49
|
AssociatedResourceViewConfigPatch,
|
|
49
50
|
ChartApplyRequest,
|
|
51
|
+
ChartPartialPatch,
|
|
50
52
|
ChartUpsertPatch,
|
|
51
53
|
CustomButtonsApplyRequest,
|
|
54
|
+
CustomButtonPartialPatch,
|
|
52
55
|
CustomButtonViewButtonBindingPatch,
|
|
53
56
|
CustomButtonViewConfigPatch,
|
|
54
57
|
CustomButtonMatchRulePatch,
|
|
55
58
|
CustomButtonPatch,
|
|
56
59
|
CustomButtonRemovePatch,
|
|
57
60
|
CustomButtonUpsertPatch,
|
|
61
|
+
FieldMatchMappingPatch,
|
|
58
62
|
FieldPatch,
|
|
59
63
|
FieldRemovePatch,
|
|
60
64
|
FieldSelector,
|
|
@@ -85,6 +89,7 @@ from .models import (
|
|
|
85
89
|
VisibilityPatch,
|
|
86
90
|
ViewAssociatedResourcesPatch,
|
|
87
91
|
ViewButtonBindingPatch,
|
|
92
|
+
ViewPartialPatch,
|
|
88
93
|
ViewUpsertPatch,
|
|
89
94
|
ViewFilterOperator,
|
|
90
95
|
ViewGetResponse,
|
|
@@ -2397,6 +2402,140 @@ class AiBuilderFacade:
|
|
|
2397
2402
|
**button_style_catalog_payload(),
|
|
2398
2403
|
}
|
|
2399
2404
|
|
|
2405
|
+
def _expand_custom_button_partial_patches(
|
|
2406
|
+
self,
|
|
2407
|
+
*,
|
|
2408
|
+
profile: str,
|
|
2409
|
+
app_key: str,
|
|
2410
|
+
existing_by_id: dict[int, dict[str, Any]],
|
|
2411
|
+
existing_by_text: dict[str, list[dict[str, Any]]],
|
|
2412
|
+
patch_buttons: list[CustomButtonPartialPatch],
|
|
2413
|
+
) -> tuple[list[CustomButtonUpsertPatch], list[dict[str, Any]], list[dict[str, Any]]]:
|
|
2414
|
+
expanded: list[CustomButtonUpsertPatch] = []
|
|
2415
|
+
issues: list[dict[str, Any]] = []
|
|
2416
|
+
results: list[dict[str, Any]] = []
|
|
2417
|
+
for index, patch in enumerate(patch_buttons):
|
|
2418
|
+
selector_id = _coerce_positive_int(patch.button_id)
|
|
2419
|
+
selector_text = str(patch.button_text or "").strip()
|
|
2420
|
+
button_id: int | None = None
|
|
2421
|
+
if selector_id is not None:
|
|
2422
|
+
if selector_id not in existing_by_id:
|
|
2423
|
+
issue = {
|
|
2424
|
+
"error_code": "CUSTOM_BUTTON_NOT_FOUND",
|
|
2425
|
+
"reason_path": f"patch_buttons[{index}].button_id",
|
|
2426
|
+
"button_id": selector_id,
|
|
2427
|
+
"message": "button_id does not exist in the current app draft",
|
|
2428
|
+
"next_action": "call app_get and use custom_buttons[].button_id",
|
|
2429
|
+
}
|
|
2430
|
+
issues.append(issue)
|
|
2431
|
+
results.append({"index": index, "status": "failed", **issue})
|
|
2432
|
+
continue
|
|
2433
|
+
button_id = selector_id
|
|
2434
|
+
else:
|
|
2435
|
+
matches = existing_by_text.get(selector_text, [])
|
|
2436
|
+
if len(matches) != 1:
|
|
2437
|
+
issue = {
|
|
2438
|
+
"error_code": "AMBIGUOUS_CUSTOM_BUTTON" if matches else "CUSTOM_BUTTON_NOT_FOUND",
|
|
2439
|
+
"reason_path": f"patch_buttons[{index}].button_text",
|
|
2440
|
+
"button_text": selector_text,
|
|
2441
|
+
"candidate_button_ids": [
|
|
2442
|
+
item.get("button_id")
|
|
2443
|
+
for item in matches
|
|
2444
|
+
if _coerce_positive_int(item.get("button_id")) is not None
|
|
2445
|
+
],
|
|
2446
|
+
"message": "patch_buttons[] must target a single existing button; use button_id when names are duplicated",
|
|
2447
|
+
}
|
|
2448
|
+
issues.append(issue)
|
|
2449
|
+
results.append({"index": index, "status": "failed", **issue})
|
|
2450
|
+
continue
|
|
2451
|
+
button_id = _coerce_positive_int(matches[0].get("button_id"))
|
|
2452
|
+
if button_id is None:
|
|
2453
|
+
issue = {
|
|
2454
|
+
"error_code": "CUSTOM_BUTTON_ID_MISSING",
|
|
2455
|
+
"reason_path": f"patch_buttons[{index}].button_text",
|
|
2456
|
+
"button_text": selector_text,
|
|
2457
|
+
"message": "matched button has no readable button_id",
|
|
2458
|
+
}
|
|
2459
|
+
issues.append(issue)
|
|
2460
|
+
results.append({"index": index, "status": "failed", **issue})
|
|
2461
|
+
continue
|
|
2462
|
+
try:
|
|
2463
|
+
detail_response = self.buttons.custom_button_get(
|
|
2464
|
+
profile=profile,
|
|
2465
|
+
app_key=app_key,
|
|
2466
|
+
button_id=button_id,
|
|
2467
|
+
being_draft=True,
|
|
2468
|
+
include_raw=False,
|
|
2469
|
+
)
|
|
2470
|
+
detail = _normalize_custom_button_detail(detail_response.get("result") if isinstance(detail_response.get("result"), dict) else {})
|
|
2471
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
2472
|
+
api_error = _coerce_api_error(error)
|
|
2473
|
+
issue = {
|
|
2474
|
+
"error_code": "CUSTOM_BUTTON_PATCH_DETAIL_READ_FAILED",
|
|
2475
|
+
"reason_path": f"patch_buttons[{index}]",
|
|
2476
|
+
"button_id": button_id,
|
|
2477
|
+
"message": api_error.message,
|
|
2478
|
+
"transport_error": _transport_error_payload(api_error),
|
|
2479
|
+
}
|
|
2480
|
+
issues.append(issue)
|
|
2481
|
+
results.append({"index": index, "status": "failed", **issue})
|
|
2482
|
+
continue
|
|
2483
|
+
patch_payload = _custom_button_upsert_payload_from_detail(
|
|
2484
|
+
detail,
|
|
2485
|
+
button_id=button_id,
|
|
2486
|
+
client_key=str(patch.client_key or "").strip() or None,
|
|
2487
|
+
)
|
|
2488
|
+
normalized_set, set_issues = _normalize_custom_button_partial_set(patch.set, reason_path=f"patch_buttons[{index}].set")
|
|
2489
|
+
normalized_unset, unset_issues = _normalize_custom_button_partial_unset(patch.unset, reason_path=f"patch_buttons[{index}].unset")
|
|
2490
|
+
if set_issues or unset_issues:
|
|
2491
|
+
patch_issues = [*set_issues, *unset_issues]
|
|
2492
|
+
issues.extend(patch_issues)
|
|
2493
|
+
results.append({"index": index, "status": "failed", "button_id": button_id, "issues": patch_issues})
|
|
2494
|
+
continue
|
|
2495
|
+
for key, value in normalized_set.items():
|
|
2496
|
+
if key in {"trigger_add_data_config", "external_qrobot_config", "trigger_wings_config"} and isinstance(value, dict):
|
|
2497
|
+
if key == "trigger_add_data_config" and _custom_button_add_data_config_has_semantic_inputs(value):
|
|
2498
|
+
existing_config = patch_payload.get(key) if isinstance(patch_payload.get(key), dict) else {}
|
|
2499
|
+
merged_value: dict[str, Any] = {}
|
|
2500
|
+
for preserve_key in ("related_app_key", "related_app_name"):
|
|
2501
|
+
if preserve_key in existing_config:
|
|
2502
|
+
merged_value[preserve_key] = deepcopy(existing_config[preserve_key])
|
|
2503
|
+
_deep_merge_public_config(merged_value, value)
|
|
2504
|
+
else:
|
|
2505
|
+
merged_value = deepcopy(patch_payload.get(key) if isinstance(patch_payload.get(key), dict) else {})
|
|
2506
|
+
_deep_merge_public_config(merged_value, value)
|
|
2507
|
+
if key == "trigger_add_data_config":
|
|
2508
|
+
merged_value = _normalize_custom_button_add_data_config_for_public(merged_value)
|
|
2509
|
+
patch_payload[key] = merged_value
|
|
2510
|
+
else:
|
|
2511
|
+
patch_payload[key] = value
|
|
2512
|
+
for key in normalized_unset:
|
|
2513
|
+
patch_payload.pop(key, None)
|
|
2514
|
+
try:
|
|
2515
|
+
expanded_patch = CustomButtonUpsertPatch.model_validate(patch_payload)
|
|
2516
|
+
except Exception as error:
|
|
2517
|
+
issue = {
|
|
2518
|
+
"error_code": "CUSTOM_BUTTON_PATCH_HYDRATION_FAILED",
|
|
2519
|
+
"reason_path": f"patch_buttons[{index}]",
|
|
2520
|
+
"button_id": button_id,
|
|
2521
|
+
"message": str(error),
|
|
2522
|
+
"hydrated_payload": _compact_dict(patch_payload),
|
|
2523
|
+
}
|
|
2524
|
+
issues.append(issue)
|
|
2525
|
+
results.append({"index": index, "status": "failed", **issue})
|
|
2526
|
+
continue
|
|
2527
|
+
expanded.append(expanded_patch)
|
|
2528
|
+
results.append(
|
|
2529
|
+
{
|
|
2530
|
+
"index": index,
|
|
2531
|
+
"status": "expanded",
|
|
2532
|
+
"button_id": button_id,
|
|
2533
|
+
"set_paths": sorted(normalized_set),
|
|
2534
|
+
"unset_paths": sorted(normalized_unset),
|
|
2535
|
+
}
|
|
2536
|
+
)
|
|
2537
|
+
return expanded, issues, results
|
|
2538
|
+
|
|
2400
2539
|
def app_custom_buttons_apply(self, *, profile: str, request: CustomButtonsApplyRequest) -> JSONObject:
|
|
2401
2540
|
normalized_args = request.model_dump(mode="json")
|
|
2402
2541
|
app_key = request.app_key
|
|
@@ -2437,10 +2576,33 @@ class AiBuilderFacade:
|
|
|
2437
2576
|
if text:
|
|
2438
2577
|
existing_by_text.setdefault(text, []).append(item)
|
|
2439
2578
|
|
|
2579
|
+
upsert_buttons = list(request.upsert_buttons)
|
|
2580
|
+
if request.patch_buttons:
|
|
2581
|
+
expanded_buttons, patch_issues, patch_results = self._expand_custom_button_partial_patches(
|
|
2582
|
+
profile=profile,
|
|
2583
|
+
app_key=app_key,
|
|
2584
|
+
existing_by_id=existing_by_id,
|
|
2585
|
+
existing_by_text=existing_by_text,
|
|
2586
|
+
patch_buttons=request.patch_buttons,
|
|
2587
|
+
)
|
|
2588
|
+
if patch_issues:
|
|
2589
|
+
return finalize(
|
|
2590
|
+
_failed(
|
|
2591
|
+
"CUSTOM_BUTTON_PATCH_HYDRATION_FAILED",
|
|
2592
|
+
"one or more custom button partial patches could not be hydrated; no write was executed",
|
|
2593
|
+
normalized_args=normalized_args,
|
|
2594
|
+
details={"patch_results": patch_results, "blocking_issues": patch_issues},
|
|
2595
|
+
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
2596
|
+
)
|
|
2597
|
+
)
|
|
2598
|
+
upsert_buttons.extend(expanded_buttons)
|
|
2599
|
+
normalized_args["upsert_buttons"] = [patch.model_dump(mode="json") for patch in upsert_buttons]
|
|
2600
|
+
normalized_args["patch_results"] = patch_results
|
|
2601
|
+
|
|
2440
2602
|
compiled_add_data_configs, add_data_issues = self._compile_custom_button_semantic_add_data_configs(
|
|
2441
2603
|
profile=profile,
|
|
2442
2604
|
app_key=app_key,
|
|
2443
|
-
patches=
|
|
2605
|
+
patches=upsert_buttons,
|
|
2444
2606
|
)
|
|
2445
2607
|
upsert_ops: list[dict[str, Any]] = []
|
|
2446
2608
|
remove_ops: list[dict[str, Any]] = []
|
|
@@ -2449,7 +2611,7 @@ class AiBuilderFacade:
|
|
|
2449
2611
|
touched_existing_ids: set[int] = set()
|
|
2450
2612
|
used_client_keys: set[str] = set()
|
|
2451
2613
|
|
|
2452
|
-
for index, patch in enumerate(
|
|
2614
|
+
for index, patch in enumerate(upsert_buttons):
|
|
2453
2615
|
patch_payload = patch.model_dump(mode="json", exclude_none=True)
|
|
2454
2616
|
client_key = str(patch.client_key or "").strip()
|
|
2455
2617
|
if client_key:
|
|
@@ -2832,6 +2994,10 @@ class AiBuilderFacade:
|
|
|
2832
2994
|
"edit_version_no": edit_version_no,
|
|
2833
2995
|
"button_ids_by_client_key": client_key_map,
|
|
2834
2996
|
"readback_failed": readback_failed,
|
|
2997
|
+
"compiled_match_rules": {
|
|
2998
|
+
str(index): _summarize_compiled_match_rules(config.get("que_relation") or [])
|
|
2999
|
+
for index, config in compiled_add_data_configs.items()
|
|
3000
|
+
},
|
|
2835
3001
|
},
|
|
2836
3002
|
"request_id": None,
|
|
2837
3003
|
"suggested_next_call": None if verified else {"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
@@ -3234,6 +3400,241 @@ class AiBuilderFacade:
|
|
|
3234
3400
|
)
|
|
3235
3401
|
)
|
|
3236
3402
|
|
|
3403
|
+
def _expand_associated_resource_partial_patches(
|
|
3404
|
+
self,
|
|
3405
|
+
*,
|
|
3406
|
+
existing_by_id: dict[int, dict[str, Any]],
|
|
3407
|
+
patch_resources: list[AssociatedResourcePartialPatch],
|
|
3408
|
+
) -> tuple[list[AssociatedResourceUpsertPatch], list[dict[str, Any]], list[dict[str, Any]]]:
|
|
3409
|
+
expanded: list[AssociatedResourceUpsertPatch] = []
|
|
3410
|
+
issues: list[dict[str, Any]] = []
|
|
3411
|
+
results: list[dict[str, Any]] = []
|
|
3412
|
+
for index, patch in enumerate(patch_resources):
|
|
3413
|
+
item_id = _coerce_positive_int(patch.associated_item_id)
|
|
3414
|
+
if item_id is None or item_id not in existing_by_id:
|
|
3415
|
+
issue = _associated_resource_not_found_issue(f"patch_resources[{index}].associated_item_id", int(patch.associated_item_id), existing_by_id)
|
|
3416
|
+
issues.append(issue)
|
|
3417
|
+
results.append({"index": index, "status": "failed", **issue})
|
|
3418
|
+
continue
|
|
3419
|
+
patch_payload = _associated_resource_upsert_payload_from_existing_item(
|
|
3420
|
+
existing_by_id[item_id],
|
|
3421
|
+
associated_item_id=item_id,
|
|
3422
|
+
client_key=str(patch.client_key or "").strip() or None,
|
|
3423
|
+
)
|
|
3424
|
+
normalized_set, set_issues = _normalize_associated_resource_partial_set(patch.set, reason_path=f"patch_resources[{index}].set")
|
|
3425
|
+
normalized_unset, unset_issues = _normalize_associated_resource_partial_unset(patch.unset, reason_path=f"patch_resources[{index}].unset")
|
|
3426
|
+
if set_issues or unset_issues:
|
|
3427
|
+
patch_issues = [*set_issues, *unset_issues]
|
|
3428
|
+
issues.extend(patch_issues)
|
|
3429
|
+
results.append({"index": index, "status": "failed", "associated_item_id": item_id, "issues": patch_issues})
|
|
3430
|
+
continue
|
|
3431
|
+
for key, value in normalized_set.items():
|
|
3432
|
+
patch_payload[key] = value
|
|
3433
|
+
if key == "match_mappings":
|
|
3434
|
+
patch_payload["match_rules"] = []
|
|
3435
|
+
elif key == "match_rules":
|
|
3436
|
+
patch_payload["match_mappings"] = []
|
|
3437
|
+
for key in normalized_unset:
|
|
3438
|
+
if key == "match_rules":
|
|
3439
|
+
patch_payload["match_rules"] = []
|
|
3440
|
+
patch_payload["match_mappings"] = []
|
|
3441
|
+
if key == "match_mappings":
|
|
3442
|
+
patch_payload["match_rules"] = []
|
|
3443
|
+
patch_payload["match_mappings"] = []
|
|
3444
|
+
try:
|
|
3445
|
+
expanded_patch = AssociatedResourceUpsertPatch.model_validate(patch_payload)
|
|
3446
|
+
except Exception as error:
|
|
3447
|
+
issue = {
|
|
3448
|
+
"error_code": "ASSOCIATED_RESOURCE_PATCH_HYDRATION_FAILED",
|
|
3449
|
+
"reason_path": f"patch_resources[{index}]",
|
|
3450
|
+
"associated_item_id": item_id,
|
|
3451
|
+
"message": str(error),
|
|
3452
|
+
"hydrated_payload": _compact_dict(patch_payload),
|
|
3453
|
+
}
|
|
3454
|
+
issues.append(issue)
|
|
3455
|
+
results.append({"index": index, "status": "failed", **issue})
|
|
3456
|
+
continue
|
|
3457
|
+
validation_issue = _validate_associated_resource_patch(expanded_patch, reason_path=f"patch_resources[{index}]")
|
|
3458
|
+
if validation_issue is not None:
|
|
3459
|
+
issues.append(validation_issue)
|
|
3460
|
+
results.append({"index": index, "status": "failed", "associated_item_id": item_id, "issues": [validation_issue]})
|
|
3461
|
+
continue
|
|
3462
|
+
expanded.append(expanded_patch)
|
|
3463
|
+
results.append(
|
|
3464
|
+
{
|
|
3465
|
+
"index": index,
|
|
3466
|
+
"status": "expanded",
|
|
3467
|
+
"associated_item_id": item_id,
|
|
3468
|
+
"set_paths": sorted(normalized_set),
|
|
3469
|
+
"unset_paths": sorted(normalized_unset),
|
|
3470
|
+
}
|
|
3471
|
+
)
|
|
3472
|
+
return expanded, issues, results
|
|
3473
|
+
|
|
3474
|
+
def _compile_associated_resource_semantic_match_mappings(
|
|
3475
|
+
self,
|
|
3476
|
+
*,
|
|
3477
|
+
profile: str,
|
|
3478
|
+
app_key: str,
|
|
3479
|
+
patches: list[AssociatedResourceUpsertPatch],
|
|
3480
|
+
) -> tuple[dict[int, list[dict[str, Any]]], list[dict[str, Any]]]:
|
|
3481
|
+
compiled_by_index: dict[int, list[dict[str, Any]]] = {}
|
|
3482
|
+
issues: list[dict[str, Any]] = []
|
|
3483
|
+
semantic_patches = [
|
|
3484
|
+
(index, patch)
|
|
3485
|
+
for index, patch in enumerate(patches)
|
|
3486
|
+
if patch.match_mappings or (patch.match_rules and patch.match_mappings)
|
|
3487
|
+
]
|
|
3488
|
+
if not semantic_patches:
|
|
3489
|
+
return compiled_by_index, issues
|
|
3490
|
+
try:
|
|
3491
|
+
source_schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
3492
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
3493
|
+
api_error = _coerce_api_error(error)
|
|
3494
|
+
return {}, [
|
|
3495
|
+
{
|
|
3496
|
+
"error_code": "ASSOCIATED_RESOURCE_SOURCE_SCHEMA_READ_FAILED",
|
|
3497
|
+
"reason_path": "upsert_resources[].match_mappings",
|
|
3498
|
+
"message": api_error.message,
|
|
3499
|
+
"transport_error": _transport_error_payload(api_error),
|
|
3500
|
+
"next_action": "retry after app schema is readable",
|
|
3501
|
+
}
|
|
3502
|
+
]
|
|
3503
|
+
source_fields = list(_parse_schema(source_schema).get("fields") or [])
|
|
3504
|
+
target_schema_cache: dict[str, list[dict[str, Any]]] = {}
|
|
3505
|
+
for index, patch in semantic_patches:
|
|
3506
|
+
reason_base = f"upsert_resources[{index}].match_mappings"
|
|
3507
|
+
if patch.match_mappings and patch.match_rules:
|
|
3508
|
+
issues.append(
|
|
3509
|
+
{
|
|
3510
|
+
"_patch_index": index,
|
|
3511
|
+
"error_code": "MIXED_ASSOCIATED_RESOURCE_MAPPING_MODES",
|
|
3512
|
+
"reason_path": f"upsert_resources[{index}]",
|
|
3513
|
+
"message": "match_mappings cannot be used together with raw match_rules",
|
|
3514
|
+
"next_action": "use semantic match_mappings only, or pass legacy match_rules only",
|
|
3515
|
+
}
|
|
3516
|
+
)
|
|
3517
|
+
continue
|
|
3518
|
+
if not patch.match_mappings:
|
|
3519
|
+
continue
|
|
3520
|
+
target_app_key = str(patch.target_app_key or "").strip()
|
|
3521
|
+
if not target_app_key:
|
|
3522
|
+
issues.append(
|
|
3523
|
+
{
|
|
3524
|
+
"_patch_index": index,
|
|
3525
|
+
"error_code": "ASSOCIATED_RESOURCE_TARGET_APP_REQUIRED",
|
|
3526
|
+
"reason_path": f"upsert_resources[{index}].target_app_key",
|
|
3527
|
+
"message": "match_mappings require target_app_key",
|
|
3528
|
+
}
|
|
3529
|
+
)
|
|
3530
|
+
continue
|
|
3531
|
+
if target_app_key not in target_schema_cache:
|
|
3532
|
+
try:
|
|
3533
|
+
target_schema, _target_schema_source = self._read_schema_with_fallback(profile=profile, app_key=target_app_key)
|
|
3534
|
+
target_schema_cache[target_app_key] = list(_parse_schema(target_schema).get("fields") or [])
|
|
3535
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
3536
|
+
api_error = _coerce_api_error(error)
|
|
3537
|
+
issues.append(
|
|
3538
|
+
{
|
|
3539
|
+
"_patch_index": index,
|
|
3540
|
+
"error_code": "ASSOCIATED_RESOURCE_TARGET_SCHEMA_READ_FAILED",
|
|
3541
|
+
"reason_path": f"upsert_resources[{index}].target_app_key",
|
|
3542
|
+
"target_app_key": target_app_key,
|
|
3543
|
+
"message": api_error.message,
|
|
3544
|
+
"transport_error": _transport_error_payload(api_error),
|
|
3545
|
+
"next_action": "verify target_app_key with app_get",
|
|
3546
|
+
}
|
|
3547
|
+
)
|
|
3548
|
+
continue
|
|
3549
|
+
rules, mapping_issues = self._compile_field_match_mappings(
|
|
3550
|
+
profile=profile,
|
|
3551
|
+
source_app_key=app_key,
|
|
3552
|
+
source_fields=source_fields,
|
|
3553
|
+
target_fields=target_schema_cache[target_app_key],
|
|
3554
|
+
mappings=patch.match_mappings,
|
|
3555
|
+
reason_path=reason_base,
|
|
3556
|
+
type_error_code="ASSOCIATED_RESOURCE_MAPPING_TYPE_MISMATCH",
|
|
3557
|
+
context_label="associated resource match mapping",
|
|
3558
|
+
)
|
|
3559
|
+
if mapping_issues:
|
|
3560
|
+
for issue in mapping_issues:
|
|
3561
|
+
issue["_patch_index"] = index
|
|
3562
|
+
issues.extend(mapping_issues)
|
|
3563
|
+
continue
|
|
3564
|
+
compiled_by_index[index] = rules
|
|
3565
|
+
return compiled_by_index, issues
|
|
3566
|
+
|
|
3567
|
+
def _compile_field_match_mappings(
|
|
3568
|
+
self,
|
|
3569
|
+
*,
|
|
3570
|
+
profile: str,
|
|
3571
|
+
source_app_key: str,
|
|
3572
|
+
source_fields: list[dict[str, Any]],
|
|
3573
|
+
target_fields: list[dict[str, Any]],
|
|
3574
|
+
mappings: list[FieldMatchMappingPatch],
|
|
3575
|
+
reason_path: str,
|
|
3576
|
+
type_error_code: str,
|
|
3577
|
+
context_label: str,
|
|
3578
|
+
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
3579
|
+
issues: list[dict[str, Any]] = []
|
|
3580
|
+
rules: list[dict[str, Any]] = []
|
|
3581
|
+
for mapping_index, mapping in enumerate(mappings):
|
|
3582
|
+
target_field, target_issue = _resolve_custom_button_schema_field(
|
|
3583
|
+
fields=target_fields,
|
|
3584
|
+
selector=mapping.target_field,
|
|
3585
|
+
reason_path=f"{reason_path}[{mapping_index}].target_field",
|
|
3586
|
+
role="match_target",
|
|
3587
|
+
)
|
|
3588
|
+
if target_issue or target_field is None:
|
|
3589
|
+
if target_issue:
|
|
3590
|
+
issues.append(_retag_associated_resource_match_field_issue(target_issue))
|
|
3591
|
+
continue
|
|
3592
|
+
judge_type, operator_issue = _match_mapping_operator_to_judge_type(
|
|
3593
|
+
mapping.operator,
|
|
3594
|
+
reason_path=f"{reason_path}[{mapping_index}].operator",
|
|
3595
|
+
)
|
|
3596
|
+
if operator_issue:
|
|
3597
|
+
issues.append(operator_issue)
|
|
3598
|
+
continue
|
|
3599
|
+
if mapping.source_field is not None:
|
|
3600
|
+
source_field, source_issue = _resolve_custom_button_schema_field(
|
|
3601
|
+
fields=source_fields,
|
|
3602
|
+
selector=mapping.source_field,
|
|
3603
|
+
reason_path=f"{reason_path}[{mapping_index}].source_field",
|
|
3604
|
+
role="match_source",
|
|
3605
|
+
)
|
|
3606
|
+
if source_issue or source_field is None:
|
|
3607
|
+
if source_issue:
|
|
3608
|
+
issues.append(_retag_associated_resource_match_field_issue(source_issue))
|
|
3609
|
+
continue
|
|
3610
|
+
type_issue = _custom_button_mapping_type_issue(
|
|
3611
|
+
source_field=source_field,
|
|
3612
|
+
target_field=target_field,
|
|
3613
|
+
reason_path=f"{reason_path}[{mapping_index}]",
|
|
3614
|
+
source_app_key=source_app_key,
|
|
3615
|
+
error_code=type_error_code,
|
|
3616
|
+
context_label=context_label,
|
|
3617
|
+
)
|
|
3618
|
+
if type_issue:
|
|
3619
|
+
issues.append(type_issue)
|
|
3620
|
+
continue
|
|
3621
|
+
rule = _custom_button_field_mapping_rule(source_field=source_field, target_field=target_field)
|
|
3622
|
+
rule["judge_type"] = judge_type
|
|
3623
|
+
rules.append(rule)
|
|
3624
|
+
continue
|
|
3625
|
+
rule, value_issue = self._custom_button_default_value_rule(
|
|
3626
|
+
profile=profile,
|
|
3627
|
+
target_field=target_field,
|
|
3628
|
+
value=mapping.value,
|
|
3629
|
+
reason_path=f"{reason_path}[{mapping_index}].value",
|
|
3630
|
+
)
|
|
3631
|
+
if value_issue:
|
|
3632
|
+
issues.append(_retag_associated_resource_static_value_issue(value_issue))
|
|
3633
|
+
continue
|
|
3634
|
+
rule["judge_type"] = judge_type
|
|
3635
|
+
rules.append(rule)
|
|
3636
|
+
return rules, issues
|
|
3637
|
+
|
|
3237
3638
|
def app_associated_resources_apply(self, *, profile: str, request: AssociatedResourcesApplyRequest) -> JSONObject:
|
|
3238
3639
|
normalized_args = request.model_dump(mode="json", exclude_none=True)
|
|
3239
3640
|
app_key = request.app_key
|
|
@@ -3252,7 +3653,7 @@ class AiBuilderFacade:
|
|
|
3252
3653
|
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
3253
3654
|
|
|
3254
3655
|
try:
|
|
3255
|
-
existing_resources = self._load_associated_resources_for_builder(profile=profile, app_key=app_key)
|
|
3656
|
+
existing_resources = self._load_associated_resources_for_builder(profile=profile, app_key=app_key, include_raw=True)
|
|
3256
3657
|
except (QingflowApiError, RuntimeError) as error:
|
|
3257
3658
|
api_error = _coerce_api_error(error)
|
|
3258
3659
|
return finalize(_failed_from_api_error(
|
|
@@ -3270,8 +3671,45 @@ class AiBuilderFacade:
|
|
|
3270
3671
|
client_key_to_patch: dict[str, AssociatedResourceUpsertPatch] = {}
|
|
3271
3672
|
client_key_to_id: dict[str, int] = {}
|
|
3272
3673
|
used_client_keys: set[str] = set()
|
|
3674
|
+
force_update_resource_ids: set[int] = set()
|
|
3675
|
+
resolved_associated_item_refs: dict[str, list[int]] = {}
|
|
3676
|
+
|
|
3677
|
+
upsert_resources = list(request.upsert_resources)
|
|
3678
|
+
if request.patch_resources:
|
|
3679
|
+
expanded_resources, patch_issues, patch_results = self._expand_associated_resource_partial_patches(
|
|
3680
|
+
existing_by_id=existing_by_id,
|
|
3681
|
+
patch_resources=request.patch_resources,
|
|
3682
|
+
)
|
|
3683
|
+
if patch_issues:
|
|
3684
|
+
return finalize(
|
|
3685
|
+
_failed(
|
|
3686
|
+
"ASSOCIATED_RESOURCE_PATCH_HYDRATION_FAILED",
|
|
3687
|
+
"one or more associated resource partial patches could not be hydrated; no write was executed",
|
|
3688
|
+
normalized_args=normalized_args,
|
|
3689
|
+
details={"patch_results": patch_results, "blocking_issues": patch_issues},
|
|
3690
|
+
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
3691
|
+
)
|
|
3692
|
+
)
|
|
3693
|
+
upsert_resources.extend(expanded_resources)
|
|
3694
|
+
force_update_resource_ids.update(
|
|
3695
|
+
item_id
|
|
3696
|
+
for item_id in (_coerce_positive_int(patch.associated_item_id) for patch in expanded_resources)
|
|
3697
|
+
if item_id is not None
|
|
3698
|
+
)
|
|
3699
|
+
normalized_args["upsert_resources"] = [patch.model_dump(mode="json") for patch in upsert_resources]
|
|
3700
|
+
normalized_args["patch_results"] = patch_results
|
|
3701
|
+
|
|
3702
|
+
compiled_resource_match_rules, resource_match_issues = self._compile_associated_resource_semantic_match_mappings(
|
|
3703
|
+
profile=profile,
|
|
3704
|
+
app_key=app_key,
|
|
3705
|
+
patches=upsert_resources,
|
|
3706
|
+
)
|
|
3707
|
+
normalized_args["compiled_match_rules"] = {
|
|
3708
|
+
str(index): _summarize_compiled_match_rules(rules)
|
|
3709
|
+
for index, rules in compiled_resource_match_rules.items()
|
|
3710
|
+
}
|
|
3273
3711
|
|
|
3274
|
-
for index, patch in enumerate(
|
|
3712
|
+
for index, patch in enumerate(upsert_resources):
|
|
3275
3713
|
client_key = str(patch.client_key or "").strip()
|
|
3276
3714
|
if client_key:
|
|
3277
3715
|
if client_key in used_client_keys:
|
|
@@ -3282,6 +3720,8 @@ class AiBuilderFacade:
|
|
|
3282
3720
|
if validation_issue is not None:
|
|
3283
3721
|
blocking_issues.append(validation_issue)
|
|
3284
3722
|
continue
|
|
3723
|
+
if index in {issue.get("_patch_index") for issue in resource_match_issues if isinstance(issue, dict)}:
|
|
3724
|
+
continue
|
|
3285
3725
|
associated_item_id = _coerce_positive_int(patch.associated_item_id)
|
|
3286
3726
|
if associated_item_id is not None:
|
|
3287
3727
|
if associated_item_id not in existing_by_id:
|
|
@@ -3293,7 +3733,14 @@ class AiBuilderFacade:
|
|
|
3293
3733
|
touched_ids.add(associated_item_id)
|
|
3294
3734
|
if client_key:
|
|
3295
3735
|
client_key_to_id[client_key] = associated_item_id
|
|
3296
|
-
operation =
|
|
3736
|
+
operation = (
|
|
3737
|
+
"update"
|
|
3738
|
+
if associated_item_id in force_update_resource_ids
|
|
3739
|
+
or _associated_resource_patch_has_match_config(patch)
|
|
3740
|
+
else "unchanged"
|
|
3741
|
+
if _associated_resource_matches_patch(existing_by_id[associated_item_id], patch)
|
|
3742
|
+
else "update"
|
|
3743
|
+
)
|
|
3297
3744
|
upsert_ops.append({"operation": operation, "associated_item_id": associated_item_id, "patch": patch, "index": index})
|
|
3298
3745
|
continue
|
|
3299
3746
|
matches = [
|
|
@@ -3327,38 +3774,40 @@ class AiBuilderFacade:
|
|
|
3327
3774
|
touched_ids.add(matched_id)
|
|
3328
3775
|
if client_key:
|
|
3329
3776
|
client_key_to_id[client_key] = matched_id
|
|
3330
|
-
|
|
3777
|
+
operation = "update" if _associated_resource_patch_has_match_config(patch) else "unchanged"
|
|
3778
|
+
upsert_ops.append({"operation": operation, "associated_item_id": matched_id, "patch": patch, "index": index})
|
|
3331
3779
|
else:
|
|
3332
3780
|
upsert_ops.append({"operation": "create", "associated_item_id": None, "patch": patch, "index": index})
|
|
3333
3781
|
|
|
3334
|
-
remove_ids = [
|
|
3335
|
-
item_id
|
|
3336
|
-
for item_id in (_coerce_positive_int(raw_id) for raw_id in request.remove_associated_item_ids)
|
|
3337
|
-
if item_id is not None
|
|
3338
|
-
]
|
|
3782
|
+
remove_ids: list[int] = []
|
|
3339
3783
|
for raw_id in request.remove_associated_item_ids:
|
|
3340
|
-
item_id =
|
|
3784
|
+
item_id, issue = _resolve_associated_resource_selector(
|
|
3785
|
+
raw_id,
|
|
3786
|
+
existing_resources=existing_resources,
|
|
3787
|
+
existing_by_id=existing_by_id,
|
|
3788
|
+
reason_path="remove_associated_item_ids",
|
|
3789
|
+
)
|
|
3341
3790
|
if item_id is None:
|
|
3342
|
-
blocking_issues.append({"error_code": "ASSOCIATED_RESOURCE_NOT_FOUND", "reason_path": "remove_associated_item_ids", "received": raw_id})
|
|
3791
|
+
blocking_issues.append(issue or {"error_code": "ASSOCIATED_RESOURCE_NOT_FOUND", "reason_path": "remove_associated_item_ids", "received": raw_id})
|
|
3343
3792
|
continue
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
elif item_id in touched_ids:
|
|
3793
|
+
remove_ids.append(item_id)
|
|
3794
|
+
if item_id in touched_ids:
|
|
3347
3795
|
blocking_issues.append(_duplicate_associated_resource_issue("remove_associated_item_ids", item_id))
|
|
3348
3796
|
else:
|
|
3349
3797
|
touched_ids.add(item_id)
|
|
3350
3798
|
|
|
3351
|
-
reorder_ids = [
|
|
3352
|
-
item_id
|
|
3353
|
-
for item_id in (_coerce_positive_int(raw_id) for raw_id in request.reorder_associated_item_ids)
|
|
3354
|
-
if item_id is not None
|
|
3355
|
-
]
|
|
3799
|
+
reorder_ids: list[int] = []
|
|
3356
3800
|
for raw_id in request.reorder_associated_item_ids:
|
|
3357
|
-
item_id =
|
|
3801
|
+
item_id, issue = _resolve_associated_resource_selector(
|
|
3802
|
+
raw_id,
|
|
3803
|
+
existing_resources=existing_resources,
|
|
3804
|
+
existing_by_id=existing_by_id,
|
|
3805
|
+
reason_path="reorder_associated_item_ids",
|
|
3806
|
+
)
|
|
3358
3807
|
if item_id is None:
|
|
3359
|
-
blocking_issues.append({"error_code": "ASSOCIATED_RESOURCE_NOT_FOUND", "reason_path": "reorder_associated_item_ids", "received": raw_id})
|
|
3360
|
-
|
|
3361
|
-
|
|
3808
|
+
blocking_issues.append(issue or {"error_code": "ASSOCIATED_RESOURCE_NOT_FOUND", "reason_path": "reorder_associated_item_ids", "received": raw_id})
|
|
3809
|
+
continue
|
|
3810
|
+
reorder_ids.append(item_id)
|
|
3362
3811
|
|
|
3363
3812
|
for index, view_config in enumerate(request.view_configs):
|
|
3364
3813
|
refs = [str(ref or "").strip() for ref in view_config.associated_item_refs if str(ref or "").strip()]
|
|
@@ -3372,21 +3821,26 @@ class AiBuilderFacade:
|
|
|
3372
3821
|
"message": "associated_item_refs must reference client_key values from upsert_resources in the same apply call",
|
|
3373
3822
|
}
|
|
3374
3823
|
)
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
{
|
|
3383
|
-
"error_code": "ASSOCIATED_RESOURCE_NOT_FOUND",
|
|
3384
|
-
"reason_path": f"view_configs[{index}].associated_item_ids",
|
|
3385
|
-
"invalid_associated_item_ids": invalid_ids,
|
|
3386
|
-
"available_associated_item_ids": sorted(existing_by_id),
|
|
3387
|
-
"message": "view_configs.associated_item_ids must use app-level associated_item_id values from app_get",
|
|
3388
|
-
}
|
|
3824
|
+
resolved_ids: list[int] = []
|
|
3825
|
+
for raw_id in view_config.associated_item_ids:
|
|
3826
|
+
item_id, issue = _resolve_associated_resource_selector(
|
|
3827
|
+
raw_id,
|
|
3828
|
+
existing_resources=existing_resources,
|
|
3829
|
+
existing_by_id=existing_by_id,
|
|
3830
|
+
reason_path=f"view_configs[{index}].associated_item_ids",
|
|
3389
3831
|
)
|
|
3832
|
+
if item_id is None:
|
|
3833
|
+
blocking_issues.append(
|
|
3834
|
+
issue
|
|
3835
|
+
or {
|
|
3836
|
+
"error_code": "ASSOCIATED_RESOURCE_NOT_FOUND",
|
|
3837
|
+
"reason_path": f"view_configs[{index}].associated_item_ids",
|
|
3838
|
+
"received": raw_id,
|
|
3839
|
+
}
|
|
3840
|
+
)
|
|
3841
|
+
continue
|
|
3842
|
+
resolved_ids.append(item_id)
|
|
3843
|
+
resolved_associated_item_refs[f"view_configs[{index}].associated_item_ids"] = resolved_ids
|
|
3390
3844
|
raw_limit_type = str(view_config.limit_type or ("all" if view_config.visible else "")).strip().lower()
|
|
3391
3845
|
if view_config.visible and raw_limit_type and raw_limit_type not in {"all", "select"}:
|
|
3392
3846
|
blocking_issues.append(
|
|
@@ -3398,6 +3852,15 @@ class AiBuilderFacade:
|
|
|
3398
3852
|
}
|
|
3399
3853
|
)
|
|
3400
3854
|
|
|
3855
|
+
blocking_issues.extend(
|
|
3856
|
+
[
|
|
3857
|
+
{key: value for key, value in issue.items() if key != "_patch_index"}
|
|
3858
|
+
for issue in resource_match_issues
|
|
3859
|
+
]
|
|
3860
|
+
)
|
|
3861
|
+
if resolved_associated_item_refs:
|
|
3862
|
+
normalized_args["resolved_associated_item_refs"] = deepcopy(resolved_associated_item_refs)
|
|
3863
|
+
|
|
3401
3864
|
if blocking_issues:
|
|
3402
3865
|
return finalize(
|
|
3403
3866
|
_failed(
|
|
@@ -3453,7 +3916,7 @@ class AiBuilderFacade:
|
|
|
3453
3916
|
"safe_to_retry": True,
|
|
3454
3917
|
"publish_requested": False,
|
|
3455
3918
|
"published": False,
|
|
3456
|
-
"associated_resources": existing_resources,
|
|
3919
|
+
"associated_resources": _strip_internal_associated_resource_raw(existing_resources),
|
|
3457
3920
|
}
|
|
3458
3921
|
return finalize(response)
|
|
3459
3922
|
|
|
@@ -3485,7 +3948,12 @@ class AiBuilderFacade:
|
|
|
3485
3948
|
client_key_to_id[str(patch.client_key)] = item_id
|
|
3486
3949
|
elif op["operation"] == "create":
|
|
3487
3950
|
write_executed = True
|
|
3488
|
-
self._associated_resource_create(
|
|
3951
|
+
self._associated_resource_create(
|
|
3952
|
+
profile=profile,
|
|
3953
|
+
app_key=app_key,
|
|
3954
|
+
patch=patch,
|
|
3955
|
+
match_rules_override=compiled_resource_match_rules.get(int(op["index"])),
|
|
3956
|
+
)
|
|
3489
3957
|
readback_resources = self._load_associated_resources_for_builder(profile=profile, app_key=app_key)
|
|
3490
3958
|
matches = [
|
|
3491
3959
|
item
|
|
@@ -3500,7 +3968,14 @@ class AiBuilderFacade:
|
|
|
3500
3968
|
else:
|
|
3501
3969
|
item_id = int(op["associated_item_id"])
|
|
3502
3970
|
write_executed = True
|
|
3503
|
-
self._associated_resource_update(
|
|
3971
|
+
self._associated_resource_update(
|
|
3972
|
+
profile=profile,
|
|
3973
|
+
app_key=app_key,
|
|
3974
|
+
associated_item_id=item_id,
|
|
3975
|
+
patch=patch,
|
|
3976
|
+
existing_item=existing_by_id.get(item_id),
|
|
3977
|
+
match_rules_override=compiled_resource_match_rules.get(int(op["index"])),
|
|
3978
|
+
)
|
|
3504
3979
|
updated.append(_associated_resource_result_entry("update", op["index"], patch, associated_item_id=item_id))
|
|
3505
3980
|
if patch.client_key:
|
|
3506
3981
|
client_key_to_id[str(patch.client_key)] = item_id
|
|
@@ -3542,12 +4017,13 @@ class AiBuilderFacade:
|
|
|
3542
4017
|
resources_after = []
|
|
3543
4018
|
|
|
3544
4019
|
for index, view_config in enumerate(request.view_configs):
|
|
4020
|
+
resolved_view_config_ids = resolved_associated_item_refs.get(f"view_configs[{index}].associated_item_ids", [])
|
|
3545
4021
|
selected_ids = [
|
|
3546
4022
|
item_id
|
|
3547
4023
|
for item_id in (
|
|
3548
4024
|
_coerce_positive_int(raw_id)
|
|
3549
4025
|
for raw_id in [
|
|
3550
|
-
*
|
|
4026
|
+
*resolved_view_config_ids,
|
|
3551
4027
|
*[client_key_to_id.get(str(ref or "").strip()) for ref in view_config.associated_item_refs],
|
|
3552
4028
|
]
|
|
3553
4029
|
)
|
|
@@ -3641,7 +4117,16 @@ class AiBuilderFacade:
|
|
|
3641
4117
|
"normalized_args": normalized_args,
|
|
3642
4118
|
"missing_fields": [],
|
|
3643
4119
|
"allowed_values": {"graph_type": ["chart", "view"], "report_source": ["app", "dataset"], "view_config.limit_type": ["all", "select"]},
|
|
3644
|
-
"details": {
|
|
4120
|
+
"details": {
|
|
4121
|
+
"edit_version_no": edit_version_no,
|
|
4122
|
+
"associated_item_ids_by_client_key": client_key_to_id,
|
|
4123
|
+
"readback_failed": readback_failed,
|
|
4124
|
+
"compiled_match_rules": {
|
|
4125
|
+
str(index): _summarize_compiled_match_rules(rules)
|
|
4126
|
+
for index, rules in compiled_resource_match_rules.items()
|
|
4127
|
+
},
|
|
4128
|
+
"resolved_associated_item_refs": resolved_associated_item_refs,
|
|
4129
|
+
},
|
|
3645
4130
|
"request_id": None,
|
|
3646
4131
|
"suggested_next_call": None if verified else {"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
3647
4132
|
"noop": False,
|
|
@@ -4835,7 +5320,7 @@ class AiBuilderFacade:
|
|
|
4835
5320
|
fallback_items = self.charts.qingbi_report_list(profile=profile, app_key=app_key).get("items") or []
|
|
4836
5321
|
return list(fallback_items) if isinstance(fallback_items, list) else [], "fallback"
|
|
4837
5322
|
|
|
4838
|
-
def _load_associated_resources_for_builder(self, *, profile: str, app_key: str) -> list[dict[str, Any]]:
|
|
5323
|
+
def _load_associated_resources_for_builder(self, *, profile: str, app_key: str, include_raw: bool = False) -> list[dict[str, Any]]:
|
|
4839
5324
|
def runner(_: Any, context: BackendRequestContext) -> list[dict[str, Any]]:
|
|
4840
5325
|
payload = self.apps.backend.request(
|
|
4841
5326
|
"GET",
|
|
@@ -4843,12 +5328,19 @@ class AiBuilderFacade:
|
|
|
4843
5328
|
f"/app/{app_key}/asosChart",
|
|
4844
5329
|
params={"role": 1, "beingDraft": True},
|
|
4845
5330
|
)
|
|
4846
|
-
return _normalize_associated_resources_payload(payload)
|
|
5331
|
+
return _normalize_associated_resources_payload(payload, include_raw=include_raw)
|
|
4847
5332
|
|
|
4848
5333
|
return self.apps._run(profile, runner)
|
|
4849
5334
|
|
|
4850
|
-
def _associated_resource_create(
|
|
4851
|
-
|
|
5335
|
+
def _associated_resource_create(
|
|
5336
|
+
self,
|
|
5337
|
+
*,
|
|
5338
|
+
profile: str,
|
|
5339
|
+
app_key: str,
|
|
5340
|
+
patch: AssociatedResourceUpsertPatch,
|
|
5341
|
+
match_rules_override: list[dict[str, Any]] | None = None,
|
|
5342
|
+
) -> JSONObject:
|
|
5343
|
+
payload = _serialize_associated_resource_create_payload(patch, match_rules_override=match_rules_override)
|
|
4852
5344
|
|
|
4853
5345
|
def runner(_: Any, context: BackendRequestContext) -> JSONObject:
|
|
4854
5346
|
result = self.apps.backend.request("POST", context, f"/app/{app_key}/asosChart", json_body=payload)
|
|
@@ -4863,8 +5355,15 @@ class AiBuilderFacade:
|
|
|
4863
5355
|
app_key: str,
|
|
4864
5356
|
associated_item_id: int,
|
|
4865
5357
|
patch: AssociatedResourceUpsertPatch,
|
|
5358
|
+
existing_item: dict[str, Any] | None = None,
|
|
5359
|
+
match_rules_override: list[dict[str, Any]] | None = None,
|
|
4866
5360
|
) -> JSONObject:
|
|
4867
|
-
payload = _serialize_associated_resource_update_payload(
|
|
5361
|
+
payload = _serialize_associated_resource_update_payload(
|
|
5362
|
+
patch,
|
|
5363
|
+
associated_item_id=associated_item_id,
|
|
5364
|
+
existing_item=existing_item,
|
|
5365
|
+
match_rules_override=match_rules_override,
|
|
5366
|
+
)
|
|
4868
5367
|
|
|
4869
5368
|
def runner(_: Any, context: BackendRequestContext) -> JSONObject:
|
|
4870
5369
|
result = self.apps.backend.request("POST", context, f"/app/{app_key}/asosChart/{associated_item_id}", json_body=payload)
|
|
@@ -4984,6 +5483,7 @@ class AiBuilderFacade:
|
|
|
4984
5483
|
continue
|
|
4985
5484
|
compiled_config, config_issues = self._compile_custom_button_add_data_config(
|
|
4986
5485
|
profile=profile,
|
|
5486
|
+
source_app_key=app_key,
|
|
4987
5487
|
source_fields=list(source_fields),
|
|
4988
5488
|
target_fields=target_schema_cache[target_app_key],
|
|
4989
5489
|
target_app_key=target_app_key,
|
|
@@ -5001,6 +5501,7 @@ class AiBuilderFacade:
|
|
|
5001
5501
|
self,
|
|
5002
5502
|
*,
|
|
5003
5503
|
profile: str,
|
|
5504
|
+
source_app_key: str,
|
|
5004
5505
|
source_fields: list[dict[str, Any]],
|
|
5005
5506
|
target_fields: list[dict[str, Any]],
|
|
5006
5507
|
target_app_key: str,
|
|
@@ -5035,6 +5536,9 @@ class AiBuilderFacade:
|
|
|
5035
5536
|
source_field=source_field,
|
|
5036
5537
|
target_field=target_field,
|
|
5037
5538
|
reason_path=f"{reason_path}.field_mappings[{mapping_index}]",
|
|
5539
|
+
source_app_key=source_app_key,
|
|
5540
|
+
error_code="CUSTOM_BUTTON_MAPPING_TYPE_MISMATCH",
|
|
5541
|
+
context_label="addData copy mapping",
|
|
5038
5542
|
)
|
|
5039
5543
|
if type_issue:
|
|
5040
5544
|
issues.append(type_issue)
|
|
@@ -5349,19 +5853,19 @@ class AiBuilderFacade:
|
|
|
5349
5853
|
except (QingflowApiError, RuntimeError) as error:
|
|
5350
5854
|
api_error = _coerce_api_error(error)
|
|
5351
5855
|
has_requested_list_buttons = any(
|
|
5352
|
-
_normalize_view_button_config_type(item.get("configType")) == "
|
|
5856
|
+
_normalize_view_button_config_type(item.get("configType")) == "INSIDE"
|
|
5353
5857
|
for item in new_dtos
|
|
5354
5858
|
if isinstance(item, dict)
|
|
5355
5859
|
)
|
|
5356
5860
|
fallback_dtos = [
|
|
5357
5861
|
item
|
|
5358
5862
|
for item in merged_dtos
|
|
5359
|
-
if _normalize_view_button_config_type(item.get("configType")) != "
|
|
5863
|
+
if _normalize_view_button_config_type(item.get("configType")) != "INSIDE"
|
|
5360
5864
|
]
|
|
5361
5865
|
fallback_new_dtos = [
|
|
5362
5866
|
item
|
|
5363
5867
|
for item in new_dtos
|
|
5364
|
-
if _normalize_view_button_config_type(item.get("configType")) != "
|
|
5868
|
+
if _normalize_view_button_config_type(item.get("configType")) != "INSIDE"
|
|
5365
5869
|
]
|
|
5366
5870
|
if has_requested_list_buttons and fallback_new_dtos and fallback_dtos != merged_dtos:
|
|
5367
5871
|
fallback_payload = _build_view_buttons_only_update_payload(current_config, button_config_dtos=fallback_dtos)
|
|
@@ -5373,12 +5877,12 @@ class AiBuilderFacade:
|
|
|
5373
5877
|
"index": config_index,
|
|
5374
5878
|
"operation": "view_config",
|
|
5375
5879
|
"status": "failed",
|
|
5376
|
-
"error_code": "
|
|
5880
|
+
"error_code": "INSIDE_BUTTON_BACKEND_UNSUPPORTED",
|
|
5377
5881
|
"view_key": view_key,
|
|
5378
|
-
"message": "backend rejected list-button placement; header/detail placements were retried without
|
|
5882
|
+
"message": "backend rejected inside/list-button placement; header/detail placements were retried without inside buttons",
|
|
5379
5883
|
"backend_message": api_error.message,
|
|
5380
5884
|
"transport_error": _transport_error_payload(api_error),
|
|
5381
|
-
"next_action": "use placement=header/detail for now, or verify the backend
|
|
5885
|
+
"next_action": "use placement=header/detail for now, or verify the backend inside-button payload separately",
|
|
5382
5886
|
}
|
|
5383
5887
|
failed.append(unsupported_list_issue)
|
|
5384
5888
|
except (QingflowApiError, RuntimeError) as fallback_error:
|
|
@@ -5402,12 +5906,12 @@ class AiBuilderFacade:
|
|
|
5402
5906
|
"index": config_index,
|
|
5403
5907
|
"operation": "view_config",
|
|
5404
5908
|
"status": "failed",
|
|
5405
|
-
"error_code": "
|
|
5909
|
+
"error_code": "INSIDE_BUTTON_BACKEND_UNSUPPORTED",
|
|
5406
5910
|
"view_key": view_key,
|
|
5407
|
-
"message": "backend rejected list-button placement",
|
|
5911
|
+
"message": "backend rejected inside/list-button placement",
|
|
5408
5912
|
"backend_message": api_error.message,
|
|
5409
5913
|
"transport_error": _transport_error_payload(api_error),
|
|
5410
|
-
"next_action": "use placement=header/detail for now, or verify the backend
|
|
5914
|
+
"next_action": "use placement=header/detail for now, or verify the backend inside-button payload separately",
|
|
5411
5915
|
}
|
|
5412
5916
|
failed.append(issue)
|
|
5413
5917
|
results.append(issue)
|
|
@@ -5648,6 +6152,7 @@ class AiBuilderFacade:
|
|
|
5648
6152
|
config if isinstance(config, dict) else {},
|
|
5649
6153
|
available_resources=associated_resources,
|
|
5650
6154
|
)
|
|
6155
|
+
buttons_config = _extract_view_buttons_config(config if isinstance(config, dict) else {})
|
|
5651
6156
|
|
|
5652
6157
|
response = ViewGetResponse(
|
|
5653
6158
|
view_key=view_key,
|
|
@@ -5656,6 +6161,7 @@ class AiBuilderFacade:
|
|
|
5656
6161
|
config=deepcopy(config) if isinstance(config, dict) else {},
|
|
5657
6162
|
questions=questions,
|
|
5658
6163
|
associations=associations,
|
|
6164
|
+
buttons_config=buttons_config,
|
|
5659
6165
|
associated_resources_config=associated_resources_config,
|
|
5660
6166
|
)
|
|
5661
6167
|
question_entries = _extract_view_question_entries(config.get("viewgraphQuestions"))
|
|
@@ -5682,6 +6188,7 @@ class AiBuilderFacade:
|
|
|
5682
6188
|
"verification": verification,
|
|
5683
6189
|
"verified": all(bool(value) for value in verification.values()),
|
|
5684
6190
|
"query_conditions": query_conditions,
|
|
6191
|
+
"buttons_config": buttons_config,
|
|
5685
6192
|
"associated_resources_config": associated_resources_config,
|
|
5686
6193
|
**response.model_dump(mode="json"),
|
|
5687
6194
|
}
|
|
@@ -6165,6 +6672,173 @@ class AiBuilderFacade:
|
|
|
6165
6672
|
"verification": {"field_count": len(field_names)},
|
|
6166
6673
|
}
|
|
6167
6674
|
|
|
6675
|
+
def _expand_view_partial_patches(
|
|
6676
|
+
self,
|
|
6677
|
+
*,
|
|
6678
|
+
profile: str,
|
|
6679
|
+
app_key: str,
|
|
6680
|
+
schema: dict[str, Any],
|
|
6681
|
+
existing_by_key: dict[str, dict[str, Any]],
|
|
6682
|
+
existing_by_name: dict[str, list[dict[str, Any]]],
|
|
6683
|
+
patch_views: list[ViewPartialPatch],
|
|
6684
|
+
) -> tuple[list[ViewUpsertPatch], list[dict[str, Any]], list[dict[str, Any]]]:
|
|
6685
|
+
parsed_schema = _parse_schema(schema)
|
|
6686
|
+
field_names_by_id = {
|
|
6687
|
+
field_id: str(field.get("name") or "").strip()
|
|
6688
|
+
for field in parsed_schema.get("fields") or []
|
|
6689
|
+
if isinstance(field, dict)
|
|
6690
|
+
and (field_id := _coerce_nonnegative_int(field.get("que_id") or field.get("field_id"))) is not None
|
|
6691
|
+
and str(field.get("name") or "").strip()
|
|
6692
|
+
}
|
|
6693
|
+
expanded: list[ViewUpsertPatch] = []
|
|
6694
|
+
issues: list[dict[str, Any]] = []
|
|
6695
|
+
results: list[dict[str, Any]] = []
|
|
6696
|
+
for index, patch in enumerate(patch_views):
|
|
6697
|
+
view_key = str(patch.view_key or "").strip()
|
|
6698
|
+
name = str(patch.name or "").strip()
|
|
6699
|
+
matched_view: dict[str, Any] | None = None
|
|
6700
|
+
if view_key:
|
|
6701
|
+
matched_view = existing_by_key.get(view_key)
|
|
6702
|
+
if matched_view is None:
|
|
6703
|
+
issue = {
|
|
6704
|
+
"error_code": "UNKNOWN_VIEW",
|
|
6705
|
+
"reason_path": f"patch_views[{index}].view_key",
|
|
6706
|
+
"view_key": view_key,
|
|
6707
|
+
"message": "view_key does not exist on this app",
|
|
6708
|
+
}
|
|
6709
|
+
issues.append(issue)
|
|
6710
|
+
results.append({"index": index, "status": "failed", **issue})
|
|
6711
|
+
continue
|
|
6712
|
+
else:
|
|
6713
|
+
matches = existing_by_name.get(name, [])
|
|
6714
|
+
if len(matches) != 1:
|
|
6715
|
+
issue = {
|
|
6716
|
+
"error_code": "AMBIGUOUS_VIEW" if matches else "UNKNOWN_VIEW",
|
|
6717
|
+
"reason_path": f"patch_views[{index}].name",
|
|
6718
|
+
"view_name": name,
|
|
6719
|
+
"matches": [
|
|
6720
|
+
{"name": _extract_view_name(view), "view_key": _extract_view_key(view), "type": _normalize_view_type_name(view.get("viewgraphType") or view.get("type"))}
|
|
6721
|
+
for view in matches
|
|
6722
|
+
],
|
|
6723
|
+
"message": "patch_views[] must target a single existing view; use view_key when names are duplicated",
|
|
6724
|
+
}
|
|
6725
|
+
issues.append(issue)
|
|
6726
|
+
results.append({"index": index, "status": "failed", **issue})
|
|
6727
|
+
continue
|
|
6728
|
+
matched_view = matches[0]
|
|
6729
|
+
view_key = _extract_view_key(matched_view)
|
|
6730
|
+
try:
|
|
6731
|
+
config_response = self.views.view_get_config(profile=profile, viewgraph_key=view_key)
|
|
6732
|
+
config = config_response.get("result") if isinstance(config_response.get("result"), dict) else {}
|
|
6733
|
+
if not isinstance(config, dict):
|
|
6734
|
+
config = {}
|
|
6735
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
6736
|
+
api_error = _coerce_api_error(error)
|
|
6737
|
+
issue = {
|
|
6738
|
+
"error_code": "VIEW_PATCH_CONFIG_READ_FAILED",
|
|
6739
|
+
"reason_path": f"patch_views[{index}]",
|
|
6740
|
+
"view_key": view_key,
|
|
6741
|
+
"message": api_error.message,
|
|
6742
|
+
"transport_error": _transport_error_payload(api_error),
|
|
6743
|
+
}
|
|
6744
|
+
issues.append(issue)
|
|
6745
|
+
results.append({"index": index, "status": "failed", **issue})
|
|
6746
|
+
continue
|
|
6747
|
+
question_list: list[dict[str, Any]] = []
|
|
6748
|
+
try:
|
|
6749
|
+
question_response = self.views.view_list_questions(profile=profile, viewgraph_key=view_key)
|
|
6750
|
+
raw_questions = question_response.get("result")
|
|
6751
|
+
if isinstance(raw_questions, list):
|
|
6752
|
+
question_list = [deepcopy(item) for item in raw_questions if isinstance(item, dict)]
|
|
6753
|
+
except (QingflowApiError, RuntimeError):
|
|
6754
|
+
question_list = []
|
|
6755
|
+
base_summary = {
|
|
6756
|
+
"name": _extract_view_name(config) or _extract_view_name(matched_view or {}) or name or view_key,
|
|
6757
|
+
"view_key": view_key,
|
|
6758
|
+
"type": _normalize_view_type_name(config.get("viewgraphType") or (matched_view or {}).get("viewgraphType") or (matched_view or {}).get("type")),
|
|
6759
|
+
}
|
|
6760
|
+
summary = _merge_view_summary_with_config(base_summary, config=config, question_list=question_list)
|
|
6761
|
+
current_payload = _view_upsert_payload_from_existing_view(
|
|
6762
|
+
config=config,
|
|
6763
|
+
summary=summary,
|
|
6764
|
+
view_key=view_key,
|
|
6765
|
+
field_names_by_id=field_names_by_id,
|
|
6766
|
+
)
|
|
6767
|
+
normalized_set, set_issues = _normalize_view_partial_set(patch.set, reason_path=f"patch_views[{index}].set")
|
|
6768
|
+
normalized_unset, unset_issues = _normalize_view_partial_unset(patch.unset, reason_path=f"patch_views[{index}].unset")
|
|
6769
|
+
if set_issues or unset_issues:
|
|
6770
|
+
patch_issues = [*set_issues, *unset_issues]
|
|
6771
|
+
issues.extend(patch_issues)
|
|
6772
|
+
results.append({"index": index, "status": "failed", "view_key": view_key, "issues": patch_issues})
|
|
6773
|
+
continue
|
|
6774
|
+
touched_keys = set(normalized_set) | set(normalized_unset)
|
|
6775
|
+
patch_payload = deepcopy(current_payload)
|
|
6776
|
+
for key, value in normalized_set.items():
|
|
6777
|
+
if key in {"query_conditions", "associated_resources", "visibility"} and isinstance(value, dict):
|
|
6778
|
+
merged_value = deepcopy(patch_payload.get(key) if isinstance(patch_payload.get(key), dict) else {})
|
|
6779
|
+
_deep_merge_public_config(merged_value, value)
|
|
6780
|
+
patch_payload[key] = merged_value
|
|
6781
|
+
else:
|
|
6782
|
+
patch_payload[key] = value
|
|
6783
|
+
for key in normalized_unset:
|
|
6784
|
+
if key == "filters":
|
|
6785
|
+
patch_payload["filters"] = []
|
|
6786
|
+
elif key == "buttons":
|
|
6787
|
+
patch_payload["buttons"] = []
|
|
6788
|
+
elif key == "query_conditions":
|
|
6789
|
+
patch_payload["query_conditions"] = {"enabled": False, "exact": False, "hide_before_query": False, "rows": []}
|
|
6790
|
+
elif key == "associated_resources":
|
|
6791
|
+
patch_payload["associated_resources"] = {"visible": False}
|
|
6792
|
+
elif key == "visibility":
|
|
6793
|
+
patch_payload.pop("visibility", None)
|
|
6794
|
+
else:
|
|
6795
|
+
issue = {
|
|
6796
|
+
"error_code": "VIEW_PATCH_UNSET_NOT_SUPPORTED",
|
|
6797
|
+
"reason_path": f"patch_views[{index}].unset",
|
|
6798
|
+
"field": key,
|
|
6799
|
+
"message": f"cannot unset {key}; use set with an explicit replacement value",
|
|
6800
|
+
}
|
|
6801
|
+
issues.append(issue)
|
|
6802
|
+
patch_payload["_partial_update"] = True
|
|
6803
|
+
patch_payload["_preserve_filters"] = "filters" not in touched_keys
|
|
6804
|
+
patch_payload["_preserve_buttons"] = "buttons" not in touched_keys
|
|
6805
|
+
patch_payload["_preserve_query_conditions"] = "query_conditions" not in touched_keys
|
|
6806
|
+
patch_payload["_preserve_associated_resources"] = "associated_resources" not in touched_keys
|
|
6807
|
+
try:
|
|
6808
|
+
expanded_patch = ViewUpsertPatch.model_validate(patch_payload)
|
|
6809
|
+
except Exception as error:
|
|
6810
|
+
issue = {
|
|
6811
|
+
"error_code": "VIEW_PATCH_HYDRATION_FAILED",
|
|
6812
|
+
"reason_path": f"patch_views[{index}]",
|
|
6813
|
+
"view_key": view_key,
|
|
6814
|
+
"message": str(error),
|
|
6815
|
+
"hydrated_payload": _compact_dict({k: v for k, v in patch_payload.items() if not str(k).startswith("_")}),
|
|
6816
|
+
}
|
|
6817
|
+
issues.append(issue)
|
|
6818
|
+
results.append({"index": index, "status": "failed", **issue})
|
|
6819
|
+
continue
|
|
6820
|
+
expanded.append(expanded_patch)
|
|
6821
|
+
results.append(
|
|
6822
|
+
{
|
|
6823
|
+
"index": index,
|
|
6824
|
+
"status": "expanded",
|
|
6825
|
+
"view_key": view_key,
|
|
6826
|
+
"set_paths": sorted(normalized_set),
|
|
6827
|
+
"unset_paths": sorted(normalized_unset),
|
|
6828
|
+
"preserved_paths": sorted(
|
|
6829
|
+
key
|
|
6830
|
+
for key, preserve in {
|
|
6831
|
+
"filters": expanded_patch.preserve_filters,
|
|
6832
|
+
"buttons": expanded_patch.preserve_buttons,
|
|
6833
|
+
"query_conditions": expanded_patch.preserve_query_conditions,
|
|
6834
|
+
"associated_resources": expanded_patch.preserve_associated_resources,
|
|
6835
|
+
}.items()
|
|
6836
|
+
if preserve
|
|
6837
|
+
),
|
|
6838
|
+
}
|
|
6839
|
+
)
|
|
6840
|
+
return expanded, issues, results
|
|
6841
|
+
|
|
6168
6842
|
def app_read(self, *, profile: str, app_key: str, include_raw: bool = False) -> JSONObject:
|
|
6169
6843
|
state = self._load_app_state(profile=profile, app_key=app_key)
|
|
6170
6844
|
base_result = state["base"]
|
|
@@ -7461,16 +8135,19 @@ class AiBuilderFacade:
|
|
|
7461
8135
|
profile: str,
|
|
7462
8136
|
app_key: str,
|
|
7463
8137
|
upsert_views: list[ViewUpsertPatch],
|
|
8138
|
+
patch_views: list[ViewPartialPatch] | None = None,
|
|
7464
8139
|
remove_views: list[str],
|
|
7465
8140
|
publish: bool = True,
|
|
7466
8141
|
) -> JSONObject:
|
|
8142
|
+
patch_views = patch_views or []
|
|
7467
8143
|
normalized_args = {
|
|
7468
8144
|
"app_key": app_key,
|
|
7469
8145
|
"upsert_views": [patch.model_dump(mode="json") for patch in upsert_views],
|
|
8146
|
+
"patch_views": [patch.model_dump(mode="json") for patch in patch_views],
|
|
7470
8147
|
"remove_views": list(remove_views),
|
|
7471
8148
|
"publish": publish,
|
|
7472
8149
|
}
|
|
7473
|
-
if not upsert_views and not remove_views:
|
|
8150
|
+
if not upsert_views and not patch_views and not remove_views:
|
|
7474
8151
|
response = {
|
|
7475
8152
|
"status": "success",
|
|
7476
8153
|
"error_code": None,
|
|
@@ -7535,6 +8212,28 @@ class AiBuilderFacade:
|
|
|
7535
8212
|
existing_by_name.setdefault(name, []).append(view)
|
|
7536
8213
|
parsed_schema = _parse_schema(schema)
|
|
7537
8214
|
field_names = {field["name"] for field in parsed_schema["fields"]}
|
|
8215
|
+
if patch_views:
|
|
8216
|
+
expanded_views, patch_issues, patch_results = self._expand_view_partial_patches(
|
|
8217
|
+
profile=profile,
|
|
8218
|
+
app_key=app_key,
|
|
8219
|
+
schema=schema,
|
|
8220
|
+
existing_by_key=existing_by_key,
|
|
8221
|
+
existing_by_name=existing_by_name,
|
|
8222
|
+
patch_views=patch_views,
|
|
8223
|
+
)
|
|
8224
|
+
if patch_issues:
|
|
8225
|
+
return finalize(
|
|
8226
|
+
_failed(
|
|
8227
|
+
"VIEW_PATCH_HYDRATION_FAILED",
|
|
8228
|
+
"one or more view partial patches could not be hydrated; no write was executed",
|
|
8229
|
+
normalized_args=normalized_args,
|
|
8230
|
+
details={"patch_results": patch_results, "blocking_issues": patch_issues},
|
|
8231
|
+
suggested_next_call={"tool_name": "view_get", "arguments": {"profile": profile, "view_key": patch_issues[0].get("view_key") or "VIEW_KEY"}},
|
|
8232
|
+
)
|
|
8233
|
+
)
|
|
8234
|
+
upsert_views = [*upsert_views, *expanded_views]
|
|
8235
|
+
normalized_args["upsert_views"] = [patch.model_dump(mode="json") for patch in upsert_views]
|
|
8236
|
+
normalized_args["patch_results"] = patch_results
|
|
7538
8237
|
current_fields_by_name = {
|
|
7539
8238
|
str(field.get("name") or ""): field
|
|
7540
8239
|
for field in parsed_schema["fields"]
|
|
@@ -7701,7 +8400,13 @@ class AiBuilderFacade:
|
|
|
7701
8400
|
missing_fields=[gantt_field_name],
|
|
7702
8401
|
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
7703
8402
|
)
|
|
7704
|
-
translated_filters,
|
|
8403
|
+
translated_filters: list[list[dict[str, Any]]] | None
|
|
8404
|
+
filter_issues: list[dict[str, Any]]
|
|
8405
|
+
if patch.preserve_filters:
|
|
8406
|
+
translated_filters = None
|
|
8407
|
+
filter_issues = []
|
|
8408
|
+
else:
|
|
8409
|
+
translated_filters, filter_issues = _build_view_filter_groups(current_fields_by_name=current_fields_by_name, filters=patch.filters)
|
|
7705
8410
|
if filter_issues:
|
|
7706
8411
|
first_issue = filter_issues[0]
|
|
7707
8412
|
return _failed(
|
|
@@ -7717,10 +8422,15 @@ class AiBuilderFacade:
|
|
|
7717
8422
|
allowed_values=first_issue.get("allowed_values") or {"view_types": [member.value for member in PublicViewType], "view.filter.operator": [member.value for member in ViewFilterOperator]},
|
|
7718
8423
|
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
7719
8424
|
)
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
8425
|
+
if patch.preserve_query_conditions:
|
|
8426
|
+
query_condition_payload = None
|
|
8427
|
+
expected_query_conditions = None
|
|
8428
|
+
query_condition_issues = []
|
|
8429
|
+
else:
|
|
8430
|
+
query_condition_payload, expected_query_conditions, query_condition_issues = _build_view_query_conditions_payload(
|
|
8431
|
+
current_fields_by_name=current_fields_by_name,
|
|
8432
|
+
query_conditions=patch.query_conditions,
|
|
8433
|
+
)
|
|
7724
8434
|
if query_condition_issues:
|
|
7725
8435
|
first_issue = query_condition_issues[0]
|
|
7726
8436
|
return _failed(
|
|
@@ -7741,10 +8451,15 @@ class AiBuilderFacade:
|
|
|
7741
8451
|
},
|
|
7742
8452
|
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
7743
8453
|
)
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
|
|
8454
|
+
if patch.preserve_associated_resources:
|
|
8455
|
+
associated_resources_payload = None
|
|
8456
|
+
expected_associated_resources = None
|
|
8457
|
+
associated_resource_issues = []
|
|
8458
|
+
else:
|
|
8459
|
+
associated_resources_payload, expected_associated_resources, associated_resource_issues = _build_view_associated_resources_payload(
|
|
8460
|
+
associated_resources=patch.associated_resources,
|
|
8461
|
+
available_resources=associated_resources,
|
|
8462
|
+
)
|
|
7748
8463
|
if associated_resource_issues:
|
|
7749
8464
|
first_issue = associated_resource_issues[0]
|
|
7750
8465
|
return _failed(
|
|
@@ -7765,7 +8480,10 @@ class AiBuilderFacade:
|
|
|
7765
8480
|
)
|
|
7766
8481
|
explicit_button_dtos: list[dict[str, Any]] | None = None
|
|
7767
8482
|
expected_button_summary: list[dict[str, Any]] | None = None
|
|
7768
|
-
if patch.
|
|
8483
|
+
if patch.preserve_buttons:
|
|
8484
|
+
explicit_button_dtos = None
|
|
8485
|
+
expected_button_summary = None
|
|
8486
|
+
elif patch.buttons is not None:
|
|
7769
8487
|
explicit_button_dtos, button_issues = _build_view_button_dtos(
|
|
7770
8488
|
current_fields_by_name=current_fields_by_name,
|
|
7771
8489
|
bindings=patch.buttons,
|
|
@@ -8239,7 +8957,10 @@ class AiBuilderFacade:
|
|
|
8239
8957
|
expected_filter_summary = _normalize_view_filter_groups_for_compare(expected_filters)
|
|
8240
8958
|
expected_data_scope = "CUSTOM" if expected_filter_summary else "ALL"
|
|
8241
8959
|
actual_data_scope = str(config_result.get("dataScope") or "").strip().upper() or None
|
|
8242
|
-
filters_verified =
|
|
8960
|
+
filters_verified = (
|
|
8961
|
+
_view_filter_groups_equivalent(expected_filter_summary, actual_filters)
|
|
8962
|
+
and actual_data_scope == expected_data_scope
|
|
8963
|
+
)
|
|
8243
8964
|
verification_entry["filters_verified"] = filters_verified
|
|
8244
8965
|
verification_entry["view_key"] = verification_key
|
|
8245
8966
|
verification_entry["expected_filters"] = expected_filter_summary
|
|
@@ -8737,6 +9458,126 @@ class AiBuilderFacade:
|
|
|
8737
9458
|
"verified": verified,
|
|
8738
9459
|
}
|
|
8739
9460
|
|
|
9461
|
+
def _expand_chart_partial_patches(
|
|
9462
|
+
self,
|
|
9463
|
+
*,
|
|
9464
|
+
profile: str,
|
|
9465
|
+
app_key: str,
|
|
9466
|
+
existing_by_id: dict[str, dict[str, Any]],
|
|
9467
|
+
existing_by_name: dict[str, list[dict[str, Any]]],
|
|
9468
|
+
patch_charts: list[ChartPartialPatch],
|
|
9469
|
+
) -> tuple[list[ChartUpsertPatch], list[dict[str, Any]], list[dict[str, Any]]]:
|
|
9470
|
+
expanded: list[ChartUpsertPatch] = []
|
|
9471
|
+
issues: list[dict[str, Any]] = []
|
|
9472
|
+
results: list[dict[str, Any]] = []
|
|
9473
|
+
for index, patch in enumerate(patch_charts):
|
|
9474
|
+
chart_id = str(patch.chart_id or "").strip()
|
|
9475
|
+
if chart_id:
|
|
9476
|
+
if chart_id not in existing_by_id:
|
|
9477
|
+
issue = {
|
|
9478
|
+
"error_code": "CHART_NOT_FOUND",
|
|
9479
|
+
"reason_path": f"patch_charts[{index}].chart_id",
|
|
9480
|
+
"chart_id": chart_id,
|
|
9481
|
+
"message": "chart_id does not exist under this app",
|
|
9482
|
+
}
|
|
9483
|
+
issues.append(issue)
|
|
9484
|
+
results.append({"index": index, "status": "failed", **issue})
|
|
9485
|
+
continue
|
|
9486
|
+
else:
|
|
9487
|
+
name = str(patch.name or "").strip()
|
|
9488
|
+
matches = existing_by_name.get(name, [])
|
|
9489
|
+
if len(matches) != 1:
|
|
9490
|
+
issue = {
|
|
9491
|
+
"error_code": "AMBIGUOUS_CHART" if matches else "CHART_NOT_FOUND",
|
|
9492
|
+
"reason_path": f"patch_charts[{index}].name",
|
|
9493
|
+
"name": name,
|
|
9494
|
+
"candidate_chart_ids": [
|
|
9495
|
+
_extract_chart_identifier(item)
|
|
9496
|
+
for item in matches
|
|
9497
|
+
if _extract_chart_identifier(item)
|
|
9498
|
+
],
|
|
9499
|
+
"message": "patch_charts[] must target a single existing chart; use chart_id when names are duplicated",
|
|
9500
|
+
}
|
|
9501
|
+
issues.append(issue)
|
|
9502
|
+
results.append({"index": index, "status": "failed", **issue})
|
|
9503
|
+
continue
|
|
9504
|
+
chart_id = _extract_chart_identifier(matches[0])
|
|
9505
|
+
if not chart_id:
|
|
9506
|
+
issue = {"error_code": "CHART_ID_MISSING", "reason_path": f"patch_charts[{index}].name", "name": name}
|
|
9507
|
+
issues.append(issue)
|
|
9508
|
+
results.append({"index": index, "status": "failed", **issue})
|
|
9509
|
+
continue
|
|
9510
|
+
try:
|
|
9511
|
+
base = self.charts.qingbi_report_get_base(profile=profile, chart_id=chart_id).get("result") or {}
|
|
9512
|
+
config = self.charts.qingbi_report_get_config(profile=profile, chart_id=chart_id).get("result") or {}
|
|
9513
|
+
if not isinstance(base, dict):
|
|
9514
|
+
base = {}
|
|
9515
|
+
if not isinstance(config, dict):
|
|
9516
|
+
config = {}
|
|
9517
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
9518
|
+
api_error = _coerce_api_error(error)
|
|
9519
|
+
issue = {
|
|
9520
|
+
"error_code": "CHART_PATCH_DETAIL_READ_FAILED",
|
|
9521
|
+
"reason_path": f"patch_charts[{index}]",
|
|
9522
|
+
"chart_id": chart_id,
|
|
9523
|
+
"message": api_error.message,
|
|
9524
|
+
"transport_error": _transport_error_payload(api_error),
|
|
9525
|
+
}
|
|
9526
|
+
issues.append(issue)
|
|
9527
|
+
results.append({"index": index, "status": "failed", **issue})
|
|
9528
|
+
continue
|
|
9529
|
+
patch_payload = _chart_upsert_payload_from_existing(chart_id=chart_id, base=base, config=config)
|
|
9530
|
+
normalized_set, set_issues = _normalize_chart_partial_set(patch.set, reason_path=f"patch_charts[{index}].set")
|
|
9531
|
+
normalized_unset, unset_issues = _normalize_chart_partial_unset(patch.unset, reason_path=f"patch_charts[{index}].unset")
|
|
9532
|
+
if set_issues or unset_issues:
|
|
9533
|
+
patch_issues = [*set_issues, *unset_issues]
|
|
9534
|
+
issues.extend(patch_issues)
|
|
9535
|
+
results.append({"index": index, "status": "failed", "chart_id": chart_id, "issues": patch_issues})
|
|
9536
|
+
continue
|
|
9537
|
+
explicit_set_paths = set(normalized_set)
|
|
9538
|
+
for key, value in normalized_set.items():
|
|
9539
|
+
if key in {"config", "visibility"} and isinstance(value, dict):
|
|
9540
|
+
merged_value = deepcopy(patch_payload.get(key) if isinstance(patch_payload.get(key), dict) else {})
|
|
9541
|
+
_deep_merge_public_config(merged_value, value)
|
|
9542
|
+
patch_payload[key] = merged_value
|
|
9543
|
+
else:
|
|
9544
|
+
patch_payload[key] = value
|
|
9545
|
+
for key in normalized_unset:
|
|
9546
|
+
if key == "filters":
|
|
9547
|
+
patch_payload["filters"] = []
|
|
9548
|
+
explicit_set_paths.add("filters")
|
|
9549
|
+
elif key in {"question_config", "user_config"}:
|
|
9550
|
+
patch_payload[key] = []
|
|
9551
|
+
explicit_set_paths.add(key)
|
|
9552
|
+
elif key == "visibility":
|
|
9553
|
+
patch_payload.pop("visibility", None)
|
|
9554
|
+
try:
|
|
9555
|
+
expanded_patch = ChartUpsertPatch.model_validate(patch_payload)
|
|
9556
|
+
except Exception as error:
|
|
9557
|
+
issue = {
|
|
9558
|
+
"error_code": "CHART_PATCH_HYDRATION_FAILED",
|
|
9559
|
+
"reason_path": f"patch_charts[{index}]",
|
|
9560
|
+
"chart_id": chart_id,
|
|
9561
|
+
"message": str(error),
|
|
9562
|
+
"hydrated_payload": _compact_dict(patch_payload),
|
|
9563
|
+
}
|
|
9564
|
+
issues.append(issue)
|
|
9565
|
+
results.append({"index": index, "status": "failed", **issue})
|
|
9566
|
+
continue
|
|
9567
|
+
# Preserve model_fields_set semantics so config generation knows which public fields were explicitly replaced.
|
|
9568
|
+
expanded_patch.__pydantic_fields_set__ = set(explicit_set_paths)
|
|
9569
|
+
expanded.append(expanded_patch)
|
|
9570
|
+
results.append(
|
|
9571
|
+
{
|
|
9572
|
+
"index": index,
|
|
9573
|
+
"status": "expanded",
|
|
9574
|
+
"chart_id": chart_id,
|
|
9575
|
+
"set_paths": sorted(normalized_set),
|
|
9576
|
+
"unset_paths": sorted(normalized_unset),
|
|
9577
|
+
}
|
|
9578
|
+
)
|
|
9579
|
+
return expanded, issues, results
|
|
9580
|
+
|
|
8740
9581
|
def chart_apply(self, *, profile: str, request: ChartApplyRequest) -> JSONObject:
|
|
8741
9582
|
normalized_args = request.model_dump(mode="json")
|
|
8742
9583
|
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
@@ -8796,14 +9637,44 @@ class AiBuilderFacade:
|
|
|
8796
9637
|
continue
|
|
8797
9638
|
existing_by_name.setdefault(item_name, []).append(deepcopy(item))
|
|
8798
9639
|
|
|
9640
|
+
upsert_charts = list(request.upsert_charts)
|
|
9641
|
+
if request.patch_charts:
|
|
9642
|
+
expanded_charts, patch_issues, patch_results = self._expand_chart_partial_patches(
|
|
9643
|
+
profile=profile,
|
|
9644
|
+
app_key=app_key,
|
|
9645
|
+
existing_by_id=existing_by_id,
|
|
9646
|
+
existing_by_name=existing_by_name,
|
|
9647
|
+
patch_charts=request.patch_charts,
|
|
9648
|
+
)
|
|
9649
|
+
if patch_issues:
|
|
9650
|
+
return finalize(
|
|
9651
|
+
_failed(
|
|
9652
|
+
"CHART_PATCH_HYDRATION_FAILED",
|
|
9653
|
+
"one or more chart partial patches could not be hydrated; no write was executed",
|
|
9654
|
+
normalized_args=normalized_args,
|
|
9655
|
+
details={"patch_results": patch_results, "blocking_issues": patch_issues},
|
|
9656
|
+
suggested_next_call={"tool_name": "chart_get", "arguments": {"profile": profile, "chart_id": patch_issues[0].get("chart_id") or "CHART_ID"}},
|
|
9657
|
+
)
|
|
9658
|
+
)
|
|
9659
|
+
upsert_charts.extend(expanded_charts)
|
|
9660
|
+
normalized_args["upsert_charts"] = [patch.model_dump(mode="json") for patch in upsert_charts]
|
|
9661
|
+
normalized_args["patch_results"] = patch_results
|
|
9662
|
+
|
|
8799
9663
|
chart_results: list[dict[str, Any]] = []
|
|
8800
9664
|
created_ids: list[str] = []
|
|
8801
9665
|
updated_ids: list[str] = []
|
|
8802
9666
|
removed_ids: list[str] = []
|
|
8803
9667
|
failed_items: list[dict[str, Any]] = []
|
|
8804
9668
|
|
|
8805
|
-
for patch in
|
|
9669
|
+
for patch in upsert_charts:
|
|
8806
9670
|
try:
|
|
9671
|
+
dataset_source = _chart_patch_dataset_source_type(patch)
|
|
9672
|
+
if dataset_source:
|
|
9673
|
+
raise ValueError(
|
|
9674
|
+
"app_charts_apply only creates or edits app-source QingBI reports with dataSourceType=qingflow; "
|
|
9675
|
+
f"dataset report source '{dataset_source}' is not supported for creation/update yet. "
|
|
9676
|
+
"Create the dataset report in QingBI first, then attach it with app_associated_resources_apply using report_source='dataset'."
|
|
9677
|
+
)
|
|
8807
9678
|
config_update_requested = _chart_patch_updates_chart_config(patch)
|
|
8808
9679
|
chart_visible_auth = (
|
|
8809
9680
|
self._compile_visibility_to_chart_visible_auth(profile=profile, visibility=patch.visibility)
|
|
@@ -8827,6 +9698,13 @@ class AiBuilderFacade:
|
|
|
8827
9698
|
existing_name = str((existing or {}).get("chartName") or "").strip()
|
|
8828
9699
|
existing_type = _normalize_backend_chart_type((existing or {}).get("chartType"))
|
|
8829
9700
|
target_type = _map_public_chart_type_to_backend(patch.chart_type)
|
|
9701
|
+
existing_source_type = _chart_item_dataset_source_type(existing or {})
|
|
9702
|
+
if existing_source_type:
|
|
9703
|
+
raise ValueError(
|
|
9704
|
+
"app_charts_apply only creates or edits app-source QingBI reports with dataSourceType=qingflow; "
|
|
9705
|
+
f"existing chart '{chart_id or patch.name}' uses dataset report source '{existing_source_type}' and is not supported for update yet. "
|
|
9706
|
+
"Update it in QingBI directly, then attach the existing report with app_associated_resources_apply using report_source='dataset'."
|
|
9707
|
+
)
|
|
8830
9708
|
if existing is None:
|
|
8831
9709
|
temp_chart_id = str(patch.chart_id or f"mcp_{uuid4().hex[:16]}")
|
|
8832
9710
|
create_payload = {
|
|
@@ -10154,6 +11032,94 @@ def _custom_button_add_data_config_has_semantic_inputs(value: dict[str, Any]) ->
|
|
|
10154
11032
|
return bool(value.get("field_mappings") or value.get("fieldMappings") or value.get("default_values") or value.get("defaultValues"))
|
|
10155
11033
|
|
|
10156
11034
|
|
|
11035
|
+
def _normalize_custom_button_add_data_config_for_public(value: dict[str, Any]) -> dict[str, Any]:
|
|
11036
|
+
normalized: dict[str, Any] = {}
|
|
11037
|
+
related_app_key = _first_present(value, "related_app_key", "relatedAppKey", "target_app_key", "targetAppKey")
|
|
11038
|
+
related_app_name = _first_present(value, "related_app_name", "relatedAppName")
|
|
11039
|
+
if related_app_key is not None:
|
|
11040
|
+
normalized["related_app_key"] = related_app_key
|
|
11041
|
+
if related_app_name is not None:
|
|
11042
|
+
normalized["related_app_name"] = related_app_name
|
|
11043
|
+
relation_rules = _first_present(value, "que_relation", "queRelation")
|
|
11044
|
+
if isinstance(relation_rules, list):
|
|
11045
|
+
normalized["que_relation"] = [
|
|
11046
|
+
_normalize_custom_button_match_rule_for_public(rule)
|
|
11047
|
+
for rule in relation_rules
|
|
11048
|
+
if isinstance(rule, dict)
|
|
11049
|
+
]
|
|
11050
|
+
field_mappings = _first_present(value, "field_mappings", "fieldMappings", "mappings")
|
|
11051
|
+
if isinstance(field_mappings, list):
|
|
11052
|
+
normalized["field_mappings"] = deepcopy(field_mappings)
|
|
11053
|
+
default_values = _first_present(value, "default_values", "defaultValues", "defaults")
|
|
11054
|
+
if isinstance(default_values, dict):
|
|
11055
|
+
normalized["default_values"] = deepcopy(default_values)
|
|
11056
|
+
return _compact_dict(normalized)
|
|
11057
|
+
|
|
11058
|
+
|
|
11059
|
+
def _normalize_custom_button_match_rule_for_public(value: dict[str, Any]) -> dict[str, Any]:
|
|
11060
|
+
normalized: dict[str, Any] = {}
|
|
11061
|
+
key_pairs = {
|
|
11062
|
+
"que_id": ("que_id", "queId"),
|
|
11063
|
+
"que_title": ("que_title", "queTitle"),
|
|
11064
|
+
"que_type": ("que_type", "queType"),
|
|
11065
|
+
"date_type": ("date_type", "dateType"),
|
|
11066
|
+
"judge_type": ("judge_type", "judgeType"),
|
|
11067
|
+
"match_type": ("match_type", "matchType"),
|
|
11068
|
+
"judge_que_type": ("judge_que_type", "judgeQueType"),
|
|
11069
|
+
"judge_que_id": ("judge_que_id", "judgeQueId"),
|
|
11070
|
+
"path_value": ("path_value", "pathValue"),
|
|
11071
|
+
"table_update_type": ("table_update_type", "tableUpdateType"),
|
|
11072
|
+
"multi_value": ("multi_value", "multiValue"),
|
|
11073
|
+
"add_rule": ("add_rule", "addRule"),
|
|
11074
|
+
"field_id_prefix": ("field_id_prefix", "fieldIdPrefix"),
|
|
11075
|
+
}
|
|
11076
|
+
for public_key, aliases in key_pairs.items():
|
|
11077
|
+
raw_value = _first_present(value, *aliases)
|
|
11078
|
+
if raw_value is not None:
|
|
11079
|
+
normalized[public_key] = raw_value
|
|
11080
|
+
judge_values = _first_present(value, "judge_values", "judgeValues")
|
|
11081
|
+
if isinstance(judge_values, list):
|
|
11082
|
+
normalized["judge_values"] = [str(item) for item in judge_values if item is not None]
|
|
11083
|
+
elif judge_values is not None:
|
|
11084
|
+
normalized["judge_values"] = [str(judge_values)]
|
|
11085
|
+
judge_que_detail = _first_present(value, "judge_que_detail", "judgeQueDetail")
|
|
11086
|
+
if isinstance(judge_que_detail, dict):
|
|
11087
|
+
normalized["judge_que_detail"] = _compact_dict(
|
|
11088
|
+
{
|
|
11089
|
+
"que_id": _first_present(judge_que_detail, "que_id", "queId"),
|
|
11090
|
+
"que_title": _first_present(judge_que_detail, "que_title", "queTitle"),
|
|
11091
|
+
"que_type": _first_present(judge_que_detail, "que_type", "queType"),
|
|
11092
|
+
}
|
|
11093
|
+
)
|
|
11094
|
+
judge_value_details = _first_present(value, "judge_value_details", "judgeValueDetails")
|
|
11095
|
+
if isinstance(judge_value_details, list):
|
|
11096
|
+
details: list[dict[str, Any]] = []
|
|
11097
|
+
for item in judge_value_details:
|
|
11098
|
+
if not isinstance(item, dict):
|
|
11099
|
+
continue
|
|
11100
|
+
detail = _compact_dict(
|
|
11101
|
+
{
|
|
11102
|
+
"id": _first_present(item, "id", "opt_id", "optId", "member_id", "memberId"),
|
|
11103
|
+
"value": _first_present(item, "value", "name", "label", "title"),
|
|
11104
|
+
}
|
|
11105
|
+
)
|
|
11106
|
+
if detail:
|
|
11107
|
+
details.append(detail)
|
|
11108
|
+
normalized["judge_value_details"] = details
|
|
11109
|
+
filter_condition = _first_present(value, "filter_condition", "filterCondition")
|
|
11110
|
+
if isinstance(filter_condition, list):
|
|
11111
|
+
normalized["filter_condition"] = [
|
|
11112
|
+
[
|
|
11113
|
+
_normalize_custom_button_match_rule_for_public(item)
|
|
11114
|
+
for item in group
|
|
11115
|
+
if isinstance(item, dict)
|
|
11116
|
+
]
|
|
11117
|
+
for group in filter_condition
|
|
11118
|
+
if isinstance(group, list)
|
|
11119
|
+
]
|
|
11120
|
+
return _compact_dict(normalized)
|
|
11121
|
+
|
|
11122
|
+
|
|
10157
11123
|
def _custom_button_selector_payload(selector: Any) -> dict[str, Any]:
|
|
10158
11124
|
if isinstance(selector, dict):
|
|
10159
11125
|
payload = dict(selector)
|
|
@@ -10171,11 +11137,83 @@ def _custom_button_selector_payload(selector: Any) -> dict[str, Any]:
|
|
|
10171
11137
|
raw = str(selector or "").strip()
|
|
10172
11138
|
if not raw:
|
|
10173
11139
|
return {}
|
|
10174
|
-
|
|
10175
|
-
|
|
11140
|
+
parsed_int = _coerce_any_int(raw)
|
|
11141
|
+
if parsed_int is not None:
|
|
11142
|
+
return {"que_id": parsed_int}
|
|
10176
11143
|
return {"name": raw}
|
|
10177
11144
|
|
|
10178
11145
|
|
|
11146
|
+
_SYSTEM_MATCH_FIELD_ALIASES: dict[str, int] = {
|
|
11147
|
+
"数据id": -17,
|
|
11148
|
+
"数据ID": -17,
|
|
11149
|
+
"数据_id": -17,
|
|
11150
|
+
"row_record_id": -17,
|
|
11151
|
+
"data_id": -17,
|
|
11152
|
+
"record_id": -17,
|
|
11153
|
+
"apply_id": -17,
|
|
11154
|
+
"_id": -17,
|
|
11155
|
+
"编号": 0,
|
|
11156
|
+
"数据编号": 0,
|
|
11157
|
+
"record_number": 0,
|
|
11158
|
+
"serial_number": 0,
|
|
11159
|
+
"apply_num": 0,
|
|
11160
|
+
"data_num": 0,
|
|
11161
|
+
}
|
|
11162
|
+
|
|
11163
|
+
|
|
11164
|
+
def _normalize_system_match_alias(value: Any) -> str:
|
|
11165
|
+
raw = str(value or "").strip()
|
|
11166
|
+
if raw.startswith("{{") and raw.endswith("}}"):
|
|
11167
|
+
raw = raw[2:-2].strip()
|
|
11168
|
+
return raw.replace(" ", "").lower()
|
|
11169
|
+
|
|
11170
|
+
|
|
11171
|
+
def _system_match_field_from_selector(selector: Any) -> dict[str, Any] | None:
|
|
11172
|
+
if isinstance(selector, dict):
|
|
11173
|
+
for key in ("field_id", "fieldId", "que_id", "queId"):
|
|
11174
|
+
parsed = _coerce_any_int(selector.get(key))
|
|
11175
|
+
if parsed in {-17, 0}:
|
|
11176
|
+
return _system_match_field(parsed)
|
|
11177
|
+
for key in ("name", "title", "label", "value", "field", "source_field", "target_field"):
|
|
11178
|
+
raw = str(selector.get(key) or "").strip()
|
|
11179
|
+
if raw:
|
|
11180
|
+
field = _system_match_field_from_selector(raw)
|
|
11181
|
+
if field is not None:
|
|
11182
|
+
return field
|
|
11183
|
+
return None
|
|
11184
|
+
parsed = _coerce_any_int(selector)
|
|
11185
|
+
if parsed in {-17, 0}:
|
|
11186
|
+
return _system_match_field(parsed)
|
|
11187
|
+
normalized = _normalize_system_match_alias(selector)
|
|
11188
|
+
if normalized in _SYSTEM_MATCH_FIELD_ALIASES:
|
|
11189
|
+
return _system_match_field(_SYSTEM_MATCH_FIELD_ALIASES[normalized])
|
|
11190
|
+
return None
|
|
11191
|
+
|
|
11192
|
+
|
|
11193
|
+
def _system_match_field(que_id: int) -> dict[str, Any] | None:
|
|
11194
|
+
if que_id == -17:
|
|
11195
|
+
return {
|
|
11196
|
+
"field_id": -17,
|
|
11197
|
+
"que_id": -17,
|
|
11198
|
+
"que_type": 8,
|
|
11199
|
+
"name": "数据ID",
|
|
11200
|
+
"type": "system_record_id",
|
|
11201
|
+
"system_field": True,
|
|
11202
|
+
"system_kind": "row_record_id",
|
|
11203
|
+
}
|
|
11204
|
+
if que_id == 0:
|
|
11205
|
+
return {
|
|
11206
|
+
"field_id": 0,
|
|
11207
|
+
"que_id": 0,
|
|
11208
|
+
"que_type": 8,
|
|
11209
|
+
"name": "编号",
|
|
11210
|
+
"type": "system_record_number",
|
|
11211
|
+
"system_field": True,
|
|
11212
|
+
"system_kind": "record_number",
|
|
11213
|
+
}
|
|
11214
|
+
return None
|
|
11215
|
+
|
|
11216
|
+
|
|
10179
11217
|
def _resolve_custom_button_schema_field(
|
|
10180
11218
|
*,
|
|
10181
11219
|
fields: list[dict[str, Any]],
|
|
@@ -10184,6 +11222,11 @@ def _resolve_custom_button_schema_field(
|
|
|
10184
11222
|
role: str,
|
|
10185
11223
|
) -> tuple[dict[str, Any] | None, dict[str, Any] | None]:
|
|
10186
11224
|
selector_payload = _custom_button_selector_payload(selector)
|
|
11225
|
+
allow_system = role in {"source", "match_source", "match_target"}
|
|
11226
|
+
if allow_system:
|
|
11227
|
+
system_field = _system_match_field_from_selector(selector_payload or selector)
|
|
11228
|
+
if system_field is not None:
|
|
11229
|
+
return system_field, None
|
|
10187
11230
|
if not selector_payload:
|
|
10188
11231
|
return None, {
|
|
10189
11232
|
"error_code": "CUSTOM_BUTTON_MAPPING_FIELD_NOT_FOUND",
|
|
@@ -10193,18 +11236,19 @@ def _resolve_custom_button_schema_field(
|
|
|
10193
11236
|
}
|
|
10194
11237
|
matched: list[dict[str, Any]] = []
|
|
10195
11238
|
field_id = selector_payload.get("field_id")
|
|
10196
|
-
que_id =
|
|
11239
|
+
que_id = _coerce_any_int(selector_payload.get("que_id"))
|
|
10197
11240
|
name = str(selector_payload.get("name") or "").strip()
|
|
10198
11241
|
if field_id is not None:
|
|
10199
11242
|
field_id_text = str(field_id).strip()
|
|
11243
|
+
field_id_int = _coerce_any_int(field_id_text)
|
|
10200
11244
|
matched = [
|
|
10201
11245
|
field
|
|
10202
11246
|
for field in fields
|
|
10203
11247
|
if str(field.get("field_id") or "").strip() == field_id_text
|
|
10204
|
-
or (
|
|
11248
|
+
or (field_id_int is not None and _coerce_any_int(field.get("que_id")) == field_id_int)
|
|
10205
11249
|
]
|
|
10206
11250
|
elif que_id is not None:
|
|
10207
|
-
matched = [field for field in fields if
|
|
11251
|
+
matched = [field for field in fields if _coerce_any_int(field.get("que_id")) == que_id]
|
|
10208
11252
|
elif name:
|
|
10209
11253
|
matched = [field for field in fields if str(field.get("name") or "").strip() == name]
|
|
10210
11254
|
if len(matched) == 1:
|
|
@@ -10227,40 +11271,239 @@ def _custom_button_mapping_type_issue(
|
|
|
10227
11271
|
source_field: dict[str, Any],
|
|
10228
11272
|
target_field: dict[str, Any],
|
|
10229
11273
|
reason_path: str,
|
|
11274
|
+
source_app_key: str | None = None,
|
|
11275
|
+
error_code: str = "CUSTOM_BUTTON_MAPPING_TYPE_MISMATCH",
|
|
11276
|
+
context_label: str = "field mapping",
|
|
10230
11277
|
) -> dict[str, Any] | None:
|
|
10231
11278
|
source_type = str(source_field.get("type") or "")
|
|
10232
11279
|
target_type = str(target_field.get("type") or "")
|
|
11280
|
+
compatible, reason = _match_mapping_types_compatible(
|
|
11281
|
+
source_field=source_field,
|
|
11282
|
+
target_field=target_field,
|
|
11283
|
+
source_app_key=source_app_key,
|
|
11284
|
+
)
|
|
11285
|
+
if compatible:
|
|
11286
|
+
return None
|
|
11287
|
+
if reason == "reference_source_mismatch":
|
|
11288
|
+
target_app_key = _relation_target_app_key(target_field)
|
|
11289
|
+
return {
|
|
11290
|
+
"error_code": "MATCH_RULE_REFERENCE_SOURCE_MISMATCH",
|
|
11291
|
+
"reason_path": reason_path,
|
|
11292
|
+
"source_app_key": source_app_key,
|
|
11293
|
+
"target_relation_app_key": target_app_key,
|
|
11294
|
+
"source_field": _match_field_summary(source_field),
|
|
11295
|
+
"target_field": _match_field_summary(target_field),
|
|
11296
|
+
"message": "数据ID represents the current source record, but the target relation field points to another app",
|
|
11297
|
+
"next_action": "choose a relation field that targets the current source app, or map a compatible non-relation field",
|
|
11298
|
+
}
|
|
11299
|
+
return {
|
|
11300
|
+
"error_code": error_code,
|
|
11301
|
+
"reason_path": reason_path,
|
|
11302
|
+
"source_field": _match_field_summary(source_field),
|
|
11303
|
+
"target_field": _match_field_summary(target_field),
|
|
11304
|
+
"message": f"source and target fields have incompatible types for {context_label}",
|
|
11305
|
+
"compatibility_reason": reason,
|
|
11306
|
+
"next_action": "choose fields with the same type family; use 数据ID only for current-record id/relation matching and use static value/default_values for constants",
|
|
11307
|
+
}
|
|
11308
|
+
|
|
11309
|
+
|
|
11310
|
+
def _retag_associated_resource_match_field_issue(issue: dict[str, Any]) -> dict[str, Any]:
|
|
11311
|
+
retagged = dict(issue)
|
|
11312
|
+
code = str(retagged.get("error_code") or "")
|
|
11313
|
+
if code == "CUSTOM_BUTTON_MAPPING_FIELD_NOT_FOUND":
|
|
11314
|
+
retagged["error_code"] = "ASSOCIATED_RESOURCE_MAPPING_FIELD_NOT_FOUND"
|
|
11315
|
+
elif code == "CUSTOM_BUTTON_MAPPING_FIELD_AMBIGUOUS":
|
|
11316
|
+
retagged["error_code"] = "ASSOCIATED_RESOURCE_MAPPING_FIELD_AMBIGUOUS"
|
|
11317
|
+
return retagged
|
|
11318
|
+
|
|
11319
|
+
|
|
11320
|
+
def _retag_associated_resource_static_value_issue(issue: dict[str, Any]) -> dict[str, Any]:
|
|
11321
|
+
retagged = dict(issue)
|
|
11322
|
+
if str(retagged.get("error_code") or "") == "CUSTOM_BUTTON_DEFAULT_VALUE_UNSUPPORTED":
|
|
11323
|
+
retagged["error_code"] = "ASSOCIATED_RESOURCE_STATIC_VALUE_UNSUPPORTED"
|
|
11324
|
+
retagged["message"] = str(retagged.get("message") or "static associated resource match value is unsupported")
|
|
11325
|
+
retagged["next_action"] = str(
|
|
11326
|
+
retagged.get("next_action")
|
|
11327
|
+
or "retry match_mappings[].value with a literal value supported by the target field type"
|
|
11328
|
+
)
|
|
11329
|
+
return retagged
|
|
11330
|
+
|
|
11331
|
+
|
|
11332
|
+
def _match_field_summary(field: dict[str, Any]) -> dict[str, Any]:
|
|
11333
|
+
return {
|
|
11334
|
+
"title": field.get("name"),
|
|
11335
|
+
"field_id": field.get("field_id"),
|
|
11336
|
+
"que_id": field.get("que_id"),
|
|
11337
|
+
"type": field.get("type"),
|
|
11338
|
+
"system_field": bool(field.get("system_field")),
|
|
11339
|
+
}
|
|
11340
|
+
|
|
11341
|
+
|
|
11342
|
+
def _relation_target_app_key(field: dict[str, Any]) -> str | None:
|
|
11343
|
+
for key in ("target_app_key", "targetAppKey", "refer_app_key", "referAppKey"):
|
|
11344
|
+
value = str(field.get(key) or "").strip()
|
|
11345
|
+
if value:
|
|
11346
|
+
return value
|
|
11347
|
+
for container_key in ("relation", "reference", "reference_config", "referenceConfig", "config"):
|
|
11348
|
+
container = field.get(container_key)
|
|
11349
|
+
if not isinstance(container, dict):
|
|
11350
|
+
continue
|
|
11351
|
+
nested = _relation_target_app_key(container)
|
|
11352
|
+
if nested:
|
|
11353
|
+
return nested
|
|
11354
|
+
template = field.get("_reference_config_template")
|
|
11355
|
+
if isinstance(template, dict):
|
|
11356
|
+
nested = _relation_target_app_key(template)
|
|
11357
|
+
if nested:
|
|
11358
|
+
return nested
|
|
11359
|
+
return None
|
|
11360
|
+
|
|
11361
|
+
|
|
11362
|
+
def _match_mapping_types_compatible(
|
|
11363
|
+
*,
|
|
11364
|
+
source_field: dict[str, Any],
|
|
11365
|
+
target_field: dict[str, Any],
|
|
11366
|
+
source_app_key: str | None = None,
|
|
11367
|
+
) -> tuple[bool, str | None]:
|
|
11368
|
+
source_type = str(source_field.get("type") or "")
|
|
11369
|
+
target_type = str(target_field.get("type") or "")
|
|
11370
|
+
unsupported = {
|
|
11371
|
+
FieldType.attachment.value,
|
|
11372
|
+
FieldType.subtable.value,
|
|
11373
|
+
FieldType.code_block.value,
|
|
11374
|
+
FieldType.q_linker.value,
|
|
11375
|
+
FieldType.address.value,
|
|
11376
|
+
}
|
|
11377
|
+
if source_type in unsupported or target_type in unsupported:
|
|
11378
|
+
return False, "unsupported_field_type"
|
|
11379
|
+
|
|
11380
|
+
if target_type == FieldType.relation.value:
|
|
11381
|
+
if source_type == "system_record_id":
|
|
11382
|
+
target_app_key = _relation_target_app_key(target_field)
|
|
11383
|
+
if source_app_key and target_app_key and str(target_app_key).strip() != str(source_app_key).strip():
|
|
11384
|
+
return False, "reference_source_mismatch"
|
|
11385
|
+
return True, None
|
|
11386
|
+
if source_type == FieldType.relation.value:
|
|
11387
|
+
source_target = _relation_target_app_key(source_field)
|
|
11388
|
+
target_target = _relation_target_app_key(target_field)
|
|
11389
|
+
if source_target and target_target and source_target != target_target:
|
|
11390
|
+
return False, "relation_target_app_mismatch"
|
|
11391
|
+
return True, None
|
|
11392
|
+
return False, "relation_requires_record_id_or_relation"
|
|
11393
|
+
if source_type == FieldType.relation.value:
|
|
11394
|
+
if target_type == "system_record_id":
|
|
11395
|
+
return True, None
|
|
11396
|
+
return False, "relation_only_matches_relation_or_record_id"
|
|
11397
|
+
|
|
11398
|
+
if source_type == "system_record_id":
|
|
11399
|
+
if target_type in {
|
|
11400
|
+
"system_record_id",
|
|
11401
|
+
FieldType.text.value,
|
|
11402
|
+
FieldType.long_text.value,
|
|
11403
|
+
FieldType.number.value,
|
|
11404
|
+
FieldType.amount.value,
|
|
11405
|
+
}:
|
|
11406
|
+
return True, None
|
|
11407
|
+
return False, "record_id_requires_relation_or_id_compatible_target"
|
|
11408
|
+
if target_type == "system_record_id":
|
|
11409
|
+
if source_type in {
|
|
11410
|
+
"system_record_id",
|
|
11411
|
+
FieldType.text.value,
|
|
11412
|
+
FieldType.long_text.value,
|
|
11413
|
+
FieldType.number.value,
|
|
11414
|
+
FieldType.amount.value,
|
|
11415
|
+
}:
|
|
11416
|
+
return True, None
|
|
11417
|
+
return False, "record_id_requires_id_compatible_source"
|
|
11418
|
+
|
|
11419
|
+
if source_type == "system_record_number":
|
|
11420
|
+
if target_type in {
|
|
11421
|
+
"system_record_number",
|
|
11422
|
+
FieldType.text.value,
|
|
11423
|
+
FieldType.long_text.value,
|
|
11424
|
+
FieldType.number.value,
|
|
11425
|
+
FieldType.amount.value,
|
|
11426
|
+
}:
|
|
11427
|
+
return True, None
|
|
11428
|
+
return False, "record_number_requires_text_or_number_target"
|
|
11429
|
+
if target_type == "system_record_number":
|
|
11430
|
+
if source_type in {
|
|
11431
|
+
"system_record_number",
|
|
11432
|
+
FieldType.text.value,
|
|
11433
|
+
FieldType.long_text.value,
|
|
11434
|
+
FieldType.number.value,
|
|
11435
|
+
FieldType.amount.value,
|
|
11436
|
+
}:
|
|
11437
|
+
return True, None
|
|
11438
|
+
return False, "record_number_requires_text_or_number_source"
|
|
11439
|
+
|
|
11440
|
+
exact_only = {FieldType.member.value, FieldType.department.value}
|
|
11441
|
+
if source_type in exact_only or target_type in exact_only:
|
|
11442
|
+
return (source_type == target_type, None if source_type == target_type else "member_department_require_same_type")
|
|
11443
|
+
|
|
10233
11444
|
compatible_groups = [
|
|
10234
11445
|
{FieldType.text.value, FieldType.long_text.value, FieldType.phone.value, FieldType.email.value},
|
|
10235
11446
|
{FieldType.number.value, FieldType.amount.value},
|
|
10236
11447
|
{FieldType.date.value, FieldType.datetime.value},
|
|
10237
11448
|
{FieldType.single_select.value, FieldType.multi_select.value, FieldType.boolean.value},
|
|
10238
11449
|
]
|
|
10239
|
-
|
|
10240
|
-
|
|
10241
|
-
|
|
10242
|
-
|
|
10243
|
-
|
|
10244
|
-
|
|
10245
|
-
|
|
11450
|
+
if source_type == target_type or any(source_type in group and target_type in group for group in compatible_groups):
|
|
11451
|
+
return True, None
|
|
11452
|
+
return False, "different_type_family"
|
|
11453
|
+
|
|
11454
|
+
|
|
11455
|
+
def _match_mapping_operator_to_judge_type(operator: Any, *, reason_path: str) -> tuple[int, dict[str, Any] | None]:
|
|
11456
|
+
normalized = str(operator or "eq").strip().lower()
|
|
11457
|
+
aliases = {
|
|
11458
|
+
"eq": JUDGE_EQUAL,
|
|
11459
|
+
"equal": JUDGE_EQUAL,
|
|
11460
|
+
"=": JUDGE_EQUAL,
|
|
11461
|
+
"==": JUDGE_EQUAL,
|
|
11462
|
+
"neq": JUDGE_UNEQUAL,
|
|
11463
|
+
"ne": JUDGE_UNEQUAL,
|
|
11464
|
+
"!=": JUDGE_UNEQUAL,
|
|
11465
|
+
"not_eq": JUDGE_UNEQUAL,
|
|
11466
|
+
"not_equal": JUDGE_UNEQUAL,
|
|
11467
|
+
"gte": JUDGE_GREATER_OR_EQUAL,
|
|
11468
|
+
"ge": JUDGE_GREATER_OR_EQUAL,
|
|
11469
|
+
">=": JUDGE_GREATER_OR_EQUAL,
|
|
11470
|
+
"greater_or_equal": JUDGE_GREATER_OR_EQUAL,
|
|
11471
|
+
"lte": JUDGE_LESS_OR_EQUAL,
|
|
11472
|
+
"le": JUDGE_LESS_OR_EQUAL,
|
|
11473
|
+
"<=": JUDGE_LESS_OR_EQUAL,
|
|
11474
|
+
"less_or_equal": JUDGE_LESS_OR_EQUAL,
|
|
11475
|
+
"contains": JUDGE_FUZZY_MATCH,
|
|
11476
|
+
"like": JUDGE_FUZZY_MATCH,
|
|
11477
|
+
"fuzzy": JUDGE_FUZZY_MATCH,
|
|
11478
|
+
"fuzzy_match": JUDGE_FUZZY_MATCH,
|
|
11479
|
+
"in": JUDGE_EQUAL_ANY,
|
|
11480
|
+
"any": JUDGE_EQUAL_ANY,
|
|
11481
|
+
"equal_any": JUDGE_EQUAL_ANY,
|
|
11482
|
+
"include_any": JUDGE_INCLUDE_ANY,
|
|
11483
|
+
"includes_any": JUDGE_INCLUDE_ANY,
|
|
11484
|
+
}
|
|
11485
|
+
if normalized in aliases:
|
|
11486
|
+
return aliases[normalized], None
|
|
11487
|
+
return JUDGE_EQUAL, {
|
|
11488
|
+
"error_code": "INVALID_MATCH_MAPPING_OPERATOR",
|
|
10246
11489
|
"reason_path": reason_path,
|
|
10247
|
-
"
|
|
10248
|
-
"
|
|
10249
|
-
"message": "
|
|
10250
|
-
"next_action": "
|
|
11490
|
+
"operator": operator,
|
|
11491
|
+
"allowed_values": sorted(aliases),
|
|
11492
|
+
"message": "match_mappings[].operator is not supported",
|
|
11493
|
+
"next_action": "retry with one of the allowed operator values, such as eq, contains, in, gte, or lte",
|
|
10251
11494
|
}
|
|
10252
11495
|
|
|
10253
11496
|
|
|
10254
11497
|
def _custom_button_question_ref(field: dict[str, Any]) -> dict[str, Any]:
|
|
10255
11498
|
return {
|
|
10256
|
-
"que_id":
|
|
11499
|
+
"que_id": _coerce_any_int(field.get("que_id")),
|
|
10257
11500
|
"que_title": str(field.get("name") or ""),
|
|
10258
11501
|
"que_type": _coerce_nonnegative_int(field.get("que_type")),
|
|
10259
11502
|
}
|
|
10260
11503
|
|
|
10261
11504
|
|
|
10262
11505
|
def _custom_button_field_mapping_rule(*, source_field: dict[str, Any], target_field: dict[str, Any]) -> dict[str, Any]:
|
|
10263
|
-
source_que_id =
|
|
11506
|
+
source_que_id = _coerce_any_int(source_field.get("que_id"))
|
|
10264
11507
|
return {
|
|
10265
11508
|
**_custom_button_question_ref(target_field),
|
|
10266
11509
|
"match_type": MATCH_TYPE_QUESTION,
|
|
@@ -10285,6 +11528,36 @@ def _custom_button_default_value_rule(*, target_field: dict[str, Any], value_det
|
|
|
10285
11528
|
}
|
|
10286
11529
|
|
|
10287
11530
|
|
|
11531
|
+
def _summarize_compiled_match_rules(rules: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
11532
|
+
return [
|
|
11533
|
+
_compact_dict(
|
|
11534
|
+
{
|
|
11535
|
+
"target_field": {
|
|
11536
|
+
"title": rule.get("que_title"),
|
|
11537
|
+
"field_id": rule.get("que_id"),
|
|
11538
|
+
"que_id": rule.get("que_id"),
|
|
11539
|
+
"que_type": rule.get("que_type"),
|
|
11540
|
+
},
|
|
11541
|
+
"match_type": rule.get("match_type"),
|
|
11542
|
+
"judge_type": rule.get("judge_type"),
|
|
11543
|
+
"source_field": (
|
|
11544
|
+
{
|
|
11545
|
+
"title": (rule.get("judge_que_detail") or {}).get("que_title"),
|
|
11546
|
+
"field_id": rule.get("judge_que_id"),
|
|
11547
|
+
"que_id": rule.get("judge_que_id"),
|
|
11548
|
+
"que_type": rule.get("judge_que_type"),
|
|
11549
|
+
}
|
|
11550
|
+
if rule.get("match_type") == MATCH_TYPE_QUESTION
|
|
11551
|
+
else None
|
|
11552
|
+
),
|
|
11553
|
+
"static_values": rule.get("judge_value_details") or rule.get("judge_values") if rule.get("match_type") != MATCH_TYPE_QUESTION else None,
|
|
11554
|
+
}
|
|
11555
|
+
)
|
|
11556
|
+
for rule in rules
|
|
11557
|
+
if isinstance(rule, dict)
|
|
11558
|
+
]
|
|
11559
|
+
|
|
11560
|
+
|
|
10288
11561
|
def _resolve_custom_button_option_detail(*, target_field: dict[str, Any], value: Any) -> dict[str, Any] | None:
|
|
10289
11562
|
option_details = [item for item in target_field.get("option_details") or [] if isinstance(item, dict)]
|
|
10290
11563
|
value_id = _coerce_positive_int(value.get("id", value.get("opt_id", value.get("optId"))) if isinstance(value, dict) else value)
|
|
@@ -10438,30 +11711,172 @@ def _normalize_custom_button_summary(item: dict[str, Any]) -> dict[str, Any]:
|
|
|
10438
11711
|
return normalized
|
|
10439
11712
|
|
|
10440
11713
|
|
|
10441
|
-
def _normalize_custom_button_detail(item: dict[str, Any]) -> dict[str, Any]:
|
|
10442
|
-
normalized = _normalize_custom_button_summary(item)
|
|
10443
|
-
normalized.update(
|
|
10444
|
-
{
|
|
10445
|
-
"trigger_action": str(item.get("trigger_action") or item.get("triggerAction") or "").strip() or None,
|
|
10446
|
-
"trigger_link_url": str(item.get("trigger_link_url") or item.get("triggerLinkUrl") or "").strip() or None,
|
|
10447
|
-
}
|
|
10448
|
-
)
|
|
10449
|
-
trigger_add_data_config = item.get("trigger_add_data_config")
|
|
10450
|
-
if not isinstance(trigger_add_data_config, dict):
|
|
10451
|
-
trigger_add_data_config = item.get("triggerAddDataConfig")
|
|
10452
|
-
if isinstance(trigger_add_data_config, dict):
|
|
10453
|
-
normalized["trigger_add_data_config"] =
|
|
10454
|
-
external_qrobot_config = item.get("external_qrobot_config")
|
|
10455
|
-
if not isinstance(external_qrobot_config, dict):
|
|
10456
|
-
external_qrobot_config = item.get("customButtonExternalQRobotRelationVO")
|
|
10457
|
-
if isinstance(external_qrobot_config, dict):
|
|
10458
|
-
normalized["external_qrobot_config"] = deepcopy(external_qrobot_config)
|
|
10459
|
-
trigger_wings_config = item.get("trigger_wings_config")
|
|
10460
|
-
if not isinstance(trigger_wings_config, dict):
|
|
10461
|
-
trigger_wings_config = item.get("triggerWingsConfig")
|
|
10462
|
-
if isinstance(trigger_wings_config, dict):
|
|
10463
|
-
normalized["trigger_wings_config"] = deepcopy(trigger_wings_config)
|
|
10464
|
-
return normalized
|
|
11714
|
+
def _normalize_custom_button_detail(item: dict[str, Any]) -> dict[str, Any]:
|
|
11715
|
+
normalized = _normalize_custom_button_summary(item)
|
|
11716
|
+
normalized.update(
|
|
11717
|
+
{
|
|
11718
|
+
"trigger_action": str(item.get("trigger_action") or item.get("triggerAction") or "").strip() or None,
|
|
11719
|
+
"trigger_link_url": str(item.get("trigger_link_url") or item.get("triggerLinkUrl") or "").strip() or None,
|
|
11720
|
+
}
|
|
11721
|
+
)
|
|
11722
|
+
trigger_add_data_config = item.get("trigger_add_data_config")
|
|
11723
|
+
if not isinstance(trigger_add_data_config, dict):
|
|
11724
|
+
trigger_add_data_config = item.get("triggerAddDataConfig")
|
|
11725
|
+
if isinstance(trigger_add_data_config, dict):
|
|
11726
|
+
normalized["trigger_add_data_config"] = _normalize_custom_button_add_data_config_for_public(trigger_add_data_config)
|
|
11727
|
+
external_qrobot_config = item.get("external_qrobot_config")
|
|
11728
|
+
if not isinstance(external_qrobot_config, dict):
|
|
11729
|
+
external_qrobot_config = item.get("customButtonExternalQRobotRelationVO")
|
|
11730
|
+
if isinstance(external_qrobot_config, dict):
|
|
11731
|
+
normalized["external_qrobot_config"] = deepcopy(external_qrobot_config)
|
|
11732
|
+
trigger_wings_config = item.get("trigger_wings_config")
|
|
11733
|
+
if not isinstance(trigger_wings_config, dict):
|
|
11734
|
+
trigger_wings_config = item.get("triggerWingsConfig")
|
|
11735
|
+
if isinstance(trigger_wings_config, dict):
|
|
11736
|
+
normalized["trigger_wings_config"] = deepcopy(trigger_wings_config)
|
|
11737
|
+
return normalized
|
|
11738
|
+
|
|
11739
|
+
|
|
11740
|
+
_CUSTOM_BUTTON_PARTIAL_PATCH_KEY_ALIASES = {
|
|
11741
|
+
"button_text": "button_text",
|
|
11742
|
+
"buttonText": "button_text",
|
|
11743
|
+
"name": "button_text",
|
|
11744
|
+
"text": "button_text",
|
|
11745
|
+
"background_color": "background_color",
|
|
11746
|
+
"backgroundColor": "background_color",
|
|
11747
|
+
"text_color": "text_color",
|
|
11748
|
+
"textColor": "text_color",
|
|
11749
|
+
"button_icon": "button_icon",
|
|
11750
|
+
"buttonIcon": "button_icon",
|
|
11751
|
+
"style_preset": "style_preset",
|
|
11752
|
+
"stylePreset": "style_preset",
|
|
11753
|
+
"trigger_action": "trigger_action",
|
|
11754
|
+
"triggerAction": "trigger_action",
|
|
11755
|
+
"trigger_link_url": "trigger_link_url",
|
|
11756
|
+
"triggerLinkUrl": "trigger_link_url",
|
|
11757
|
+
"trigger_add_data_config": "trigger_add_data_config",
|
|
11758
|
+
"triggerAddDataConfig": "trigger_add_data_config",
|
|
11759
|
+
"add_data_config": "trigger_add_data_config",
|
|
11760
|
+
"addDataConfig": "trigger_add_data_config",
|
|
11761
|
+
"external_qrobot_config": "external_qrobot_config",
|
|
11762
|
+
"externalQrobotConfig": "external_qrobot_config",
|
|
11763
|
+
"customButtonExternalQRobotRelationVO": "external_qrobot_config",
|
|
11764
|
+
"trigger_wings_config": "trigger_wings_config",
|
|
11765
|
+
"triggerWingsConfig": "trigger_wings_config",
|
|
11766
|
+
}
|
|
11767
|
+
|
|
11768
|
+
_CUSTOM_BUTTON_PARTIAL_SET_KEYS = {
|
|
11769
|
+
"button_text",
|
|
11770
|
+
"background_color",
|
|
11771
|
+
"text_color",
|
|
11772
|
+
"button_icon",
|
|
11773
|
+
"style_preset",
|
|
11774
|
+
"trigger_action",
|
|
11775
|
+
"trigger_link_url",
|
|
11776
|
+
"trigger_add_data_config",
|
|
11777
|
+
"external_qrobot_config",
|
|
11778
|
+
"trigger_wings_config",
|
|
11779
|
+
}
|
|
11780
|
+
|
|
11781
|
+
_CUSTOM_BUTTON_PARTIAL_UNSET_KEYS = {
|
|
11782
|
+
"trigger_link_url",
|
|
11783
|
+
"trigger_add_data_config",
|
|
11784
|
+
"external_qrobot_config",
|
|
11785
|
+
"trigger_wings_config",
|
|
11786
|
+
}
|
|
11787
|
+
|
|
11788
|
+
|
|
11789
|
+
def _canonical_custom_button_partial_patch_key(key: Any) -> str:
|
|
11790
|
+
raw = str(key or "").strip()
|
|
11791
|
+
return _CUSTOM_BUTTON_PARTIAL_PATCH_KEY_ALIASES.get(raw, raw)
|
|
11792
|
+
|
|
11793
|
+
|
|
11794
|
+
def _normalize_custom_button_partial_set(raw_set: Any, *, reason_path: str) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
11795
|
+
if not isinstance(raw_set, dict):
|
|
11796
|
+
return {}, [{"error_code": "INVALID_CUSTOM_BUTTON_PATCH_SET", "reason_path": reason_path, "message": "patch_buttons[].set must be an object"}]
|
|
11797
|
+
normalized: dict[str, Any] = {}
|
|
11798
|
+
issues: list[dict[str, Any]] = []
|
|
11799
|
+
for raw_key, value in raw_set.items():
|
|
11800
|
+
key_path = [part for part in str(raw_key or "").strip().split(".") if part]
|
|
11801
|
+
if not key_path:
|
|
11802
|
+
continue
|
|
11803
|
+
root = _canonical_custom_button_partial_patch_key(key_path[0])
|
|
11804
|
+
if root not in _CUSTOM_BUTTON_PARTIAL_SET_KEYS:
|
|
11805
|
+
issues.append(
|
|
11806
|
+
{
|
|
11807
|
+
"error_code": "UNSUPPORTED_CUSTOM_BUTTON_PATCH_FIELD",
|
|
11808
|
+
"reason_path": f"{reason_path}.{raw_key}",
|
|
11809
|
+
"field": raw_key,
|
|
11810
|
+
"allowed_fields": sorted(_CUSTOM_BUTTON_PARTIAL_SET_KEYS),
|
|
11811
|
+
}
|
|
11812
|
+
)
|
|
11813
|
+
continue
|
|
11814
|
+
if len(key_path) == 1:
|
|
11815
|
+
normalized[root] = value
|
|
11816
|
+
continue
|
|
11817
|
+
target = normalized.setdefault(root, {})
|
|
11818
|
+
if not isinstance(target, dict):
|
|
11819
|
+
issues.append(
|
|
11820
|
+
{
|
|
11821
|
+
"error_code": "INVALID_CUSTOM_BUTTON_PATCH_PATH",
|
|
11822
|
+
"reason_path": f"{reason_path}.{raw_key}",
|
|
11823
|
+
"message": "cannot combine a scalar replacement and nested patch for the same field",
|
|
11824
|
+
}
|
|
11825
|
+
)
|
|
11826
|
+
continue
|
|
11827
|
+
cursor = target
|
|
11828
|
+
for part in key_path[1:-1]:
|
|
11829
|
+
next_value = cursor.setdefault(part, {})
|
|
11830
|
+
if not isinstance(next_value, dict):
|
|
11831
|
+
issues.append({"error_code": "INVALID_CUSTOM_BUTTON_PATCH_PATH", "reason_path": f"{reason_path}.{raw_key}"})
|
|
11832
|
+
break
|
|
11833
|
+
cursor = next_value
|
|
11834
|
+
else:
|
|
11835
|
+
cursor[key_path[-1]] = value
|
|
11836
|
+
return normalized, issues
|
|
11837
|
+
|
|
11838
|
+
|
|
11839
|
+
def _normalize_custom_button_partial_unset(raw_unset: Any, *, reason_path: str) -> tuple[set[str], list[dict[str, Any]]]:
|
|
11840
|
+
if not isinstance(raw_unset, list):
|
|
11841
|
+
return set(), [{"error_code": "INVALID_CUSTOM_BUTTON_PATCH_UNSET", "reason_path": reason_path, "message": "patch_buttons[].unset must be a list"}]
|
|
11842
|
+
normalized: set[str] = set()
|
|
11843
|
+
issues: list[dict[str, Any]] = []
|
|
11844
|
+
for index, raw_key in enumerate(raw_unset):
|
|
11845
|
+
root = _canonical_custom_button_partial_patch_key(str(raw_key or "").strip().split(".")[0])
|
|
11846
|
+
if root not in _CUSTOM_BUTTON_PARTIAL_UNSET_KEYS:
|
|
11847
|
+
issues.append(
|
|
11848
|
+
{
|
|
11849
|
+
"error_code": "UNSUPPORTED_CUSTOM_BUTTON_PATCH_UNSET_FIELD",
|
|
11850
|
+
"reason_path": f"{reason_path}[{index}]",
|
|
11851
|
+
"field": raw_key,
|
|
11852
|
+
"allowed_fields": sorted(_CUSTOM_BUTTON_PARTIAL_UNSET_KEYS),
|
|
11853
|
+
}
|
|
11854
|
+
)
|
|
11855
|
+
continue
|
|
11856
|
+
normalized.add(root)
|
|
11857
|
+
return normalized, issues
|
|
11858
|
+
|
|
11859
|
+
|
|
11860
|
+
def _custom_button_upsert_payload_from_detail(
|
|
11861
|
+
detail: dict[str, Any],
|
|
11862
|
+
*,
|
|
11863
|
+
button_id: int,
|
|
11864
|
+
client_key: str | None = None,
|
|
11865
|
+
) -> dict[str, Any]:
|
|
11866
|
+
payload = {
|
|
11867
|
+
"button_id": button_id,
|
|
11868
|
+
"client_key": client_key,
|
|
11869
|
+
"button_text": detail.get("button_text"),
|
|
11870
|
+
"background_color": detail.get("background_color"),
|
|
11871
|
+
"text_color": detail.get("text_color"),
|
|
11872
|
+
"button_icon": detail.get("button_icon"),
|
|
11873
|
+
"trigger_action": detail.get("trigger_action"),
|
|
11874
|
+
"trigger_link_url": detail.get("trigger_link_url"),
|
|
11875
|
+
"trigger_add_data_config": deepcopy(detail.get("trigger_add_data_config")) if isinstance(detail.get("trigger_add_data_config"), dict) else None,
|
|
11876
|
+
"external_qrobot_config": deepcopy(detail.get("external_qrobot_config")) if isinstance(detail.get("external_qrobot_config"), dict) else None,
|
|
11877
|
+
"trigger_wings_config": deepcopy(detail.get("trigger_wings_config")) if isinstance(detail.get("trigger_wings_config"), dict) else None,
|
|
11878
|
+
}
|
|
11879
|
+
return _compact_dict(payload)
|
|
10465
11880
|
|
|
10466
11881
|
|
|
10467
11882
|
def _failed(
|
|
@@ -10947,13 +12362,135 @@ def _bi_field_id_for_field(*, app_key: str, field: dict[str, Any], qingbi_fields
|
|
|
10947
12362
|
|
|
10948
12363
|
|
|
10949
12364
|
def _map_public_chart_type_to_backend(chart_type: PublicChartType) -> str:
|
|
10950
|
-
|
|
12365
|
+
aliases = {
|
|
10951
12366
|
PublicChartType.target: "indicator",
|
|
12367
|
+
PublicChartType.indicator: "indicator",
|
|
10952
12368
|
PublicChartType.pie: "pie",
|
|
10953
12369
|
PublicChartType.bar: "bar",
|
|
10954
12370
|
PublicChartType.line: "line",
|
|
10955
12371
|
PublicChartType.table: "detail",
|
|
10956
|
-
|
|
12372
|
+
PublicChartType.detail: "detail",
|
|
12373
|
+
}
|
|
12374
|
+
return aliases.get(chart_type, chart_type.value)
|
|
12375
|
+
|
|
12376
|
+
|
|
12377
|
+
_CHART_PARTIAL_PATCH_KEY_ALIASES = {
|
|
12378
|
+
"name": "name",
|
|
12379
|
+
"chart_name": "name",
|
|
12380
|
+
"chartName": "name",
|
|
12381
|
+
"type": "chart_type",
|
|
12382
|
+
"chart_type": "chart_type",
|
|
12383
|
+
"chartType": "chart_type",
|
|
12384
|
+
"dimension_fields": "dimension_field_ids",
|
|
12385
|
+
"dimension_field_ids": "dimension_field_ids",
|
|
12386
|
+
"dimensionFieldIds": "dimension_field_ids",
|
|
12387
|
+
"indicator_fields": "indicator_field_ids",
|
|
12388
|
+
"indicator_field_ids": "indicator_field_ids",
|
|
12389
|
+
"indicatorFieldIds": "indicator_field_ids",
|
|
12390
|
+
"metric_field_ids": "indicator_field_ids",
|
|
12391
|
+
"filters": "filters",
|
|
12392
|
+
"question_config": "question_config",
|
|
12393
|
+
"questionConfig": "question_config",
|
|
12394
|
+
"user_config": "user_config",
|
|
12395
|
+
"userConfig": "user_config",
|
|
12396
|
+
"config": "config",
|
|
12397
|
+
"visibility": "visibility",
|
|
12398
|
+
}
|
|
12399
|
+
|
|
12400
|
+
_CHART_PARTIAL_SET_KEYS = {
|
|
12401
|
+
"name",
|
|
12402
|
+
"chart_type",
|
|
12403
|
+
"dimension_field_ids",
|
|
12404
|
+
"indicator_field_ids",
|
|
12405
|
+
"filters",
|
|
12406
|
+
"question_config",
|
|
12407
|
+
"user_config",
|
|
12408
|
+
"config",
|
|
12409
|
+
"visibility",
|
|
12410
|
+
}
|
|
12411
|
+
|
|
12412
|
+
_CHART_PARTIAL_UNSET_KEYS = {"filters", "question_config", "user_config", "visibility"}
|
|
12413
|
+
|
|
12414
|
+
|
|
12415
|
+
def _canonical_chart_partial_patch_key(key: Any) -> str:
|
|
12416
|
+
raw = str(key or "").strip()
|
|
12417
|
+
return _CHART_PARTIAL_PATCH_KEY_ALIASES.get(raw, raw)
|
|
12418
|
+
|
|
12419
|
+
|
|
12420
|
+
def _normalize_chart_partial_set(raw_set: Any, *, reason_path: str) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
12421
|
+
if not isinstance(raw_set, dict):
|
|
12422
|
+
return {}, [{"error_code": "INVALID_CHART_PATCH_SET", "reason_path": reason_path, "message": "patch_charts[].set must be an object"}]
|
|
12423
|
+
normalized: dict[str, Any] = {}
|
|
12424
|
+
issues: list[dict[str, Any]] = []
|
|
12425
|
+
for raw_key, value in raw_set.items():
|
|
12426
|
+
key_path = [part for part in str(raw_key or "").strip().split(".") if part]
|
|
12427
|
+
if not key_path:
|
|
12428
|
+
continue
|
|
12429
|
+
root = _canonical_chart_partial_patch_key(key_path[0])
|
|
12430
|
+
if root not in _CHART_PARTIAL_SET_KEYS:
|
|
12431
|
+
issues.append(
|
|
12432
|
+
{
|
|
12433
|
+
"error_code": "UNSUPPORTED_CHART_PATCH_FIELD",
|
|
12434
|
+
"reason_path": f"{reason_path}.{raw_key}",
|
|
12435
|
+
"field": raw_key,
|
|
12436
|
+
"allowed_fields": sorted(_CHART_PARTIAL_SET_KEYS),
|
|
12437
|
+
}
|
|
12438
|
+
)
|
|
12439
|
+
continue
|
|
12440
|
+
if len(key_path) == 1:
|
|
12441
|
+
normalized[root] = value
|
|
12442
|
+
continue
|
|
12443
|
+
target = normalized.setdefault(root, {})
|
|
12444
|
+
if not isinstance(target, dict):
|
|
12445
|
+
issues.append({"error_code": "INVALID_CHART_PATCH_PATH", "reason_path": f"{reason_path}.{raw_key}"})
|
|
12446
|
+
continue
|
|
12447
|
+
cursor = target
|
|
12448
|
+
for part in key_path[1:-1]:
|
|
12449
|
+
next_value = cursor.setdefault(part, {})
|
|
12450
|
+
if not isinstance(next_value, dict):
|
|
12451
|
+
issues.append({"error_code": "INVALID_CHART_PATCH_PATH", "reason_path": f"{reason_path}.{raw_key}"})
|
|
12452
|
+
break
|
|
12453
|
+
cursor = next_value
|
|
12454
|
+
else:
|
|
12455
|
+
cursor[key_path[-1]] = value
|
|
12456
|
+
return normalized, issues
|
|
12457
|
+
|
|
12458
|
+
|
|
12459
|
+
def _normalize_chart_partial_unset(raw_unset: Any, *, reason_path: str) -> tuple[set[str], list[dict[str, Any]]]:
|
|
12460
|
+
if not isinstance(raw_unset, list):
|
|
12461
|
+
return set(), [{"error_code": "INVALID_CHART_PATCH_UNSET", "reason_path": reason_path, "message": "patch_charts[].unset must be a list"}]
|
|
12462
|
+
normalized: set[str] = set()
|
|
12463
|
+
issues: list[dict[str, Any]] = []
|
|
12464
|
+
for index, raw_key in enumerate(raw_unset):
|
|
12465
|
+
root = _canonical_chart_partial_patch_key(str(raw_key or "").strip().split(".")[0])
|
|
12466
|
+
if root not in _CHART_PARTIAL_UNSET_KEYS:
|
|
12467
|
+
issues.append(
|
|
12468
|
+
{
|
|
12469
|
+
"error_code": "UNSUPPORTED_CHART_PATCH_UNSET_FIELD",
|
|
12470
|
+
"reason_path": f"{reason_path}[{index}]",
|
|
12471
|
+
"field": raw_key,
|
|
12472
|
+
"allowed_fields": sorted(_CHART_PARTIAL_UNSET_KEYS),
|
|
12473
|
+
}
|
|
12474
|
+
)
|
|
12475
|
+
continue
|
|
12476
|
+
normalized.add(root)
|
|
12477
|
+
return normalized, issues
|
|
12478
|
+
|
|
12479
|
+
|
|
12480
|
+
def _chart_upsert_payload_from_existing(
|
|
12481
|
+
*,
|
|
12482
|
+
chart_id: str,
|
|
12483
|
+
base: dict[str, Any],
|
|
12484
|
+
config: dict[str, Any],
|
|
12485
|
+
) -> dict[str, Any]:
|
|
12486
|
+
return _compact_dict(
|
|
12487
|
+
{
|
|
12488
|
+
"chart_id": chart_id,
|
|
12489
|
+
"name": str(base.get("chartName") or config.get("chartName") or chart_id).strip() or chart_id,
|
|
12490
|
+
"chart_type": _public_chart_type_from_backend(base.get("chartType") or config.get("chartType")),
|
|
12491
|
+
"config": deepcopy(config),
|
|
12492
|
+
}
|
|
12493
|
+
)
|
|
10957
12494
|
|
|
10958
12495
|
|
|
10959
12496
|
def _qingbi_field_type_from_public_field(field_type: str | None) -> str:
|
|
@@ -11070,6 +12607,25 @@ def _build_public_metric_fields(
|
|
|
11070
12607
|
return metrics or [_default_public_total_metric()]
|
|
11071
12608
|
|
|
11072
12609
|
|
|
12610
|
+
def _split_axis_metric_fields(metrics: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
12611
|
+
if not metrics:
|
|
12612
|
+
default_metric = _default_public_total_metric()
|
|
12613
|
+
return [deepcopy(default_metric)], [deepcopy(default_metric)]
|
|
12614
|
+
if len(metrics) == 1:
|
|
12615
|
+
return [deepcopy(metrics[0])], [deepcopy(metrics[0])]
|
|
12616
|
+
return [deepcopy(metrics[0])], [deepcopy(metrics[1])]
|
|
12617
|
+
|
|
12618
|
+
|
|
12619
|
+
def _two_gauge_metric_fields(metrics: list[dict[str, Any]], *, requested_metric_count: int) -> list[dict[str, Any]]:
|
|
12620
|
+
if not metrics:
|
|
12621
|
+
return []
|
|
12622
|
+
if len(metrics) == 1:
|
|
12623
|
+
if requested_metric_count <= 0:
|
|
12624
|
+
return [deepcopy(metrics[0])]
|
|
12625
|
+
return [deepcopy(metrics[0]), _default_public_total_metric()]
|
|
12626
|
+
return [deepcopy(metrics[0]), deepcopy(metrics[1])]
|
|
12627
|
+
|
|
12628
|
+
|
|
11073
12629
|
def _build_public_chart_filter_matrix(
|
|
11074
12630
|
rules: list[Any],
|
|
11075
12631
|
*,
|
|
@@ -11117,6 +12673,13 @@ def _build_public_chart_config_payload(
|
|
|
11117
12673
|
qingbi_fields_by_id: dict[str, dict[str, Any]],
|
|
11118
12674
|
) -> dict[str, Any]:
|
|
11119
12675
|
config = deepcopy(patch.config)
|
|
12676
|
+
explicit_fields = set(getattr(patch, "model_fields_set", set()) or set())
|
|
12677
|
+
if "dimension_field_ids" in explicit_fields:
|
|
12678
|
+
config.pop("selectedDimensions", None)
|
|
12679
|
+
if "indicator_field_ids" in explicit_fields:
|
|
12680
|
+
config.pop("selectedMetrics", None)
|
|
12681
|
+
if "filters" in explicit_fields:
|
|
12682
|
+
config.pop("beforeAggregationFilterMatrix", None)
|
|
11120
12683
|
aggregate = str(config.pop("aggregate", "count") or "count").lower()
|
|
11121
12684
|
before_filters = deepcopy(config.pop("beforeAggregationFilterMatrix", []))
|
|
11122
12685
|
after_filters = deepcopy(config.pop("afterAggregationFilterMatrix", []))
|
|
@@ -11131,23 +12694,28 @@ def _build_public_chart_config_payload(
|
|
|
11131
12694
|
for selector in list(config.pop("query_condition_field_ids", []) or []):
|
|
11132
12695
|
field = _resolve_public_field(selector, field_lookup=field_lookup)
|
|
11133
12696
|
query_condition_field_ids.append(_bi_field_id_for_field(app_key=app_key, field=field, qingbi_fields_by_id=qingbi_fields_by_id))
|
|
12697
|
+
backend_chart_type = _map_public_chart_type_to_backend(patch.chart_type)
|
|
12698
|
+
if backend_chart_type == "gauge" and not patch.indicator_field_ids and "selectedMetrics" not in config:
|
|
12699
|
+
raise ValueError("gauge charts require at least one indicator_field_ids value; pass one metric and the CLI will pair it with 数据总量")
|
|
12700
|
+
selected_dimensions = _build_public_dimension_fields(
|
|
12701
|
+
patch.dimension_field_ids,
|
|
12702
|
+
app_key=app_key,
|
|
12703
|
+
field_lookup=field_lookup,
|
|
12704
|
+
qingbi_fields_by_id=qingbi_fields_by_id,
|
|
12705
|
+
)
|
|
12706
|
+
selected_metrics = _build_public_metric_fields(
|
|
12707
|
+
patch.indicator_field_ids,
|
|
12708
|
+
app_key=app_key,
|
|
12709
|
+
field_lookup=field_lookup,
|
|
12710
|
+
qingbi_fields_by_id=qingbi_fields_by_id,
|
|
12711
|
+
aggregate=aggregate,
|
|
12712
|
+
)
|
|
11134
12713
|
payload: dict[str, Any] = {
|
|
11135
12714
|
"chartName": patch.name,
|
|
11136
|
-
"chartType":
|
|
12715
|
+
"chartType": backend_chart_type,
|
|
11137
12716
|
"dataSource": {"dataSourceId": app_key, "dataSourceType": "qingflow"},
|
|
11138
|
-
"selectedDimensions":
|
|
11139
|
-
|
|
11140
|
-
app_key=app_key,
|
|
11141
|
-
field_lookup=field_lookup,
|
|
11142
|
-
qingbi_fields_by_id=qingbi_fields_by_id,
|
|
11143
|
-
),
|
|
11144
|
-
"selectedMetrics": _build_public_metric_fields(
|
|
11145
|
-
patch.indicator_field_ids,
|
|
11146
|
-
app_key=app_key,
|
|
11147
|
-
field_lookup=field_lookup,
|
|
11148
|
-
qingbi_fields_by_id=qingbi_fields_by_id,
|
|
11149
|
-
aggregate=aggregate,
|
|
11150
|
-
),
|
|
12717
|
+
"selectedDimensions": selected_dimensions,
|
|
12718
|
+
"selectedMetrics": selected_metrics,
|
|
11151
12719
|
"beforeAggregationFilterMatrix": before_filters,
|
|
11152
12720
|
"afterAggregationFilterMatrix": after_filters,
|
|
11153
12721
|
"chartStyleConfigs": deepcopy(config.pop("chartStyleConfigs", [])),
|
|
@@ -11163,6 +12731,23 @@ def _build_public_chart_config_payload(
|
|
|
11163
12731
|
"queryConditionStatus": bool(config.pop("queryConditionStatus", bool(query_condition_field_ids))),
|
|
11164
12732
|
"queryConditionExact": bool(config.pop("queryConditionExact", False)),
|
|
11165
12733
|
}
|
|
12734
|
+
if backend_chart_type == "summary":
|
|
12735
|
+
payload.pop("selectedDimensions", None)
|
|
12736
|
+
payload.setdefault("xDimensions", deepcopy(selected_dimensions))
|
|
12737
|
+
payload.setdefault("yDimensions", [])
|
|
12738
|
+
elif backend_chart_type == "scatter":
|
|
12739
|
+
x_metrics, y_metrics = _split_axis_metric_fields(selected_metrics)
|
|
12740
|
+
payload.pop("selectedMetrics", None)
|
|
12741
|
+
payload.setdefault("xMetrics", x_metrics)
|
|
12742
|
+
payload.setdefault("yMetrics", y_metrics)
|
|
12743
|
+
elif backend_chart_type == "dualaxes":
|
|
12744
|
+
left_metrics, right_metrics = _split_axis_metric_fields(selected_metrics)
|
|
12745
|
+
payload.pop("selectedMetrics", None)
|
|
12746
|
+
payload.setdefault("leftMetrics", left_metrics)
|
|
12747
|
+
payload.setdefault("rightMetrics", right_metrics)
|
|
12748
|
+
elif backend_chart_type == "gauge":
|
|
12749
|
+
payload["selectedDimensions"] = []
|
|
12750
|
+
payload["selectedMetrics"] = _two_gauge_metric_fields(selected_metrics, requested_metric_count=len(patch.indicator_field_ids or []))
|
|
11166
12751
|
for key in (
|
|
11167
12752
|
"selectedTime",
|
|
11168
12753
|
"xDimensions",
|
|
@@ -11185,6 +12770,46 @@ def _chart_patch_updates_chart_config(patch: ChartUpsertPatch) -> bool:
|
|
|
11185
12770
|
return bool({"dimension_field_ids", "indicator_field_ids", "filters", "config"} & explicit_fields)
|
|
11186
12771
|
|
|
11187
12772
|
|
|
12773
|
+
def _chart_patch_dataset_source_type(patch: ChartUpsertPatch) -> str:
|
|
12774
|
+
config = patch.config if isinstance(patch.config, dict) else {}
|
|
12775
|
+
candidates = [
|
|
12776
|
+
config.get("dataSourceType"),
|
|
12777
|
+
config.get("data_source_type"),
|
|
12778
|
+
]
|
|
12779
|
+
data_source = config.get("dataSource")
|
|
12780
|
+
if isinstance(data_source, dict):
|
|
12781
|
+
candidates.extend([data_source.get("dataSourceType"), data_source.get("data_source_type"), data_source.get("type")])
|
|
12782
|
+
data_source = config.get("data_source")
|
|
12783
|
+
if isinstance(data_source, dict):
|
|
12784
|
+
candidates.extend([data_source.get("dataSourceType"), data_source.get("data_source_type"), data_source.get("type")])
|
|
12785
|
+
for candidate in candidates:
|
|
12786
|
+
normalized = str(candidate or "").strip().lower()
|
|
12787
|
+
if not normalized:
|
|
12788
|
+
continue
|
|
12789
|
+
if normalized not in {"qingflow", "app"}:
|
|
12790
|
+
return normalized
|
|
12791
|
+
return ""
|
|
12792
|
+
|
|
12793
|
+
|
|
12794
|
+
def _chart_item_dataset_source_type(item: dict[str, Any]) -> str:
|
|
12795
|
+
candidates = [
|
|
12796
|
+
item.get("dataSourceType"),
|
|
12797
|
+
item.get("data_source_type"),
|
|
12798
|
+
item.get("sourceType"),
|
|
12799
|
+
item.get("source_type"),
|
|
12800
|
+
]
|
|
12801
|
+
data_source = item.get("dataSource")
|
|
12802
|
+
if isinstance(data_source, dict):
|
|
12803
|
+
candidates.extend([data_source.get("dataSourceType"), data_source.get("data_source_type"), data_source.get("type")])
|
|
12804
|
+
for candidate in candidates:
|
|
12805
|
+
normalized = str(candidate or "").strip().lower()
|
|
12806
|
+
if not normalized:
|
|
12807
|
+
continue
|
|
12808
|
+
if normalized not in {"qingflow", "app", "bi_qingflow"}:
|
|
12809
|
+
return normalized
|
|
12810
|
+
return ""
|
|
12811
|
+
|
|
12812
|
+
|
|
11188
12813
|
def _build_public_portal_base_payload(
|
|
11189
12814
|
*,
|
|
11190
12815
|
dash_name: str,
|
|
@@ -11255,16 +12880,33 @@ def _normalize_backend_chart_type(value: Any) -> str:
|
|
|
11255
12880
|
"12": "dualaxes",
|
|
11256
12881
|
"13": "map",
|
|
11257
12882
|
"14": "timeline",
|
|
12883
|
+
"15": "area",
|
|
12884
|
+
"16": "stacked_column",
|
|
12885
|
+
"17": "stacked_bar",
|
|
12886
|
+
"18": "rose",
|
|
12887
|
+
"19": "stacked_area",
|
|
12888
|
+
"20": "pct_stack_col",
|
|
12889
|
+
"21": "pct_stack_bar",
|
|
12890
|
+
"22": "pct_stack_area",
|
|
12891
|
+
"23": "waterfall",
|
|
12892
|
+
"24": "gauge",
|
|
12893
|
+
"25": "heatmap",
|
|
12894
|
+
"26": "histogram",
|
|
12895
|
+
"27": "treemap",
|
|
12896
|
+
}
|
|
12897
|
+
aliases = {
|
|
12898
|
+
"percent_stacked_column": "pct_stack_col",
|
|
12899
|
+
"percent_stacked_bar": "pct_stack_bar",
|
|
12900
|
+
"percent_stacked_area": "pct_stack_area",
|
|
11258
12901
|
}
|
|
11259
|
-
|
|
12902
|
+
normalized = by_code.get(raw, raw.lower())
|
|
12903
|
+
return aliases.get(normalized, normalized)
|
|
11260
12904
|
|
|
11261
12905
|
|
|
11262
12906
|
def _public_chart_type_from_backend(value: Any) -> str:
|
|
11263
12907
|
normalized = _normalize_backend_chart_type(value)
|
|
11264
12908
|
return {
|
|
11265
12909
|
"indicator": PublicChartType.target.value,
|
|
11266
|
-
"summary": PublicChartType.target.value,
|
|
11267
|
-
"columnar": PublicChartType.bar.value,
|
|
11268
12910
|
"detail": PublicChartType.table.value,
|
|
11269
12911
|
}.get(normalized, normalized)
|
|
11270
12912
|
|
|
@@ -17350,7 +18992,9 @@ def _normalize_view_button_type(value: Any) -> str | None:
|
|
|
17350
18992
|
|
|
17351
18993
|
def _normalize_view_button_config_type(value: Any) -> str | None:
|
|
17352
18994
|
normalized = str(value or "").strip().upper()
|
|
17353
|
-
if normalized
|
|
18995
|
+
if normalized == "LIST":
|
|
18996
|
+
return "INSIDE"
|
|
18997
|
+
if normalized in {"TOP", "DETAIL", "INSIDE"}:
|
|
17354
18998
|
return normalized
|
|
17355
18999
|
return None
|
|
17356
19000
|
|
|
@@ -17436,7 +19080,7 @@ def _extract_view_button_entries(config: dict[str, Any]) -> tuple[list[dict[str,
|
|
|
17436
19080
|
if not isinstance(item, dict):
|
|
17437
19081
|
continue
|
|
17438
19082
|
entry = deepcopy(item)
|
|
17439
|
-
entry.setdefault("configType", "
|
|
19083
|
+
entry.setdefault("configType", "INSIDE")
|
|
17440
19084
|
entries.append(entry)
|
|
17441
19085
|
return entries, "buttonConfig"
|
|
17442
19086
|
|
|
@@ -17471,13 +19115,47 @@ def _normalize_view_button_entry(entry: dict[str, Any]) -> dict[str, Any]:
|
|
|
17471
19115
|
normalized[public_key] = deepcopy(value)
|
|
17472
19116
|
trigger_add_data_config = entry.get("triggerAddDataConfig")
|
|
17473
19117
|
if isinstance(trigger_add_data_config, dict):
|
|
17474
|
-
normalized["trigger_add_data_config"] =
|
|
19118
|
+
normalized["trigger_add_data_config"] = _normalize_custom_button_add_data_config_for_public(trigger_add_data_config)
|
|
17475
19119
|
trigger_wings_config = entry.get("triggerWingsConfig")
|
|
17476
19120
|
if isinstance(trigger_wings_config, dict):
|
|
17477
19121
|
normalized["trigger_wings_config"] = deepcopy(trigger_wings_config)
|
|
17478
19122
|
return normalized
|
|
17479
19123
|
|
|
17480
19124
|
|
|
19125
|
+
def _public_view_button_placement(config_type: str | None) -> str | None:
|
|
19126
|
+
normalized = _normalize_view_button_config_type(config_type)
|
|
19127
|
+
if normalized == "TOP":
|
|
19128
|
+
return "header"
|
|
19129
|
+
if normalized == "DETAIL":
|
|
19130
|
+
return "detail"
|
|
19131
|
+
if normalized == "INSIDE":
|
|
19132
|
+
return "list"
|
|
19133
|
+
return None
|
|
19134
|
+
|
|
19135
|
+
|
|
19136
|
+
def _extract_view_buttons_config(config: dict[str, Any]) -> dict[str, Any]:
|
|
19137
|
+
entries, source = _extract_view_button_entries(config)
|
|
19138
|
+
placements: dict[str, list[dict[str, Any]]] = {"header": [], "detail": [], "list": []}
|
|
19139
|
+
items: list[dict[str, Any]] = []
|
|
19140
|
+
for entry in entries:
|
|
19141
|
+
normalized = _normalize_view_button_entry(entry)
|
|
19142
|
+
placement = _public_view_button_placement(normalized.get("config_type"))
|
|
19143
|
+
normalized["placement"] = placement
|
|
19144
|
+
items.append(normalized)
|
|
19145
|
+
if placement:
|
|
19146
|
+
placements.setdefault(placement, []).append(normalized)
|
|
19147
|
+
status = "ok" if items else "none"
|
|
19148
|
+
result: dict[str, Any] = {
|
|
19149
|
+
"status": status,
|
|
19150
|
+
"button_count": len(items),
|
|
19151
|
+
"placements": placements,
|
|
19152
|
+
"items": items,
|
|
19153
|
+
}
|
|
19154
|
+
if source:
|
|
19155
|
+
result["read_source"] = source
|
|
19156
|
+
return result
|
|
19157
|
+
|
|
19158
|
+
|
|
17481
19159
|
def _normalize_view_buttons_for_compare(value: Any) -> list[dict[str, Any]]:
|
|
17482
19160
|
if isinstance(value, dict):
|
|
17483
19161
|
entries, _ = _extract_view_button_entries(value)
|
|
@@ -17612,7 +19290,7 @@ def _canonicalize_view_button_summary_order(items: list[dict[str, Any]]) -> list
|
|
|
17612
19290
|
detail_main_buttons.append(item)
|
|
17613
19291
|
elif config_type == "DETAIL":
|
|
17614
19292
|
detail_more_buttons.append(item)
|
|
17615
|
-
elif config_type == "
|
|
19293
|
+
elif config_type == "INSIDE":
|
|
17616
19294
|
list_buttons.append(item)
|
|
17617
19295
|
else:
|
|
17618
19296
|
other_buttons.append(item)
|
|
@@ -17765,7 +19443,7 @@ def _build_grouped_view_button_config(button_config_dtos: list[dict[str, Any]])
|
|
|
17765
19443
|
being_main = bool(item.get("beingMain", False))
|
|
17766
19444
|
if config_type == "TOP":
|
|
17767
19445
|
grouped["topButtonList"].append(item)
|
|
17768
|
-
elif config_type == "
|
|
19446
|
+
elif config_type == "INSIDE":
|
|
17769
19447
|
grouped["listButtonList"].append(item)
|
|
17770
19448
|
elif being_main:
|
|
17771
19449
|
grouped["mainButtonDetailList"].append(item)
|
|
@@ -18577,7 +20255,7 @@ def _build_view_associated_resources_payload(
|
|
|
18577
20255
|
"missing_fields": [],
|
|
18578
20256
|
"received": raw_item_id,
|
|
18579
20257
|
"message": "associated_item_id must be an app-level associated resource id from app_get.associated_resources",
|
|
18580
|
-
"next_action": "
|
|
20258
|
+
"next_action": "prefer app_associated_resources_apply for associated report/view display; it can resolve associated_item_id, chart_id/chart_key, or view_key",
|
|
18581
20259
|
}
|
|
18582
20260
|
]
|
|
18583
20261
|
item_ids.append(item_id)
|
|
@@ -18591,7 +20269,7 @@ def _build_view_associated_resources_payload(
|
|
|
18591
20269
|
"invalid_associated_item_ids": missing_ids,
|
|
18592
20270
|
"available_associated_item_ids": sorted(available_by_id),
|
|
18593
20271
|
"message": "associated_resource references ids that are not in the app-level associated resource pool",
|
|
18594
|
-
"next_action": "
|
|
20272
|
+
"next_action": "prefer app_associated_resources_apply for associated report/view display; it can resolve associated_item_id, chart_id/chart_key, or view_key",
|
|
18595
20273
|
}
|
|
18596
20274
|
]
|
|
18597
20275
|
payload = {
|
|
@@ -18663,7 +20341,7 @@ def _associated_resources_config_matches(expected: dict[str, Any], actual: dict[
|
|
|
18663
20341
|
return True
|
|
18664
20342
|
|
|
18665
20343
|
|
|
18666
|
-
def _normalize_associated_resources_payload(payload: Any) -> list[dict[str, Any]]:
|
|
20344
|
+
def _normalize_associated_resources_payload(payload: Any, *, include_raw: bool = False) -> list[dict[str, Any]]:
|
|
18667
20345
|
if isinstance(payload, dict):
|
|
18668
20346
|
raw_items = (
|
|
18669
20347
|
payload.get("associated_resources")
|
|
@@ -18684,7 +20362,7 @@ def _normalize_associated_resources_payload(payload: Any) -> list[dict[str, Any]
|
|
|
18684
20362
|
normalized_items: list[dict[str, Any]] = []
|
|
18685
20363
|
seen_ids: set[int] = set()
|
|
18686
20364
|
for raw_item in raw_items:
|
|
18687
|
-
item = _normalize_associated_resource_item(raw_item)
|
|
20365
|
+
item = _normalize_associated_resource_item(raw_item, include_raw=include_raw)
|
|
18688
20366
|
item_id = _coerce_positive_int(item.get("associated_item_id"))
|
|
18689
20367
|
if item_id is None or item_id in seen_ids:
|
|
18690
20368
|
continue
|
|
@@ -18694,7 +20372,7 @@ def _normalize_associated_resources_payload(payload: Any) -> list[dict[str, Any]
|
|
|
18694
20372
|
return normalized_items
|
|
18695
20373
|
|
|
18696
20374
|
|
|
18697
|
-
def _normalize_associated_resource_item(raw_item: Any) -> dict[str, Any]:
|
|
20375
|
+
def _normalize_associated_resource_item(raw_item: Any, *, include_raw: bool = False) -> dict[str, Any]:
|
|
18698
20376
|
if not isinstance(raw_item, dict):
|
|
18699
20377
|
return {}
|
|
18700
20378
|
item_id = _first_present(raw_item, "associated_item_id", "associatedItemId", "asosChartId", "id")
|
|
@@ -18725,7 +20403,10 @@ def _normalize_associated_resource_item(raw_item: Any) -> dict[str, Any]:
|
|
|
18725
20403
|
if chart_type is not None:
|
|
18726
20404
|
item["chart_type"] = _public_chart_type_from_backend(chart_type)
|
|
18727
20405
|
item["report_source"] = _public_report_source_from_backend_source(source_type)
|
|
18728
|
-
|
|
20406
|
+
item = _compact_dict(item)
|
|
20407
|
+
if include_raw:
|
|
20408
|
+
item["_raw"] = deepcopy(raw_item)
|
|
20409
|
+
return item
|
|
18729
20410
|
|
|
18730
20411
|
|
|
18731
20412
|
def _normalize_associated_graph_type(raw_item: dict[str, Any], *, chart_key: Any, view_key: Any) -> str:
|
|
@@ -18752,6 +20433,14 @@ def _associated_resource_index(resources: list[dict[str, Any]]) -> dict[int, dic
|
|
|
18752
20433
|
return indexed
|
|
18753
20434
|
|
|
18754
20435
|
|
|
20436
|
+
def _strip_internal_associated_resource_raw(resources: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
20437
|
+
return [
|
|
20438
|
+
_compact_dict({key: deepcopy(value) for key, value in item.items() if key != "_raw"})
|
|
20439
|
+
for item in resources
|
|
20440
|
+
if isinstance(item, dict)
|
|
20441
|
+
]
|
|
20442
|
+
|
|
20443
|
+
|
|
18755
20444
|
def _public_associated_graph_type(value: Any) -> str:
|
|
18756
20445
|
normalized = str(value or "").strip().lower()
|
|
18757
20446
|
if normalized in {"view", "viewgraph"}:
|
|
@@ -18887,6 +20576,188 @@ def _associated_resource_matches_patch(item: dict[str, Any], patch: AssociatedRe
|
|
|
18887
20576
|
return _associated_resource_public_key(item) == _associated_resource_public_key(patch)
|
|
18888
20577
|
|
|
18889
20578
|
|
|
20579
|
+
_ASSOCIATED_RESOURCE_PARTIAL_PATCH_KEY_ALIASES = {
|
|
20580
|
+
"graph_type": "graph_type",
|
|
20581
|
+
"graphType": "graph_type",
|
|
20582
|
+
"resource_type": "graph_type",
|
|
20583
|
+
"resourceType": "graph_type",
|
|
20584
|
+
"target_app_key": "target_app_key",
|
|
20585
|
+
"targetAppKey": "target_app_key",
|
|
20586
|
+
"app_key": "target_app_key",
|
|
20587
|
+
"appKey": "target_app_key",
|
|
20588
|
+
"view_key": "view_key",
|
|
20589
|
+
"viewKey": "view_key",
|
|
20590
|
+
"viewgraphKey": "view_key",
|
|
20591
|
+
"viewGraphKey": "view_key",
|
|
20592
|
+
"chart_key": "chart_key",
|
|
20593
|
+
"chartKey": "chart_key",
|
|
20594
|
+
"report_source": "report_source",
|
|
20595
|
+
"reportSource": "report_source",
|
|
20596
|
+
"match_rules": "match_rules",
|
|
20597
|
+
"matchRules": "match_rules",
|
|
20598
|
+
"match_mappings": "match_mappings",
|
|
20599
|
+
"matchMappings": "match_mappings",
|
|
20600
|
+
}
|
|
20601
|
+
|
|
20602
|
+
_ASSOCIATED_RESOURCE_PARTIAL_SET_KEYS = {
|
|
20603
|
+
"graph_type",
|
|
20604
|
+
"target_app_key",
|
|
20605
|
+
"view_key",
|
|
20606
|
+
"chart_key",
|
|
20607
|
+
"report_source",
|
|
20608
|
+
"match_rules",
|
|
20609
|
+
"match_mappings",
|
|
20610
|
+
}
|
|
20611
|
+
|
|
20612
|
+
_ASSOCIATED_RESOURCE_PARTIAL_UNSET_KEYS = {"match_rules", "match_mappings"}
|
|
20613
|
+
|
|
20614
|
+
|
|
20615
|
+
def _canonical_associated_resource_partial_patch_key(key: Any) -> str:
|
|
20616
|
+
raw = str(key or "").strip()
|
|
20617
|
+
return _ASSOCIATED_RESOURCE_PARTIAL_PATCH_KEY_ALIASES.get(raw, raw)
|
|
20618
|
+
|
|
20619
|
+
|
|
20620
|
+
def _normalize_associated_resource_partial_set(raw_set: Any, *, reason_path: str) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
20621
|
+
if not isinstance(raw_set, dict):
|
|
20622
|
+
return {}, [{"error_code": "INVALID_ASSOCIATED_RESOURCE_PATCH_SET", "reason_path": reason_path, "message": "patch_resources[].set must be an object"}]
|
|
20623
|
+
normalized: dict[str, Any] = {}
|
|
20624
|
+
issues: list[dict[str, Any]] = []
|
|
20625
|
+
for raw_key, value in raw_set.items():
|
|
20626
|
+
root = _canonical_associated_resource_partial_patch_key(raw_key)
|
|
20627
|
+
if root not in _ASSOCIATED_RESOURCE_PARTIAL_SET_KEYS:
|
|
20628
|
+
issues.append(
|
|
20629
|
+
{
|
|
20630
|
+
"error_code": "UNSUPPORTED_ASSOCIATED_RESOURCE_PATCH_FIELD",
|
|
20631
|
+
"reason_path": f"{reason_path}.{raw_key}",
|
|
20632
|
+
"field": raw_key,
|
|
20633
|
+
"allowed_fields": sorted(_ASSOCIATED_RESOURCE_PARTIAL_SET_KEYS),
|
|
20634
|
+
}
|
|
20635
|
+
)
|
|
20636
|
+
continue
|
|
20637
|
+
normalized[root] = value
|
|
20638
|
+
return normalized, issues
|
|
20639
|
+
|
|
20640
|
+
|
|
20641
|
+
def _normalize_associated_resource_partial_unset(raw_unset: Any, *, reason_path: str) -> tuple[set[str], list[dict[str, Any]]]:
|
|
20642
|
+
if not isinstance(raw_unset, list):
|
|
20643
|
+
return set(), [{"error_code": "INVALID_ASSOCIATED_RESOURCE_PATCH_UNSET", "reason_path": reason_path, "message": "patch_resources[].unset must be a list"}]
|
|
20644
|
+
normalized: set[str] = set()
|
|
20645
|
+
issues: list[dict[str, Any]] = []
|
|
20646
|
+
for index, raw_key in enumerate(raw_unset):
|
|
20647
|
+
root = _canonical_associated_resource_partial_patch_key(raw_key)
|
|
20648
|
+
if root not in _ASSOCIATED_RESOURCE_PARTIAL_UNSET_KEYS:
|
|
20649
|
+
issues.append(
|
|
20650
|
+
{
|
|
20651
|
+
"error_code": "UNSUPPORTED_ASSOCIATED_RESOURCE_PATCH_UNSET_FIELD",
|
|
20652
|
+
"reason_path": f"{reason_path}[{index}]",
|
|
20653
|
+
"field": raw_key,
|
|
20654
|
+
"allowed_fields": sorted(_ASSOCIATED_RESOURCE_PARTIAL_UNSET_KEYS),
|
|
20655
|
+
}
|
|
20656
|
+
)
|
|
20657
|
+
continue
|
|
20658
|
+
normalized.add(root)
|
|
20659
|
+
return normalized, issues
|
|
20660
|
+
|
|
20661
|
+
|
|
20662
|
+
def _associated_resource_upsert_payload_from_existing_item(
|
|
20663
|
+
item: dict[str, Any],
|
|
20664
|
+
*,
|
|
20665
|
+
associated_item_id: int,
|
|
20666
|
+
client_key: str | None = None,
|
|
20667
|
+
) -> dict[str, Any]:
|
|
20668
|
+
graph_type = str(item.get("graph_type") or item.get("resource_type") or "").strip().lower()
|
|
20669
|
+
payload = {
|
|
20670
|
+
"associated_item_id": associated_item_id,
|
|
20671
|
+
"client_key": client_key,
|
|
20672
|
+
"graph_type": "view" if graph_type == "view" else "chart",
|
|
20673
|
+
"target_app_key": item.get("target_app_key"),
|
|
20674
|
+
"view_key": item.get("view_key"),
|
|
20675
|
+
"chart_key": item.get("chart_key"),
|
|
20676
|
+
"report_source": item.get("report_source"),
|
|
20677
|
+
"match_rules": [],
|
|
20678
|
+
}
|
|
20679
|
+
raw = item.get("_raw") if isinstance(item.get("_raw"), dict) else {}
|
|
20680
|
+
raw_match_rules = raw.get("matchRules") if isinstance(raw, dict) else None
|
|
20681
|
+
if isinstance(raw_match_rules, list):
|
|
20682
|
+
if raw_match_rules and all(isinstance(group, list) for group in raw_match_rules):
|
|
20683
|
+
flattened = [rule for group in raw_match_rules for rule in group if isinstance(rule, dict)]
|
|
20684
|
+
payload["match_rules"] = flattened
|
|
20685
|
+
else:
|
|
20686
|
+
payload["match_rules"] = [rule for rule in raw_match_rules if isinstance(rule, dict)]
|
|
20687
|
+
return _compact_dict(payload)
|
|
20688
|
+
|
|
20689
|
+
|
|
20690
|
+
def _resolve_associated_resource_selector(
|
|
20691
|
+
selector: Any,
|
|
20692
|
+
*,
|
|
20693
|
+
existing_resources: list[dict[str, Any]],
|
|
20694
|
+
existing_by_id: dict[int, dict[str, Any]],
|
|
20695
|
+
reason_path: str,
|
|
20696
|
+
) -> tuple[int | None, dict[str, Any] | None]:
|
|
20697
|
+
item_id = _coerce_positive_int(selector)
|
|
20698
|
+
if item_id is not None:
|
|
20699
|
+
if item_id in existing_by_id:
|
|
20700
|
+
return item_id, None
|
|
20701
|
+
raw = str(selector or "").strip()
|
|
20702
|
+
if not raw:
|
|
20703
|
+
return None, {
|
|
20704
|
+
"error_code": "ASSOCIATED_RESOURCE_NOT_FOUND",
|
|
20705
|
+
"reason_path": reason_path,
|
|
20706
|
+
"received": selector,
|
|
20707
|
+
"available_associated_item_ids": sorted(existing_by_id),
|
|
20708
|
+
"message": "associated resource selector cannot be empty",
|
|
20709
|
+
}
|
|
20710
|
+
matches: list[dict[str, Any]] = []
|
|
20711
|
+
for resource in existing_resources:
|
|
20712
|
+
if not isinstance(resource, dict):
|
|
20713
|
+
continue
|
|
20714
|
+
candidates = [
|
|
20715
|
+
resource.get("chart_id"),
|
|
20716
|
+
resource.get("chart_key"),
|
|
20717
|
+
resource.get("view_key"),
|
|
20718
|
+
resource.get("associated_item_id"),
|
|
20719
|
+
]
|
|
20720
|
+
if raw in {str(candidate).strip() for candidate in candidates if candidate not in {None, ""}}:
|
|
20721
|
+
matches.append(resource)
|
|
20722
|
+
if len(matches) == 1:
|
|
20723
|
+
resolved_id = _coerce_positive_int(matches[0].get("associated_item_id"))
|
|
20724
|
+
if resolved_id is not None:
|
|
20725
|
+
return resolved_id, None
|
|
20726
|
+
if len(matches) > 1:
|
|
20727
|
+
return None, {
|
|
20728
|
+
"error_code": "AMBIGUOUS_ASSOCIATED_RESOURCE",
|
|
20729
|
+
"reason_path": reason_path,
|
|
20730
|
+
"received": selector,
|
|
20731
|
+
"candidate_associated_item_ids": [
|
|
20732
|
+
item_id
|
|
20733
|
+
for item_id in (_coerce_positive_int(item.get("associated_item_id")) for item in matches)
|
|
20734
|
+
if item_id is not None
|
|
20735
|
+
],
|
|
20736
|
+
"message": "selector matches multiple associated resources; pass associated_item_id",
|
|
20737
|
+
}
|
|
20738
|
+
return None, {
|
|
20739
|
+
"error_code": "ASSOCIATED_RESOURCE_NOT_FOUND",
|
|
20740
|
+
"reason_path": reason_path,
|
|
20741
|
+
"received": selector,
|
|
20742
|
+
"available_associated_item_ids": sorted(existing_by_id),
|
|
20743
|
+
"available_chart_ids": sorted(
|
|
20744
|
+
{
|
|
20745
|
+
str(resource.get("chart_id") or resource.get("chart_key") or "").strip()
|
|
20746
|
+
for resource in existing_resources
|
|
20747
|
+
if str(resource.get("chart_id") or resource.get("chart_key") or "").strip()
|
|
20748
|
+
}
|
|
20749
|
+
),
|
|
20750
|
+
"available_view_keys": sorted(
|
|
20751
|
+
{
|
|
20752
|
+
str(resource.get("view_key") or "").strip()
|
|
20753
|
+
for resource in existing_resources
|
|
20754
|
+
if str(resource.get("view_key") or "").strip()
|
|
20755
|
+
}
|
|
20756
|
+
),
|
|
20757
|
+
"message": "selector must be an associated_item_id, QingBI chart_id/chart_key, or Qingflow view_key from the app-level associated resource pool",
|
|
20758
|
+
}
|
|
20759
|
+
|
|
20760
|
+
|
|
18890
20761
|
def _associated_resource_not_found_issue(reason_path: str, associated_item_id: int, existing_by_id: dict[int, dict[str, Any]]) -> dict[str, Any]:
|
|
18891
20762
|
return {
|
|
18892
20763
|
"error_code": "ASSOCIATED_RESOURCE_NOT_FOUND",
|
|
@@ -18894,7 +20765,7 @@ def _associated_resource_not_found_issue(reason_path: str, associated_item_id: i
|
|
|
18894
20765
|
"associated_item_id": associated_item_id,
|
|
18895
20766
|
"available_associated_item_ids": sorted(existing_by_id),
|
|
18896
20767
|
"message": "associated_item_id is not in the app-level associated resource pool",
|
|
18897
|
-
"next_action": "call app_get and use associated_resources[].associated_item_id
|
|
20768
|
+
"next_action": "call app_get and use associated_resources[].associated_item_id, chart_id/chart_key, or view_key",
|
|
18898
20769
|
}
|
|
18899
20770
|
|
|
18900
20771
|
|
|
@@ -18907,14 +20778,33 @@ def _duplicate_associated_resource_issue(reason_path: str, associated_item_id: i
|
|
|
18907
20778
|
}
|
|
18908
20779
|
|
|
18909
20780
|
|
|
18910
|
-
def
|
|
20781
|
+
def _associated_resource_patch_has_match_config(patch: AssociatedResourceUpsertPatch) -> bool:
|
|
20782
|
+
fields_set = getattr(patch, "model_fields_set", set())
|
|
20783
|
+
return bool(patch.match_mappings) or bool(patch.match_rules) or "match_mappings" in fields_set
|
|
20784
|
+
|
|
20785
|
+
|
|
20786
|
+
def _serialize_associated_resource_match_rules(match_rules: list[Any]) -> list[list[dict[str, Any]]]:
|
|
18911
20787
|
if not match_rules:
|
|
18912
20788
|
return []
|
|
18913
|
-
|
|
20789
|
+
serialized: list[dict[str, Any]] = []
|
|
20790
|
+
for rule in match_rules:
|
|
20791
|
+
if isinstance(rule, CustomButtonMatchRulePatch):
|
|
20792
|
+
payload = rule.model_dump(mode="json", exclude_none=True)
|
|
20793
|
+
elif isinstance(rule, dict):
|
|
20794
|
+
payload = rule
|
|
20795
|
+
else:
|
|
20796
|
+
continue
|
|
20797
|
+
serialized.append(_serialize_custom_button_match_rule(payload))
|
|
20798
|
+
return [serialized] if serialized else []
|
|
18914
20799
|
|
|
18915
20800
|
|
|
18916
|
-
def _serialize_associated_resource_create_payload(
|
|
20801
|
+
def _serialize_associated_resource_create_payload(
|
|
20802
|
+
patch: AssociatedResourceUpsertPatch,
|
|
20803
|
+
*,
|
|
20804
|
+
match_rules_override: list[dict[str, Any]] | None = None,
|
|
20805
|
+
) -> dict[str, Any]:
|
|
18917
20806
|
graph_type = _public_associated_graph_type(patch.graph_type)
|
|
20807
|
+
match_rules = match_rules_override if match_rules_override is not None else patch.match_rules
|
|
18918
20808
|
return {
|
|
18919
20809
|
"appKey": patch.target_app_key,
|
|
18920
20810
|
"chartList": [
|
|
@@ -18924,22 +20814,32 @@ def _serialize_associated_resource_create_payload(patch: AssociatedResourceUpser
|
|
|
18924
20814
|
"graphType": _backend_associated_graph_type(graph_type),
|
|
18925
20815
|
}
|
|
18926
20816
|
],
|
|
18927
|
-
"matchRules": _serialize_associated_resource_match_rules(
|
|
20817
|
+
"matchRules": _serialize_associated_resource_match_rules(match_rules),
|
|
18928
20818
|
}
|
|
18929
20819
|
|
|
18930
20820
|
|
|
18931
|
-
def _serialize_associated_resource_update_payload(
|
|
20821
|
+
def _serialize_associated_resource_update_payload(
|
|
20822
|
+
patch: AssociatedResourceUpsertPatch,
|
|
20823
|
+
*,
|
|
20824
|
+
associated_item_id: int,
|
|
20825
|
+
existing_item: dict[str, Any] | None = None,
|
|
20826
|
+
match_rules_override: list[dict[str, Any]] | None = None,
|
|
20827
|
+
) -> dict[str, Any]:
|
|
18932
20828
|
graph_type = _public_associated_graph_type(patch.graph_type)
|
|
18933
|
-
|
|
20829
|
+
raw = existing_item.get("_raw") if isinstance(existing_item, dict) and isinstance(existing_item.get("_raw"), dict) else None
|
|
20830
|
+
payload = deepcopy(raw) if isinstance(raw, dict) else {}
|
|
20831
|
+
match_rules = match_rules_override if match_rules_override is not None else patch.match_rules
|
|
20832
|
+
payload.update(
|
|
18934
20833
|
{
|
|
18935
20834
|
"id": associated_item_id,
|
|
18936
20835
|
"appKey": patch.target_app_key,
|
|
18937
20836
|
"chartKey": patch.view_key if graph_type == "view" else patch.chart_key,
|
|
18938
20837
|
"sourceType": _backend_source_type_for_associated_resource_patch(patch),
|
|
18939
20838
|
"graphType": _backend_associated_graph_type(graph_type),
|
|
18940
|
-
"matchRules": _serialize_associated_resource_match_rules(
|
|
20839
|
+
"matchRules": _serialize_associated_resource_match_rules(match_rules),
|
|
18941
20840
|
}
|
|
18942
20841
|
)
|
|
20842
|
+
return _compact_dict(payload)
|
|
18943
20843
|
|
|
18944
20844
|
|
|
18945
20845
|
def _associated_resource_result_entry(
|
|
@@ -19014,6 +20914,204 @@ def _build_view_buttons_only_update_payload(config: Any, *, button_config_dtos:
|
|
|
19014
20914
|
return payload
|
|
19015
20915
|
|
|
19016
20916
|
|
|
20917
|
+
_VIEW_PARTIAL_PATCH_KEY_ALIASES = {
|
|
20918
|
+
"view_name": "name",
|
|
20919
|
+
"viewName": "name",
|
|
20920
|
+
"view_key": "view_key",
|
|
20921
|
+
"viewKey": "view_key",
|
|
20922
|
+
"viewgraphKey": "view_key",
|
|
20923
|
+
"type": "type",
|
|
20924
|
+
"view_type": "type",
|
|
20925
|
+
"viewType": "type",
|
|
20926
|
+
"columns": "columns",
|
|
20927
|
+
"column_names": "columns",
|
|
20928
|
+
"columnNames": "columns",
|
|
20929
|
+
"fields": "columns",
|
|
20930
|
+
"group_by": "group_by",
|
|
20931
|
+
"groupBy": "group_by",
|
|
20932
|
+
"filters": "filters",
|
|
20933
|
+
"filter_rules": "filters",
|
|
20934
|
+
"filterRules": "filters",
|
|
20935
|
+
"start_field": "start_field",
|
|
20936
|
+
"startField": "start_field",
|
|
20937
|
+
"end_field": "end_field",
|
|
20938
|
+
"endField": "end_field",
|
|
20939
|
+
"title_field": "title_field",
|
|
20940
|
+
"titleField": "title_field",
|
|
20941
|
+
"buttons": "buttons",
|
|
20942
|
+
"visibility": "visibility",
|
|
20943
|
+
"query_conditions": "query_conditions",
|
|
20944
|
+
"queryConditions": "query_conditions",
|
|
20945
|
+
"query_condition": "query_conditions",
|
|
20946
|
+
"queryCondition": "query_conditions",
|
|
20947
|
+
"associated_resources": "associated_resources",
|
|
20948
|
+
"associatedResources": "associated_resources",
|
|
20949
|
+
"associated_reports": "associated_resources",
|
|
20950
|
+
"associatedReports": "associated_resources",
|
|
20951
|
+
}
|
|
20952
|
+
|
|
20953
|
+
|
|
20954
|
+
def _canonical_view_partial_patch_key(key: Any) -> str:
|
|
20955
|
+
raw = str(key or "").strip()
|
|
20956
|
+
return _VIEW_PARTIAL_PATCH_KEY_ALIASES.get(raw, raw)
|
|
20957
|
+
|
|
20958
|
+
|
|
20959
|
+
def _normalize_view_partial_set(raw_set: Any, *, reason_path: str) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
20960
|
+
if not isinstance(raw_set, dict):
|
|
20961
|
+
return {}, [{"error_code": "INVALID_VIEW_PATCH_SET", "reason_path": reason_path, "message": "patch_views[].set must be an object"}]
|
|
20962
|
+
normalized: dict[str, Any] = {}
|
|
20963
|
+
issues: list[dict[str, Any]] = []
|
|
20964
|
+
for raw_key, value in raw_set.items():
|
|
20965
|
+
key_path = [part for part in str(raw_key or "").strip().split(".") if part]
|
|
20966
|
+
if not key_path:
|
|
20967
|
+
continue
|
|
20968
|
+
root = _canonical_view_partial_patch_key(key_path[0])
|
|
20969
|
+
if root not in _VIEW_PARTIAL_SET_KEYS:
|
|
20970
|
+
issues.append(
|
|
20971
|
+
{
|
|
20972
|
+
"error_code": "UNSUPPORTED_VIEW_PATCH_FIELD",
|
|
20973
|
+
"reason_path": f"{reason_path}.{raw_key}",
|
|
20974
|
+
"field": raw_key,
|
|
20975
|
+
"allowed_fields": sorted(_VIEW_PARTIAL_SET_KEYS),
|
|
20976
|
+
}
|
|
20977
|
+
)
|
|
20978
|
+
continue
|
|
20979
|
+
if len(key_path) == 1:
|
|
20980
|
+
normalized[root] = value
|
|
20981
|
+
continue
|
|
20982
|
+
target = normalized.setdefault(root, {})
|
|
20983
|
+
if not isinstance(target, dict):
|
|
20984
|
+
issues.append(
|
|
20985
|
+
{
|
|
20986
|
+
"error_code": "INVALID_VIEW_PATCH_PATH",
|
|
20987
|
+
"reason_path": f"{reason_path}.{raw_key}",
|
|
20988
|
+
"message": "cannot combine a scalar replacement and nested patch for the same field",
|
|
20989
|
+
}
|
|
20990
|
+
)
|
|
20991
|
+
continue
|
|
20992
|
+
cursor = target
|
|
20993
|
+
for part in key_path[1:-1]:
|
|
20994
|
+
next_value = cursor.setdefault(part, {})
|
|
20995
|
+
if not isinstance(next_value, dict):
|
|
20996
|
+
issues.append({"error_code": "INVALID_VIEW_PATCH_PATH", "reason_path": f"{reason_path}.{raw_key}"})
|
|
20997
|
+
break
|
|
20998
|
+
cursor = next_value
|
|
20999
|
+
else:
|
|
21000
|
+
cursor[key_path[-1]] = value
|
|
21001
|
+
return normalized, issues
|
|
21002
|
+
|
|
21003
|
+
|
|
21004
|
+
def _normalize_view_partial_unset(raw_unset: Any, *, reason_path: str) -> tuple[set[str], list[dict[str, Any]]]:
|
|
21005
|
+
if not isinstance(raw_unset, list):
|
|
21006
|
+
return set(), [{"error_code": "INVALID_VIEW_PATCH_UNSET", "reason_path": reason_path, "message": "patch_views[].unset must be a list"}]
|
|
21007
|
+
normalized: set[str] = set()
|
|
21008
|
+
issues: list[dict[str, Any]] = []
|
|
21009
|
+
for index, raw_key in enumerate(raw_unset):
|
|
21010
|
+
root = _canonical_view_partial_patch_key(str(raw_key or "").strip().split(".")[0])
|
|
21011
|
+
if root not in _VIEW_PARTIAL_UNSET_KEYS:
|
|
21012
|
+
issues.append(
|
|
21013
|
+
{
|
|
21014
|
+
"error_code": "UNSUPPORTED_VIEW_PATCH_UNSET_FIELD",
|
|
21015
|
+
"reason_path": f"{reason_path}[{index}]",
|
|
21016
|
+
"field": raw_key,
|
|
21017
|
+
"allowed_fields": sorted(_VIEW_PARTIAL_UNSET_KEYS),
|
|
21018
|
+
}
|
|
21019
|
+
)
|
|
21020
|
+
continue
|
|
21021
|
+
normalized.add(root)
|
|
21022
|
+
return normalized, issues
|
|
21023
|
+
|
|
21024
|
+
|
|
21025
|
+
_VIEW_PARTIAL_SET_KEYS = {
|
|
21026
|
+
"name",
|
|
21027
|
+
"type",
|
|
21028
|
+
"columns",
|
|
21029
|
+
"group_by",
|
|
21030
|
+
"filters",
|
|
21031
|
+
"start_field",
|
|
21032
|
+
"end_field",
|
|
21033
|
+
"title_field",
|
|
21034
|
+
"buttons",
|
|
21035
|
+
"visibility",
|
|
21036
|
+
"query_conditions",
|
|
21037
|
+
"associated_resources",
|
|
21038
|
+
}
|
|
21039
|
+
|
|
21040
|
+
_VIEW_PARTIAL_UNSET_KEYS = {"filters", "buttons", "visibility", "query_conditions", "associated_resources"}
|
|
21041
|
+
|
|
21042
|
+
|
|
21043
|
+
def _deep_merge_public_config(target: dict[str, Any], patch: dict[str, Any]) -> None:
|
|
21044
|
+
for key, value in patch.items():
|
|
21045
|
+
if isinstance(value, dict) and isinstance(target.get(key), dict):
|
|
21046
|
+
_deep_merge_public_config(cast(dict[str, Any], target[key]), value)
|
|
21047
|
+
else:
|
|
21048
|
+
target[key] = deepcopy(value)
|
|
21049
|
+
|
|
21050
|
+
|
|
21051
|
+
def _field_name_by_id(field_names_by_id: dict[int, str], field_id: Any) -> str | None:
|
|
21052
|
+
normalized_id = _coerce_nonnegative_int(field_id)
|
|
21053
|
+
if normalized_id is None:
|
|
21054
|
+
return None
|
|
21055
|
+
name = str(field_names_by_id.get(normalized_id) or "").strip()
|
|
21056
|
+
return name or None
|
|
21057
|
+
|
|
21058
|
+
|
|
21059
|
+
def _view_upsert_payload_from_existing_view(
|
|
21060
|
+
*,
|
|
21061
|
+
config: dict[str, Any],
|
|
21062
|
+
summary: dict[str, Any],
|
|
21063
|
+
view_key: str,
|
|
21064
|
+
field_names_by_id: dict[int, str],
|
|
21065
|
+
) -> dict[str, Any]:
|
|
21066
|
+
view_type = _normalize_view_type_name(config.get("viewgraphType") or summary.get("type"))
|
|
21067
|
+
configured_columns = [
|
|
21068
|
+
name
|
|
21069
|
+
for name in (_field_name_by_id(field_names_by_id, item) for item in (config.get("viewgraphQueIds") or []))
|
|
21070
|
+
if name and name not in _KNOWN_SYSTEM_VIEW_COLUMNS
|
|
21071
|
+
]
|
|
21072
|
+
columns = [
|
|
21073
|
+
str(name or "").strip()
|
|
21074
|
+
for name in (summary.get("apply_columns") or summary.get("columns") or [])
|
|
21075
|
+
if str(name or "").strip() and str(name or "").strip() not in _KNOWN_SYSTEM_VIEW_COLUMNS
|
|
21076
|
+
]
|
|
21077
|
+
if configured_columns and (not columns or len(configured_columns) >= len(columns)):
|
|
21078
|
+
columns = configured_columns
|
|
21079
|
+
display_config = summary.get("display_config") if isinstance(summary.get("display_config"), dict) else {}
|
|
21080
|
+
group_by = str(summary.get("group_by") or display_config.get("group_by") or "").strip() or _field_name_by_id(field_names_by_id, config.get("groupQueId"))
|
|
21081
|
+
title_field = str(display_config.get("title_field") or "").strip() or _field_name_by_id(field_names_by_id, config.get("titleQue"))
|
|
21082
|
+
gantt_config = config.get("viewgraphGanttConfigVO") if isinstance(config.get("viewgraphGanttConfigVO"), dict) else {}
|
|
21083
|
+
start_field = _field_name_by_id(field_names_by_id, gantt_config.get("startTimeQueId")) if isinstance(gantt_config, dict) else None
|
|
21084
|
+
end_field = _field_name_by_id(field_names_by_id, gantt_config.get("endTimeQueId")) if isinstance(gantt_config, dict) else None
|
|
21085
|
+
gantt_title = _field_name_by_id(field_names_by_id, gantt_config.get("titleQueId")) if isinstance(gantt_config, dict) else None
|
|
21086
|
+
query_conditions = _extract_view_query_conditions_config(config)
|
|
21087
|
+
query_conditions.pop("row_field_ids", None)
|
|
21088
|
+
associated_resources = _extract_view_associated_resources_config(config)
|
|
21089
|
+
payload: dict[str, Any] = {
|
|
21090
|
+
"name": str(summary.get("name") or config.get("viewgraphName") or config.get("viewName") or view_key).strip(),
|
|
21091
|
+
"view_key": view_key,
|
|
21092
|
+
"type": view_type or "table",
|
|
21093
|
+
"columns": columns,
|
|
21094
|
+
"group_by": group_by,
|
|
21095
|
+
"filters": [],
|
|
21096
|
+
"query_conditions": query_conditions,
|
|
21097
|
+
"associated_resources": {
|
|
21098
|
+
"visible": bool(associated_resources.get("visible", False)),
|
|
21099
|
+
"limit_type": associated_resources.get("limit_type"),
|
|
21100
|
+
"associated_item_ids": list(associated_resources.get("configured_associated_item_ids") or []),
|
|
21101
|
+
},
|
|
21102
|
+
}
|
|
21103
|
+
if title_field:
|
|
21104
|
+
payload["title_field"] = title_field
|
|
21105
|
+
if view_type == "gantt":
|
|
21106
|
+
if start_field:
|
|
21107
|
+
payload["start_field"] = start_field
|
|
21108
|
+
if end_field:
|
|
21109
|
+
payload["end_field"] = end_field
|
|
21110
|
+
if gantt_title:
|
|
21111
|
+
payload["title_field"] = gantt_title
|
|
21112
|
+
return payload
|
|
21113
|
+
|
|
21114
|
+
|
|
19017
21115
|
def _first_present(mapping: dict[str, Any], *keys: str) -> Any:
|
|
19018
21116
|
for key in keys:
|
|
19019
21117
|
if key in mapping and mapping[key] is not None:
|
|
@@ -19028,7 +21126,7 @@ def _build_view_create_payload(
|
|
|
19028
21126
|
schema: dict[str, Any],
|
|
19029
21127
|
patch: ViewUpsertPatch,
|
|
19030
21128
|
ordinal: int,
|
|
19031
|
-
view_filters: list[list[dict[str, Any]]],
|
|
21129
|
+
view_filters: list[list[dict[str, Any]]] | None,
|
|
19032
21130
|
current_fields_by_name: dict[str, dict[str, Any]],
|
|
19033
21131
|
auth_override: dict[str, Any] | None = None,
|
|
19034
21132
|
explicit_button_dtos: list[dict[str, Any]] | None = None,
|
|
@@ -19226,7 +21324,7 @@ def _build_view_update_payload(
|
|
|
19226
21324
|
source_viewgraph_key: str,
|
|
19227
21325
|
schema: dict[str, Any],
|
|
19228
21326
|
patch: ViewUpsertPatch,
|
|
19229
|
-
view_filters: list[list[dict[str, Any]]],
|
|
21327
|
+
view_filters: list[list[dict[str, Any]]] | None,
|
|
19230
21328
|
current_fields_by_name: dict[str, dict[str, Any]],
|
|
19231
21329
|
auth_override: dict[str, Any] | None = None,
|
|
19232
21330
|
explicit_button_dtos: list[dict[str, Any]] | None = None,
|
|
@@ -19334,7 +21432,7 @@ def _build_minimal_view_payload(
|
|
|
19334
21432
|
schema: dict[str, Any],
|
|
19335
21433
|
patch: ViewUpsertPatch,
|
|
19336
21434
|
ordinal: int,
|
|
19337
|
-
view_filters: list[list[dict[str, Any]]],
|
|
21435
|
+
view_filters: list[list[dict[str, Any]]] | None,
|
|
19338
21436
|
current_fields_by_name: dict[str, dict[str, Any]],
|
|
19339
21437
|
auth_override: dict[str, Any] | None = None,
|
|
19340
21438
|
explicit_button_dtos: list[dict[str, Any]] | None = None,
|
|
@@ -19403,9 +21501,14 @@ def _hydrate_view_backend_payload(
|
|
|
19403
21501
|
data.setdefault("beingImageAdaption", False)
|
|
19404
21502
|
data.setdefault("clippingMode", "default")
|
|
19405
21503
|
data.setdefault("frontCoverQueId", None)
|
|
19406
|
-
|
|
19407
|
-
|
|
19408
|
-
|
|
21504
|
+
if view_filters is None:
|
|
21505
|
+
data.setdefault("viewgraphLimitType", 1)
|
|
21506
|
+
data.setdefault("viewgraphLimit", [])
|
|
21507
|
+
data.setdefault("viewgraphLimitFormula", "")
|
|
21508
|
+
else:
|
|
21509
|
+
data["viewgraphLimitType"] = 1
|
|
21510
|
+
data["viewgraphLimit"] = deepcopy(view_filters)
|
|
21511
|
+
data["viewgraphLimitFormula"] = ""
|
|
19409
21512
|
data.setdefault("sortType", "defaultSort")
|
|
19410
21513
|
if not data.get("viewgraphSorts"):
|
|
19411
21514
|
sort_que_id = visible_que_id_values[0] if visible_que_id_values else 1
|
|
@@ -19417,8 +21520,11 @@ def _hydrate_view_backend_payload(
|
|
|
19417
21520
|
data.setdefault("printTpls", [])
|
|
19418
21521
|
data.setdefault("beingCommentStatus", False)
|
|
19419
21522
|
data.setdefault("usages", [])
|
|
19420
|
-
data
|
|
19421
|
-
|
|
21523
|
+
data.setdefault("dataPermissionType", "CUSTOM")
|
|
21524
|
+
if view_filters is None:
|
|
21525
|
+
data.setdefault("dataScope", "ALL")
|
|
21526
|
+
else:
|
|
21527
|
+
data["dataScope"] = "CUSTOM" if view_filters else "ALL"
|
|
19422
21528
|
data.setdefault("needPass", False)
|
|
19423
21529
|
data.setdefault("beingWorkflowNodeFutureListVisible", True)
|
|
19424
21530
|
data.setdefault("asosChartConfig", {"limitType": 1, "asosChartIdList": []})
|
|
@@ -19713,6 +21819,44 @@ def _normalize_view_filter_groups_for_compare(groups: Any) -> list[list[dict[str
|
|
|
19713
21819
|
return normalized_groups
|
|
19714
21820
|
|
|
19715
21821
|
|
|
21822
|
+
def _view_filter_rule_values_for_signature(rule: dict[str, Any]) -> list[str]:
|
|
21823
|
+
values = [str(value) for value in (rule.get("judgeValues") or [])]
|
|
21824
|
+
if values:
|
|
21825
|
+
return values
|
|
21826
|
+
fallback_values: list[str] = []
|
|
21827
|
+
for detail in rule.get("judgeValueDetails") or []:
|
|
21828
|
+
if not isinstance(detail, dict):
|
|
21829
|
+
continue
|
|
21830
|
+
item_id = detail.get("id")
|
|
21831
|
+
item_value = detail.get("value")
|
|
21832
|
+
if item_id is not None:
|
|
21833
|
+
fallback_values.append(str(item_id))
|
|
21834
|
+
elif item_value is not None:
|
|
21835
|
+
fallback_values.append(str(item_value))
|
|
21836
|
+
return fallback_values
|
|
21837
|
+
|
|
21838
|
+
|
|
21839
|
+
def _view_filter_groups_signature(groups: Any) -> list[list[dict[str, Any]]]:
|
|
21840
|
+
signature: list[list[dict[str, Any]]] = []
|
|
21841
|
+
for group in _normalize_view_filter_groups_for_compare(groups):
|
|
21842
|
+
group_signature: list[dict[str, Any]] = []
|
|
21843
|
+
for rule in group:
|
|
21844
|
+
group_signature.append(
|
|
21845
|
+
{
|
|
21846
|
+
"queId": rule.get("queId"),
|
|
21847
|
+
"judgeType": rule.get("judgeType"),
|
|
21848
|
+
"judgeValues": _view_filter_rule_values_for_signature(rule),
|
|
21849
|
+
}
|
|
21850
|
+
)
|
|
21851
|
+
if group_signature:
|
|
21852
|
+
signature.append(group_signature)
|
|
21853
|
+
return signature
|
|
21854
|
+
|
|
21855
|
+
|
|
21856
|
+
def _view_filter_groups_equivalent(expected: Any, actual: Any) -> bool:
|
|
21857
|
+
return _view_filter_groups_signature(expected) == _view_filter_groups_signature(actual)
|
|
21858
|
+
|
|
21859
|
+
|
|
19716
21860
|
def _infer_status_field_id(fields: list[dict[str, Any]]) -> str | None:
|
|
19717
21861
|
preferred_names = {"status", "状态", "订单状态", "审批状态", "流程状态"}
|
|
19718
21862
|
for field in fields:
|