@qingflow-tech/qingflow-app-user-mcp 1.0.7 → 1.0.8
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-user/references/public-surface-sync.md +3 -0
- package/src/qingflow_mcp/builder_facade/models.py +171 -9
- package/src/qingflow_mcp/builder_facade/service.py +2030 -83
- 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 +12 -2
- package/src/qingflow_mcp/server_app_user.py +1 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +189 -7
|
@@ -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,44 @@ 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()
|
|
3273
3675
|
|
|
3274
|
-
|
|
3676
|
+
upsert_resources = list(request.upsert_resources)
|
|
3677
|
+
if request.patch_resources:
|
|
3678
|
+
expanded_resources, patch_issues, patch_results = self._expand_associated_resource_partial_patches(
|
|
3679
|
+
existing_by_id=existing_by_id,
|
|
3680
|
+
patch_resources=request.patch_resources,
|
|
3681
|
+
)
|
|
3682
|
+
if patch_issues:
|
|
3683
|
+
return finalize(
|
|
3684
|
+
_failed(
|
|
3685
|
+
"ASSOCIATED_RESOURCE_PATCH_HYDRATION_FAILED",
|
|
3686
|
+
"one or more associated resource partial patches could not be hydrated; no write was executed",
|
|
3687
|
+
normalized_args=normalized_args,
|
|
3688
|
+
details={"patch_results": patch_results, "blocking_issues": patch_issues},
|
|
3689
|
+
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
3690
|
+
)
|
|
3691
|
+
)
|
|
3692
|
+
upsert_resources.extend(expanded_resources)
|
|
3693
|
+
force_update_resource_ids.update(
|
|
3694
|
+
item_id
|
|
3695
|
+
for item_id in (_coerce_positive_int(patch.associated_item_id) for patch in expanded_resources)
|
|
3696
|
+
if item_id is not None
|
|
3697
|
+
)
|
|
3698
|
+
normalized_args["upsert_resources"] = [patch.model_dump(mode="json") for patch in upsert_resources]
|
|
3699
|
+
normalized_args["patch_results"] = patch_results
|
|
3700
|
+
|
|
3701
|
+
compiled_resource_match_rules, resource_match_issues = self._compile_associated_resource_semantic_match_mappings(
|
|
3702
|
+
profile=profile,
|
|
3703
|
+
app_key=app_key,
|
|
3704
|
+
patches=upsert_resources,
|
|
3705
|
+
)
|
|
3706
|
+
normalized_args["compiled_match_rules"] = {
|
|
3707
|
+
str(index): _summarize_compiled_match_rules(rules)
|
|
3708
|
+
for index, rules in compiled_resource_match_rules.items()
|
|
3709
|
+
}
|
|
3710
|
+
|
|
3711
|
+
for index, patch in enumerate(upsert_resources):
|
|
3275
3712
|
client_key = str(patch.client_key or "").strip()
|
|
3276
3713
|
if client_key:
|
|
3277
3714
|
if client_key in used_client_keys:
|
|
@@ -3282,6 +3719,8 @@ class AiBuilderFacade:
|
|
|
3282
3719
|
if validation_issue is not None:
|
|
3283
3720
|
blocking_issues.append(validation_issue)
|
|
3284
3721
|
continue
|
|
3722
|
+
if index in {issue.get("_patch_index") for issue in resource_match_issues if isinstance(issue, dict)}:
|
|
3723
|
+
continue
|
|
3285
3724
|
associated_item_id = _coerce_positive_int(patch.associated_item_id)
|
|
3286
3725
|
if associated_item_id is not None:
|
|
3287
3726
|
if associated_item_id not in existing_by_id:
|
|
@@ -3293,7 +3732,14 @@ class AiBuilderFacade:
|
|
|
3293
3732
|
touched_ids.add(associated_item_id)
|
|
3294
3733
|
if client_key:
|
|
3295
3734
|
client_key_to_id[client_key] = associated_item_id
|
|
3296
|
-
operation =
|
|
3735
|
+
operation = (
|
|
3736
|
+
"update"
|
|
3737
|
+
if associated_item_id in force_update_resource_ids
|
|
3738
|
+
or _associated_resource_patch_has_match_config(patch)
|
|
3739
|
+
else "unchanged"
|
|
3740
|
+
if _associated_resource_matches_patch(existing_by_id[associated_item_id], patch)
|
|
3741
|
+
else "update"
|
|
3742
|
+
)
|
|
3297
3743
|
upsert_ops.append({"operation": operation, "associated_item_id": associated_item_id, "patch": patch, "index": index})
|
|
3298
3744
|
continue
|
|
3299
3745
|
matches = [
|
|
@@ -3327,7 +3773,8 @@ class AiBuilderFacade:
|
|
|
3327
3773
|
touched_ids.add(matched_id)
|
|
3328
3774
|
if client_key:
|
|
3329
3775
|
client_key_to_id[client_key] = matched_id
|
|
3330
|
-
|
|
3776
|
+
operation = "update" if _associated_resource_patch_has_match_config(patch) else "unchanged"
|
|
3777
|
+
upsert_ops.append({"operation": operation, "associated_item_id": matched_id, "patch": patch, "index": index})
|
|
3331
3778
|
else:
|
|
3332
3779
|
upsert_ops.append({"operation": "create", "associated_item_id": None, "patch": patch, "index": index})
|
|
3333
3780
|
|
|
@@ -3398,6 +3845,13 @@ class AiBuilderFacade:
|
|
|
3398
3845
|
}
|
|
3399
3846
|
)
|
|
3400
3847
|
|
|
3848
|
+
blocking_issues.extend(
|
|
3849
|
+
[
|
|
3850
|
+
{key: value for key, value in issue.items() if key != "_patch_index"}
|
|
3851
|
+
for issue in resource_match_issues
|
|
3852
|
+
]
|
|
3853
|
+
)
|
|
3854
|
+
|
|
3401
3855
|
if blocking_issues:
|
|
3402
3856
|
return finalize(
|
|
3403
3857
|
_failed(
|
|
@@ -3453,7 +3907,7 @@ class AiBuilderFacade:
|
|
|
3453
3907
|
"safe_to_retry": True,
|
|
3454
3908
|
"publish_requested": False,
|
|
3455
3909
|
"published": False,
|
|
3456
|
-
"associated_resources": existing_resources,
|
|
3910
|
+
"associated_resources": _strip_internal_associated_resource_raw(existing_resources),
|
|
3457
3911
|
}
|
|
3458
3912
|
return finalize(response)
|
|
3459
3913
|
|
|
@@ -3485,7 +3939,12 @@ class AiBuilderFacade:
|
|
|
3485
3939
|
client_key_to_id[str(patch.client_key)] = item_id
|
|
3486
3940
|
elif op["operation"] == "create":
|
|
3487
3941
|
write_executed = True
|
|
3488
|
-
self._associated_resource_create(
|
|
3942
|
+
self._associated_resource_create(
|
|
3943
|
+
profile=profile,
|
|
3944
|
+
app_key=app_key,
|
|
3945
|
+
patch=patch,
|
|
3946
|
+
match_rules_override=compiled_resource_match_rules.get(int(op["index"])),
|
|
3947
|
+
)
|
|
3489
3948
|
readback_resources = self._load_associated_resources_for_builder(profile=profile, app_key=app_key)
|
|
3490
3949
|
matches = [
|
|
3491
3950
|
item
|
|
@@ -3500,7 +3959,14 @@ class AiBuilderFacade:
|
|
|
3500
3959
|
else:
|
|
3501
3960
|
item_id = int(op["associated_item_id"])
|
|
3502
3961
|
write_executed = True
|
|
3503
|
-
self._associated_resource_update(
|
|
3962
|
+
self._associated_resource_update(
|
|
3963
|
+
profile=profile,
|
|
3964
|
+
app_key=app_key,
|
|
3965
|
+
associated_item_id=item_id,
|
|
3966
|
+
patch=patch,
|
|
3967
|
+
existing_item=existing_by_id.get(item_id),
|
|
3968
|
+
match_rules_override=compiled_resource_match_rules.get(int(op["index"])),
|
|
3969
|
+
)
|
|
3504
3970
|
updated.append(_associated_resource_result_entry("update", op["index"], patch, associated_item_id=item_id))
|
|
3505
3971
|
if patch.client_key:
|
|
3506
3972
|
client_key_to_id[str(patch.client_key)] = item_id
|
|
@@ -3641,7 +4107,15 @@ class AiBuilderFacade:
|
|
|
3641
4107
|
"normalized_args": normalized_args,
|
|
3642
4108
|
"missing_fields": [],
|
|
3643
4109
|
"allowed_values": {"graph_type": ["chart", "view"], "report_source": ["app", "dataset"], "view_config.limit_type": ["all", "select"]},
|
|
3644
|
-
"details": {
|
|
4110
|
+
"details": {
|
|
4111
|
+
"edit_version_no": edit_version_no,
|
|
4112
|
+
"associated_item_ids_by_client_key": client_key_to_id,
|
|
4113
|
+
"readback_failed": readback_failed,
|
|
4114
|
+
"compiled_match_rules": {
|
|
4115
|
+
str(index): _summarize_compiled_match_rules(rules)
|
|
4116
|
+
for index, rules in compiled_resource_match_rules.items()
|
|
4117
|
+
},
|
|
4118
|
+
},
|
|
3645
4119
|
"request_id": None,
|
|
3646
4120
|
"suggested_next_call": None if verified else {"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
3647
4121
|
"noop": False,
|
|
@@ -4835,7 +5309,7 @@ class AiBuilderFacade:
|
|
|
4835
5309
|
fallback_items = self.charts.qingbi_report_list(profile=profile, app_key=app_key).get("items") or []
|
|
4836
5310
|
return list(fallback_items) if isinstance(fallback_items, list) else [], "fallback"
|
|
4837
5311
|
|
|
4838
|
-
def _load_associated_resources_for_builder(self, *, profile: str, app_key: str) -> list[dict[str, Any]]:
|
|
5312
|
+
def _load_associated_resources_for_builder(self, *, profile: str, app_key: str, include_raw: bool = False) -> list[dict[str, Any]]:
|
|
4839
5313
|
def runner(_: Any, context: BackendRequestContext) -> list[dict[str, Any]]:
|
|
4840
5314
|
payload = self.apps.backend.request(
|
|
4841
5315
|
"GET",
|
|
@@ -4843,12 +5317,19 @@ class AiBuilderFacade:
|
|
|
4843
5317
|
f"/app/{app_key}/asosChart",
|
|
4844
5318
|
params={"role": 1, "beingDraft": True},
|
|
4845
5319
|
)
|
|
4846
|
-
return _normalize_associated_resources_payload(payload)
|
|
5320
|
+
return _normalize_associated_resources_payload(payload, include_raw=include_raw)
|
|
4847
5321
|
|
|
4848
5322
|
return self.apps._run(profile, runner)
|
|
4849
5323
|
|
|
4850
|
-
def _associated_resource_create(
|
|
4851
|
-
|
|
5324
|
+
def _associated_resource_create(
|
|
5325
|
+
self,
|
|
5326
|
+
*,
|
|
5327
|
+
profile: str,
|
|
5328
|
+
app_key: str,
|
|
5329
|
+
patch: AssociatedResourceUpsertPatch,
|
|
5330
|
+
match_rules_override: list[dict[str, Any]] | None = None,
|
|
5331
|
+
) -> JSONObject:
|
|
5332
|
+
payload = _serialize_associated_resource_create_payload(patch, match_rules_override=match_rules_override)
|
|
4852
5333
|
|
|
4853
5334
|
def runner(_: Any, context: BackendRequestContext) -> JSONObject:
|
|
4854
5335
|
result = self.apps.backend.request("POST", context, f"/app/{app_key}/asosChart", json_body=payload)
|
|
@@ -4863,8 +5344,15 @@ class AiBuilderFacade:
|
|
|
4863
5344
|
app_key: str,
|
|
4864
5345
|
associated_item_id: int,
|
|
4865
5346
|
patch: AssociatedResourceUpsertPatch,
|
|
5347
|
+
existing_item: dict[str, Any] | None = None,
|
|
5348
|
+
match_rules_override: list[dict[str, Any]] | None = None,
|
|
4866
5349
|
) -> JSONObject:
|
|
4867
|
-
payload = _serialize_associated_resource_update_payload(
|
|
5350
|
+
payload = _serialize_associated_resource_update_payload(
|
|
5351
|
+
patch,
|
|
5352
|
+
associated_item_id=associated_item_id,
|
|
5353
|
+
existing_item=existing_item,
|
|
5354
|
+
match_rules_override=match_rules_override,
|
|
5355
|
+
)
|
|
4868
5356
|
|
|
4869
5357
|
def runner(_: Any, context: BackendRequestContext) -> JSONObject:
|
|
4870
5358
|
result = self.apps.backend.request("POST", context, f"/app/{app_key}/asosChart/{associated_item_id}", json_body=payload)
|
|
@@ -4984,6 +5472,7 @@ class AiBuilderFacade:
|
|
|
4984
5472
|
continue
|
|
4985
5473
|
compiled_config, config_issues = self._compile_custom_button_add_data_config(
|
|
4986
5474
|
profile=profile,
|
|
5475
|
+
source_app_key=app_key,
|
|
4987
5476
|
source_fields=list(source_fields),
|
|
4988
5477
|
target_fields=target_schema_cache[target_app_key],
|
|
4989
5478
|
target_app_key=target_app_key,
|
|
@@ -5001,6 +5490,7 @@ class AiBuilderFacade:
|
|
|
5001
5490
|
self,
|
|
5002
5491
|
*,
|
|
5003
5492
|
profile: str,
|
|
5493
|
+
source_app_key: str,
|
|
5004
5494
|
source_fields: list[dict[str, Any]],
|
|
5005
5495
|
target_fields: list[dict[str, Any]],
|
|
5006
5496
|
target_app_key: str,
|
|
@@ -5035,6 +5525,9 @@ class AiBuilderFacade:
|
|
|
5035
5525
|
source_field=source_field,
|
|
5036
5526
|
target_field=target_field,
|
|
5037
5527
|
reason_path=f"{reason_path}.field_mappings[{mapping_index}]",
|
|
5528
|
+
source_app_key=source_app_key,
|
|
5529
|
+
error_code="CUSTOM_BUTTON_MAPPING_TYPE_MISMATCH",
|
|
5530
|
+
context_label="addData copy mapping",
|
|
5038
5531
|
)
|
|
5039
5532
|
if type_issue:
|
|
5040
5533
|
issues.append(type_issue)
|
|
@@ -5349,19 +5842,19 @@ class AiBuilderFacade:
|
|
|
5349
5842
|
except (QingflowApiError, RuntimeError) as error:
|
|
5350
5843
|
api_error = _coerce_api_error(error)
|
|
5351
5844
|
has_requested_list_buttons = any(
|
|
5352
|
-
_normalize_view_button_config_type(item.get("configType")) == "
|
|
5845
|
+
_normalize_view_button_config_type(item.get("configType")) == "INSIDE"
|
|
5353
5846
|
for item in new_dtos
|
|
5354
5847
|
if isinstance(item, dict)
|
|
5355
5848
|
)
|
|
5356
5849
|
fallback_dtos = [
|
|
5357
5850
|
item
|
|
5358
5851
|
for item in merged_dtos
|
|
5359
|
-
if _normalize_view_button_config_type(item.get("configType")) != "
|
|
5852
|
+
if _normalize_view_button_config_type(item.get("configType")) != "INSIDE"
|
|
5360
5853
|
]
|
|
5361
5854
|
fallback_new_dtos = [
|
|
5362
5855
|
item
|
|
5363
5856
|
for item in new_dtos
|
|
5364
|
-
if _normalize_view_button_config_type(item.get("configType")) != "
|
|
5857
|
+
if _normalize_view_button_config_type(item.get("configType")) != "INSIDE"
|
|
5365
5858
|
]
|
|
5366
5859
|
if has_requested_list_buttons and fallback_new_dtos and fallback_dtos != merged_dtos:
|
|
5367
5860
|
fallback_payload = _build_view_buttons_only_update_payload(current_config, button_config_dtos=fallback_dtos)
|
|
@@ -5373,12 +5866,12 @@ class AiBuilderFacade:
|
|
|
5373
5866
|
"index": config_index,
|
|
5374
5867
|
"operation": "view_config",
|
|
5375
5868
|
"status": "failed",
|
|
5376
|
-
"error_code": "
|
|
5869
|
+
"error_code": "INSIDE_BUTTON_BACKEND_UNSUPPORTED",
|
|
5377
5870
|
"view_key": view_key,
|
|
5378
|
-
"message": "backend rejected list-button placement; header/detail placements were retried without
|
|
5871
|
+
"message": "backend rejected inside/list-button placement; header/detail placements were retried without inside buttons",
|
|
5379
5872
|
"backend_message": api_error.message,
|
|
5380
5873
|
"transport_error": _transport_error_payload(api_error),
|
|
5381
|
-
"next_action": "use placement=header/detail for now, or verify the backend
|
|
5874
|
+
"next_action": "use placement=header/detail for now, or verify the backend inside-button payload separately",
|
|
5382
5875
|
}
|
|
5383
5876
|
failed.append(unsupported_list_issue)
|
|
5384
5877
|
except (QingflowApiError, RuntimeError) as fallback_error:
|
|
@@ -5402,12 +5895,12 @@ class AiBuilderFacade:
|
|
|
5402
5895
|
"index": config_index,
|
|
5403
5896
|
"operation": "view_config",
|
|
5404
5897
|
"status": "failed",
|
|
5405
|
-
"error_code": "
|
|
5898
|
+
"error_code": "INSIDE_BUTTON_BACKEND_UNSUPPORTED",
|
|
5406
5899
|
"view_key": view_key,
|
|
5407
|
-
"message": "backend rejected list-button placement",
|
|
5900
|
+
"message": "backend rejected inside/list-button placement",
|
|
5408
5901
|
"backend_message": api_error.message,
|
|
5409
5902
|
"transport_error": _transport_error_payload(api_error),
|
|
5410
|
-
"next_action": "use placement=header/detail for now, or verify the backend
|
|
5903
|
+
"next_action": "use placement=header/detail for now, or verify the backend inside-button payload separately",
|
|
5411
5904
|
}
|
|
5412
5905
|
failed.append(issue)
|
|
5413
5906
|
results.append(issue)
|
|
@@ -5648,6 +6141,7 @@ class AiBuilderFacade:
|
|
|
5648
6141
|
config if isinstance(config, dict) else {},
|
|
5649
6142
|
available_resources=associated_resources,
|
|
5650
6143
|
)
|
|
6144
|
+
buttons_config = _extract_view_buttons_config(config if isinstance(config, dict) else {})
|
|
5651
6145
|
|
|
5652
6146
|
response = ViewGetResponse(
|
|
5653
6147
|
view_key=view_key,
|
|
@@ -5656,6 +6150,7 @@ class AiBuilderFacade:
|
|
|
5656
6150
|
config=deepcopy(config) if isinstance(config, dict) else {},
|
|
5657
6151
|
questions=questions,
|
|
5658
6152
|
associations=associations,
|
|
6153
|
+
buttons_config=buttons_config,
|
|
5659
6154
|
associated_resources_config=associated_resources_config,
|
|
5660
6155
|
)
|
|
5661
6156
|
question_entries = _extract_view_question_entries(config.get("viewgraphQuestions"))
|
|
@@ -5682,6 +6177,7 @@ class AiBuilderFacade:
|
|
|
5682
6177
|
"verification": verification,
|
|
5683
6178
|
"verified": all(bool(value) for value in verification.values()),
|
|
5684
6179
|
"query_conditions": query_conditions,
|
|
6180
|
+
"buttons_config": buttons_config,
|
|
5685
6181
|
"associated_resources_config": associated_resources_config,
|
|
5686
6182
|
**response.model_dump(mode="json"),
|
|
5687
6183
|
}
|
|
@@ -6165,6 +6661,173 @@ class AiBuilderFacade:
|
|
|
6165
6661
|
"verification": {"field_count": len(field_names)},
|
|
6166
6662
|
}
|
|
6167
6663
|
|
|
6664
|
+
def _expand_view_partial_patches(
|
|
6665
|
+
self,
|
|
6666
|
+
*,
|
|
6667
|
+
profile: str,
|
|
6668
|
+
app_key: str,
|
|
6669
|
+
schema: dict[str, Any],
|
|
6670
|
+
existing_by_key: dict[str, dict[str, Any]],
|
|
6671
|
+
existing_by_name: dict[str, list[dict[str, Any]]],
|
|
6672
|
+
patch_views: list[ViewPartialPatch],
|
|
6673
|
+
) -> tuple[list[ViewUpsertPatch], list[dict[str, Any]], list[dict[str, Any]]]:
|
|
6674
|
+
parsed_schema = _parse_schema(schema)
|
|
6675
|
+
field_names_by_id = {
|
|
6676
|
+
field_id: str(field.get("name") or "").strip()
|
|
6677
|
+
for field in parsed_schema.get("fields") or []
|
|
6678
|
+
if isinstance(field, dict)
|
|
6679
|
+
and (field_id := _coerce_nonnegative_int(field.get("que_id") or field.get("field_id"))) is not None
|
|
6680
|
+
and str(field.get("name") or "").strip()
|
|
6681
|
+
}
|
|
6682
|
+
expanded: list[ViewUpsertPatch] = []
|
|
6683
|
+
issues: list[dict[str, Any]] = []
|
|
6684
|
+
results: list[dict[str, Any]] = []
|
|
6685
|
+
for index, patch in enumerate(patch_views):
|
|
6686
|
+
view_key = str(patch.view_key or "").strip()
|
|
6687
|
+
name = str(patch.name or "").strip()
|
|
6688
|
+
matched_view: dict[str, Any] | None = None
|
|
6689
|
+
if view_key:
|
|
6690
|
+
matched_view = existing_by_key.get(view_key)
|
|
6691
|
+
if matched_view is None:
|
|
6692
|
+
issue = {
|
|
6693
|
+
"error_code": "UNKNOWN_VIEW",
|
|
6694
|
+
"reason_path": f"patch_views[{index}].view_key",
|
|
6695
|
+
"view_key": view_key,
|
|
6696
|
+
"message": "view_key does not exist on this app",
|
|
6697
|
+
}
|
|
6698
|
+
issues.append(issue)
|
|
6699
|
+
results.append({"index": index, "status": "failed", **issue})
|
|
6700
|
+
continue
|
|
6701
|
+
else:
|
|
6702
|
+
matches = existing_by_name.get(name, [])
|
|
6703
|
+
if len(matches) != 1:
|
|
6704
|
+
issue = {
|
|
6705
|
+
"error_code": "AMBIGUOUS_VIEW" if matches else "UNKNOWN_VIEW",
|
|
6706
|
+
"reason_path": f"patch_views[{index}].name",
|
|
6707
|
+
"view_name": name,
|
|
6708
|
+
"matches": [
|
|
6709
|
+
{"name": _extract_view_name(view), "view_key": _extract_view_key(view), "type": _normalize_view_type_name(view.get("viewgraphType") or view.get("type"))}
|
|
6710
|
+
for view in matches
|
|
6711
|
+
],
|
|
6712
|
+
"message": "patch_views[] must target a single existing view; use view_key when names are duplicated",
|
|
6713
|
+
}
|
|
6714
|
+
issues.append(issue)
|
|
6715
|
+
results.append({"index": index, "status": "failed", **issue})
|
|
6716
|
+
continue
|
|
6717
|
+
matched_view = matches[0]
|
|
6718
|
+
view_key = _extract_view_key(matched_view)
|
|
6719
|
+
try:
|
|
6720
|
+
config_response = self.views.view_get_config(profile=profile, viewgraph_key=view_key)
|
|
6721
|
+
config = config_response.get("result") if isinstance(config_response.get("result"), dict) else {}
|
|
6722
|
+
if not isinstance(config, dict):
|
|
6723
|
+
config = {}
|
|
6724
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
6725
|
+
api_error = _coerce_api_error(error)
|
|
6726
|
+
issue = {
|
|
6727
|
+
"error_code": "VIEW_PATCH_CONFIG_READ_FAILED",
|
|
6728
|
+
"reason_path": f"patch_views[{index}]",
|
|
6729
|
+
"view_key": view_key,
|
|
6730
|
+
"message": api_error.message,
|
|
6731
|
+
"transport_error": _transport_error_payload(api_error),
|
|
6732
|
+
}
|
|
6733
|
+
issues.append(issue)
|
|
6734
|
+
results.append({"index": index, "status": "failed", **issue})
|
|
6735
|
+
continue
|
|
6736
|
+
question_list: list[dict[str, Any]] = []
|
|
6737
|
+
try:
|
|
6738
|
+
question_response = self.views.view_list_questions(profile=profile, viewgraph_key=view_key)
|
|
6739
|
+
raw_questions = question_response.get("result")
|
|
6740
|
+
if isinstance(raw_questions, list):
|
|
6741
|
+
question_list = [deepcopy(item) for item in raw_questions if isinstance(item, dict)]
|
|
6742
|
+
except (QingflowApiError, RuntimeError):
|
|
6743
|
+
question_list = []
|
|
6744
|
+
base_summary = {
|
|
6745
|
+
"name": _extract_view_name(config) or _extract_view_name(matched_view or {}) or name or view_key,
|
|
6746
|
+
"view_key": view_key,
|
|
6747
|
+
"type": _normalize_view_type_name(config.get("viewgraphType") or (matched_view or {}).get("viewgraphType") or (matched_view or {}).get("type")),
|
|
6748
|
+
}
|
|
6749
|
+
summary = _merge_view_summary_with_config(base_summary, config=config, question_list=question_list)
|
|
6750
|
+
current_payload = _view_upsert_payload_from_existing_view(
|
|
6751
|
+
config=config,
|
|
6752
|
+
summary=summary,
|
|
6753
|
+
view_key=view_key,
|
|
6754
|
+
field_names_by_id=field_names_by_id,
|
|
6755
|
+
)
|
|
6756
|
+
normalized_set, set_issues = _normalize_view_partial_set(patch.set, reason_path=f"patch_views[{index}].set")
|
|
6757
|
+
normalized_unset, unset_issues = _normalize_view_partial_unset(patch.unset, reason_path=f"patch_views[{index}].unset")
|
|
6758
|
+
if set_issues or unset_issues:
|
|
6759
|
+
patch_issues = [*set_issues, *unset_issues]
|
|
6760
|
+
issues.extend(patch_issues)
|
|
6761
|
+
results.append({"index": index, "status": "failed", "view_key": view_key, "issues": patch_issues})
|
|
6762
|
+
continue
|
|
6763
|
+
touched_keys = set(normalized_set) | set(normalized_unset)
|
|
6764
|
+
patch_payload = deepcopy(current_payload)
|
|
6765
|
+
for key, value in normalized_set.items():
|
|
6766
|
+
if key in {"query_conditions", "associated_resources", "visibility"} and isinstance(value, dict):
|
|
6767
|
+
merged_value = deepcopy(patch_payload.get(key) if isinstance(patch_payload.get(key), dict) else {})
|
|
6768
|
+
_deep_merge_public_config(merged_value, value)
|
|
6769
|
+
patch_payload[key] = merged_value
|
|
6770
|
+
else:
|
|
6771
|
+
patch_payload[key] = value
|
|
6772
|
+
for key in normalized_unset:
|
|
6773
|
+
if key == "filters":
|
|
6774
|
+
patch_payload["filters"] = []
|
|
6775
|
+
elif key == "buttons":
|
|
6776
|
+
patch_payload["buttons"] = []
|
|
6777
|
+
elif key == "query_conditions":
|
|
6778
|
+
patch_payload["query_conditions"] = {"enabled": False, "exact": False, "hide_before_query": False, "rows": []}
|
|
6779
|
+
elif key == "associated_resources":
|
|
6780
|
+
patch_payload["associated_resources"] = {"visible": False}
|
|
6781
|
+
elif key == "visibility":
|
|
6782
|
+
patch_payload.pop("visibility", None)
|
|
6783
|
+
else:
|
|
6784
|
+
issue = {
|
|
6785
|
+
"error_code": "VIEW_PATCH_UNSET_NOT_SUPPORTED",
|
|
6786
|
+
"reason_path": f"patch_views[{index}].unset",
|
|
6787
|
+
"field": key,
|
|
6788
|
+
"message": f"cannot unset {key}; use set with an explicit replacement value",
|
|
6789
|
+
}
|
|
6790
|
+
issues.append(issue)
|
|
6791
|
+
patch_payload["_partial_update"] = True
|
|
6792
|
+
patch_payload["_preserve_filters"] = "filters" not in touched_keys
|
|
6793
|
+
patch_payload["_preserve_buttons"] = "buttons" not in touched_keys
|
|
6794
|
+
patch_payload["_preserve_query_conditions"] = "query_conditions" not in touched_keys
|
|
6795
|
+
patch_payload["_preserve_associated_resources"] = "associated_resources" not in touched_keys
|
|
6796
|
+
try:
|
|
6797
|
+
expanded_patch = ViewUpsertPatch.model_validate(patch_payload)
|
|
6798
|
+
except Exception as error:
|
|
6799
|
+
issue = {
|
|
6800
|
+
"error_code": "VIEW_PATCH_HYDRATION_FAILED",
|
|
6801
|
+
"reason_path": f"patch_views[{index}]",
|
|
6802
|
+
"view_key": view_key,
|
|
6803
|
+
"message": str(error),
|
|
6804
|
+
"hydrated_payload": _compact_dict({k: v for k, v in patch_payload.items() if not str(k).startswith("_")}),
|
|
6805
|
+
}
|
|
6806
|
+
issues.append(issue)
|
|
6807
|
+
results.append({"index": index, "status": "failed", **issue})
|
|
6808
|
+
continue
|
|
6809
|
+
expanded.append(expanded_patch)
|
|
6810
|
+
results.append(
|
|
6811
|
+
{
|
|
6812
|
+
"index": index,
|
|
6813
|
+
"status": "expanded",
|
|
6814
|
+
"view_key": view_key,
|
|
6815
|
+
"set_paths": sorted(normalized_set),
|
|
6816
|
+
"unset_paths": sorted(normalized_unset),
|
|
6817
|
+
"preserved_paths": sorted(
|
|
6818
|
+
key
|
|
6819
|
+
for key, preserve in {
|
|
6820
|
+
"filters": expanded_patch.preserve_filters,
|
|
6821
|
+
"buttons": expanded_patch.preserve_buttons,
|
|
6822
|
+
"query_conditions": expanded_patch.preserve_query_conditions,
|
|
6823
|
+
"associated_resources": expanded_patch.preserve_associated_resources,
|
|
6824
|
+
}.items()
|
|
6825
|
+
if preserve
|
|
6826
|
+
),
|
|
6827
|
+
}
|
|
6828
|
+
)
|
|
6829
|
+
return expanded, issues, results
|
|
6830
|
+
|
|
6168
6831
|
def app_read(self, *, profile: str, app_key: str, include_raw: bool = False) -> JSONObject:
|
|
6169
6832
|
state = self._load_app_state(profile=profile, app_key=app_key)
|
|
6170
6833
|
base_result = state["base"]
|
|
@@ -7461,16 +8124,19 @@ class AiBuilderFacade:
|
|
|
7461
8124
|
profile: str,
|
|
7462
8125
|
app_key: str,
|
|
7463
8126
|
upsert_views: list[ViewUpsertPatch],
|
|
8127
|
+
patch_views: list[ViewPartialPatch] | None = None,
|
|
7464
8128
|
remove_views: list[str],
|
|
7465
8129
|
publish: bool = True,
|
|
7466
8130
|
) -> JSONObject:
|
|
8131
|
+
patch_views = patch_views or []
|
|
7467
8132
|
normalized_args = {
|
|
7468
8133
|
"app_key": app_key,
|
|
7469
8134
|
"upsert_views": [patch.model_dump(mode="json") for patch in upsert_views],
|
|
8135
|
+
"patch_views": [patch.model_dump(mode="json") for patch in patch_views],
|
|
7470
8136
|
"remove_views": list(remove_views),
|
|
7471
8137
|
"publish": publish,
|
|
7472
8138
|
}
|
|
7473
|
-
if not upsert_views and not remove_views:
|
|
8139
|
+
if not upsert_views and not patch_views and not remove_views:
|
|
7474
8140
|
response = {
|
|
7475
8141
|
"status": "success",
|
|
7476
8142
|
"error_code": None,
|
|
@@ -7535,6 +8201,28 @@ class AiBuilderFacade:
|
|
|
7535
8201
|
existing_by_name.setdefault(name, []).append(view)
|
|
7536
8202
|
parsed_schema = _parse_schema(schema)
|
|
7537
8203
|
field_names = {field["name"] for field in parsed_schema["fields"]}
|
|
8204
|
+
if patch_views:
|
|
8205
|
+
expanded_views, patch_issues, patch_results = self._expand_view_partial_patches(
|
|
8206
|
+
profile=profile,
|
|
8207
|
+
app_key=app_key,
|
|
8208
|
+
schema=schema,
|
|
8209
|
+
existing_by_key=existing_by_key,
|
|
8210
|
+
existing_by_name=existing_by_name,
|
|
8211
|
+
patch_views=patch_views,
|
|
8212
|
+
)
|
|
8213
|
+
if patch_issues:
|
|
8214
|
+
return finalize(
|
|
8215
|
+
_failed(
|
|
8216
|
+
"VIEW_PATCH_HYDRATION_FAILED",
|
|
8217
|
+
"one or more view partial patches could not be hydrated; no write was executed",
|
|
8218
|
+
normalized_args=normalized_args,
|
|
8219
|
+
details={"patch_results": patch_results, "blocking_issues": patch_issues},
|
|
8220
|
+
suggested_next_call={"tool_name": "view_get", "arguments": {"profile": profile, "view_key": patch_issues[0].get("view_key") or "VIEW_KEY"}},
|
|
8221
|
+
)
|
|
8222
|
+
)
|
|
8223
|
+
upsert_views = [*upsert_views, *expanded_views]
|
|
8224
|
+
normalized_args["upsert_views"] = [patch.model_dump(mode="json") for patch in upsert_views]
|
|
8225
|
+
normalized_args["patch_results"] = patch_results
|
|
7538
8226
|
current_fields_by_name = {
|
|
7539
8227
|
str(field.get("name") or ""): field
|
|
7540
8228
|
for field in parsed_schema["fields"]
|
|
@@ -7701,7 +8389,13 @@ class AiBuilderFacade:
|
|
|
7701
8389
|
missing_fields=[gantt_field_name],
|
|
7702
8390
|
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
7703
8391
|
)
|
|
7704
|
-
translated_filters,
|
|
8392
|
+
translated_filters: list[list[dict[str, Any]]] | None
|
|
8393
|
+
filter_issues: list[dict[str, Any]]
|
|
8394
|
+
if patch.preserve_filters:
|
|
8395
|
+
translated_filters = None
|
|
8396
|
+
filter_issues = []
|
|
8397
|
+
else:
|
|
8398
|
+
translated_filters, filter_issues = _build_view_filter_groups(current_fields_by_name=current_fields_by_name, filters=patch.filters)
|
|
7705
8399
|
if filter_issues:
|
|
7706
8400
|
first_issue = filter_issues[0]
|
|
7707
8401
|
return _failed(
|
|
@@ -7717,10 +8411,15 @@ class AiBuilderFacade:
|
|
|
7717
8411
|
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
8412
|
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
7719
8413
|
)
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
8414
|
+
if patch.preserve_query_conditions:
|
|
8415
|
+
query_condition_payload = None
|
|
8416
|
+
expected_query_conditions = None
|
|
8417
|
+
query_condition_issues = []
|
|
8418
|
+
else:
|
|
8419
|
+
query_condition_payload, expected_query_conditions, query_condition_issues = _build_view_query_conditions_payload(
|
|
8420
|
+
current_fields_by_name=current_fields_by_name,
|
|
8421
|
+
query_conditions=patch.query_conditions,
|
|
8422
|
+
)
|
|
7724
8423
|
if query_condition_issues:
|
|
7725
8424
|
first_issue = query_condition_issues[0]
|
|
7726
8425
|
return _failed(
|
|
@@ -7741,10 +8440,15 @@ class AiBuilderFacade:
|
|
|
7741
8440
|
},
|
|
7742
8441
|
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
7743
8442
|
)
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
|
|
8443
|
+
if patch.preserve_associated_resources:
|
|
8444
|
+
associated_resources_payload = None
|
|
8445
|
+
expected_associated_resources = None
|
|
8446
|
+
associated_resource_issues = []
|
|
8447
|
+
else:
|
|
8448
|
+
associated_resources_payload, expected_associated_resources, associated_resource_issues = _build_view_associated_resources_payload(
|
|
8449
|
+
associated_resources=patch.associated_resources,
|
|
8450
|
+
available_resources=associated_resources,
|
|
8451
|
+
)
|
|
7748
8452
|
if associated_resource_issues:
|
|
7749
8453
|
first_issue = associated_resource_issues[0]
|
|
7750
8454
|
return _failed(
|
|
@@ -7765,7 +8469,10 @@ class AiBuilderFacade:
|
|
|
7765
8469
|
)
|
|
7766
8470
|
explicit_button_dtos: list[dict[str, Any]] | None = None
|
|
7767
8471
|
expected_button_summary: list[dict[str, Any]] | None = None
|
|
7768
|
-
if patch.
|
|
8472
|
+
if patch.preserve_buttons:
|
|
8473
|
+
explicit_button_dtos = None
|
|
8474
|
+
expected_button_summary = None
|
|
8475
|
+
elif patch.buttons is not None:
|
|
7769
8476
|
explicit_button_dtos, button_issues = _build_view_button_dtos(
|
|
7770
8477
|
current_fields_by_name=current_fields_by_name,
|
|
7771
8478
|
bindings=patch.buttons,
|
|
@@ -8239,7 +8946,10 @@ class AiBuilderFacade:
|
|
|
8239
8946
|
expected_filter_summary = _normalize_view_filter_groups_for_compare(expected_filters)
|
|
8240
8947
|
expected_data_scope = "CUSTOM" if expected_filter_summary else "ALL"
|
|
8241
8948
|
actual_data_scope = str(config_result.get("dataScope") or "").strip().upper() or None
|
|
8242
|
-
filters_verified =
|
|
8949
|
+
filters_verified = (
|
|
8950
|
+
_view_filter_groups_equivalent(expected_filter_summary, actual_filters)
|
|
8951
|
+
and actual_data_scope == expected_data_scope
|
|
8952
|
+
)
|
|
8243
8953
|
verification_entry["filters_verified"] = filters_verified
|
|
8244
8954
|
verification_entry["view_key"] = verification_key
|
|
8245
8955
|
verification_entry["expected_filters"] = expected_filter_summary
|
|
@@ -8737,6 +9447,126 @@ class AiBuilderFacade:
|
|
|
8737
9447
|
"verified": verified,
|
|
8738
9448
|
}
|
|
8739
9449
|
|
|
9450
|
+
def _expand_chart_partial_patches(
|
|
9451
|
+
self,
|
|
9452
|
+
*,
|
|
9453
|
+
profile: str,
|
|
9454
|
+
app_key: str,
|
|
9455
|
+
existing_by_id: dict[str, dict[str, Any]],
|
|
9456
|
+
existing_by_name: dict[str, list[dict[str, Any]]],
|
|
9457
|
+
patch_charts: list[ChartPartialPatch],
|
|
9458
|
+
) -> tuple[list[ChartUpsertPatch], list[dict[str, Any]], list[dict[str, Any]]]:
|
|
9459
|
+
expanded: list[ChartUpsertPatch] = []
|
|
9460
|
+
issues: list[dict[str, Any]] = []
|
|
9461
|
+
results: list[dict[str, Any]] = []
|
|
9462
|
+
for index, patch in enumerate(patch_charts):
|
|
9463
|
+
chart_id = str(patch.chart_id or "").strip()
|
|
9464
|
+
if chart_id:
|
|
9465
|
+
if chart_id not in existing_by_id:
|
|
9466
|
+
issue = {
|
|
9467
|
+
"error_code": "CHART_NOT_FOUND",
|
|
9468
|
+
"reason_path": f"patch_charts[{index}].chart_id",
|
|
9469
|
+
"chart_id": chart_id,
|
|
9470
|
+
"message": "chart_id does not exist under this app",
|
|
9471
|
+
}
|
|
9472
|
+
issues.append(issue)
|
|
9473
|
+
results.append({"index": index, "status": "failed", **issue})
|
|
9474
|
+
continue
|
|
9475
|
+
else:
|
|
9476
|
+
name = str(patch.name or "").strip()
|
|
9477
|
+
matches = existing_by_name.get(name, [])
|
|
9478
|
+
if len(matches) != 1:
|
|
9479
|
+
issue = {
|
|
9480
|
+
"error_code": "AMBIGUOUS_CHART" if matches else "CHART_NOT_FOUND",
|
|
9481
|
+
"reason_path": f"patch_charts[{index}].name",
|
|
9482
|
+
"name": name,
|
|
9483
|
+
"candidate_chart_ids": [
|
|
9484
|
+
_extract_chart_identifier(item)
|
|
9485
|
+
for item in matches
|
|
9486
|
+
if _extract_chart_identifier(item)
|
|
9487
|
+
],
|
|
9488
|
+
"message": "patch_charts[] must target a single existing chart; use chart_id when names are duplicated",
|
|
9489
|
+
}
|
|
9490
|
+
issues.append(issue)
|
|
9491
|
+
results.append({"index": index, "status": "failed", **issue})
|
|
9492
|
+
continue
|
|
9493
|
+
chart_id = _extract_chart_identifier(matches[0])
|
|
9494
|
+
if not chart_id:
|
|
9495
|
+
issue = {"error_code": "CHART_ID_MISSING", "reason_path": f"patch_charts[{index}].name", "name": name}
|
|
9496
|
+
issues.append(issue)
|
|
9497
|
+
results.append({"index": index, "status": "failed", **issue})
|
|
9498
|
+
continue
|
|
9499
|
+
try:
|
|
9500
|
+
base = self.charts.qingbi_report_get_base(profile=profile, chart_id=chart_id).get("result") or {}
|
|
9501
|
+
config = self.charts.qingbi_report_get_config(profile=profile, chart_id=chart_id).get("result") or {}
|
|
9502
|
+
if not isinstance(base, dict):
|
|
9503
|
+
base = {}
|
|
9504
|
+
if not isinstance(config, dict):
|
|
9505
|
+
config = {}
|
|
9506
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
9507
|
+
api_error = _coerce_api_error(error)
|
|
9508
|
+
issue = {
|
|
9509
|
+
"error_code": "CHART_PATCH_DETAIL_READ_FAILED",
|
|
9510
|
+
"reason_path": f"patch_charts[{index}]",
|
|
9511
|
+
"chart_id": chart_id,
|
|
9512
|
+
"message": api_error.message,
|
|
9513
|
+
"transport_error": _transport_error_payload(api_error),
|
|
9514
|
+
}
|
|
9515
|
+
issues.append(issue)
|
|
9516
|
+
results.append({"index": index, "status": "failed", **issue})
|
|
9517
|
+
continue
|
|
9518
|
+
patch_payload = _chart_upsert_payload_from_existing(chart_id=chart_id, base=base, config=config)
|
|
9519
|
+
normalized_set, set_issues = _normalize_chart_partial_set(patch.set, reason_path=f"patch_charts[{index}].set")
|
|
9520
|
+
normalized_unset, unset_issues = _normalize_chart_partial_unset(patch.unset, reason_path=f"patch_charts[{index}].unset")
|
|
9521
|
+
if set_issues or unset_issues:
|
|
9522
|
+
patch_issues = [*set_issues, *unset_issues]
|
|
9523
|
+
issues.extend(patch_issues)
|
|
9524
|
+
results.append({"index": index, "status": "failed", "chart_id": chart_id, "issues": patch_issues})
|
|
9525
|
+
continue
|
|
9526
|
+
explicit_set_paths = set(normalized_set)
|
|
9527
|
+
for key, value in normalized_set.items():
|
|
9528
|
+
if key in {"config", "visibility"} and isinstance(value, dict):
|
|
9529
|
+
merged_value = deepcopy(patch_payload.get(key) if isinstance(patch_payload.get(key), dict) else {})
|
|
9530
|
+
_deep_merge_public_config(merged_value, value)
|
|
9531
|
+
patch_payload[key] = merged_value
|
|
9532
|
+
else:
|
|
9533
|
+
patch_payload[key] = value
|
|
9534
|
+
for key in normalized_unset:
|
|
9535
|
+
if key == "filters":
|
|
9536
|
+
patch_payload["filters"] = []
|
|
9537
|
+
explicit_set_paths.add("filters")
|
|
9538
|
+
elif key in {"question_config", "user_config"}:
|
|
9539
|
+
patch_payload[key] = []
|
|
9540
|
+
explicit_set_paths.add(key)
|
|
9541
|
+
elif key == "visibility":
|
|
9542
|
+
patch_payload.pop("visibility", None)
|
|
9543
|
+
try:
|
|
9544
|
+
expanded_patch = ChartUpsertPatch.model_validate(patch_payload)
|
|
9545
|
+
except Exception as error:
|
|
9546
|
+
issue = {
|
|
9547
|
+
"error_code": "CHART_PATCH_HYDRATION_FAILED",
|
|
9548
|
+
"reason_path": f"patch_charts[{index}]",
|
|
9549
|
+
"chart_id": chart_id,
|
|
9550
|
+
"message": str(error),
|
|
9551
|
+
"hydrated_payload": _compact_dict(patch_payload),
|
|
9552
|
+
}
|
|
9553
|
+
issues.append(issue)
|
|
9554
|
+
results.append({"index": index, "status": "failed", **issue})
|
|
9555
|
+
continue
|
|
9556
|
+
# Preserve model_fields_set semantics so config generation knows which public fields were explicitly replaced.
|
|
9557
|
+
expanded_patch.__pydantic_fields_set__ = set(explicit_set_paths)
|
|
9558
|
+
expanded.append(expanded_patch)
|
|
9559
|
+
results.append(
|
|
9560
|
+
{
|
|
9561
|
+
"index": index,
|
|
9562
|
+
"status": "expanded",
|
|
9563
|
+
"chart_id": chart_id,
|
|
9564
|
+
"set_paths": sorted(normalized_set),
|
|
9565
|
+
"unset_paths": sorted(normalized_unset),
|
|
9566
|
+
}
|
|
9567
|
+
)
|
|
9568
|
+
return expanded, issues, results
|
|
9569
|
+
|
|
8740
9570
|
def chart_apply(self, *, profile: str, request: ChartApplyRequest) -> JSONObject:
|
|
8741
9571
|
normalized_args = request.model_dump(mode="json")
|
|
8742
9572
|
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
@@ -8796,13 +9626,36 @@ class AiBuilderFacade:
|
|
|
8796
9626
|
continue
|
|
8797
9627
|
existing_by_name.setdefault(item_name, []).append(deepcopy(item))
|
|
8798
9628
|
|
|
9629
|
+
upsert_charts = list(request.upsert_charts)
|
|
9630
|
+
if request.patch_charts:
|
|
9631
|
+
expanded_charts, patch_issues, patch_results = self._expand_chart_partial_patches(
|
|
9632
|
+
profile=profile,
|
|
9633
|
+
app_key=app_key,
|
|
9634
|
+
existing_by_id=existing_by_id,
|
|
9635
|
+
existing_by_name=existing_by_name,
|
|
9636
|
+
patch_charts=request.patch_charts,
|
|
9637
|
+
)
|
|
9638
|
+
if patch_issues:
|
|
9639
|
+
return finalize(
|
|
9640
|
+
_failed(
|
|
9641
|
+
"CHART_PATCH_HYDRATION_FAILED",
|
|
9642
|
+
"one or more chart partial patches could not be hydrated; no write was executed",
|
|
9643
|
+
normalized_args=normalized_args,
|
|
9644
|
+
details={"patch_results": patch_results, "blocking_issues": patch_issues},
|
|
9645
|
+
suggested_next_call={"tool_name": "chart_get", "arguments": {"profile": profile, "chart_id": patch_issues[0].get("chart_id") or "CHART_ID"}},
|
|
9646
|
+
)
|
|
9647
|
+
)
|
|
9648
|
+
upsert_charts.extend(expanded_charts)
|
|
9649
|
+
normalized_args["upsert_charts"] = [patch.model_dump(mode="json") for patch in upsert_charts]
|
|
9650
|
+
normalized_args["patch_results"] = patch_results
|
|
9651
|
+
|
|
8799
9652
|
chart_results: list[dict[str, Any]] = []
|
|
8800
9653
|
created_ids: list[str] = []
|
|
8801
9654
|
updated_ids: list[str] = []
|
|
8802
9655
|
removed_ids: list[str] = []
|
|
8803
9656
|
failed_items: list[dict[str, Any]] = []
|
|
8804
9657
|
|
|
8805
|
-
for patch in
|
|
9658
|
+
for patch in upsert_charts:
|
|
8806
9659
|
try:
|
|
8807
9660
|
config_update_requested = _chart_patch_updates_chart_config(patch)
|
|
8808
9661
|
chart_visible_auth = (
|
|
@@ -10154,6 +11007,94 @@ def _custom_button_add_data_config_has_semantic_inputs(value: dict[str, Any]) ->
|
|
|
10154
11007
|
return bool(value.get("field_mappings") or value.get("fieldMappings") or value.get("default_values") or value.get("defaultValues"))
|
|
10155
11008
|
|
|
10156
11009
|
|
|
11010
|
+
def _normalize_custom_button_add_data_config_for_public(value: dict[str, Any]) -> dict[str, Any]:
|
|
11011
|
+
normalized: dict[str, Any] = {}
|
|
11012
|
+
related_app_key = _first_present(value, "related_app_key", "relatedAppKey", "target_app_key", "targetAppKey")
|
|
11013
|
+
related_app_name = _first_present(value, "related_app_name", "relatedAppName")
|
|
11014
|
+
if related_app_key is not None:
|
|
11015
|
+
normalized["related_app_key"] = related_app_key
|
|
11016
|
+
if related_app_name is not None:
|
|
11017
|
+
normalized["related_app_name"] = related_app_name
|
|
11018
|
+
relation_rules = _first_present(value, "que_relation", "queRelation")
|
|
11019
|
+
if isinstance(relation_rules, list):
|
|
11020
|
+
normalized["que_relation"] = [
|
|
11021
|
+
_normalize_custom_button_match_rule_for_public(rule)
|
|
11022
|
+
for rule in relation_rules
|
|
11023
|
+
if isinstance(rule, dict)
|
|
11024
|
+
]
|
|
11025
|
+
field_mappings = _first_present(value, "field_mappings", "fieldMappings", "mappings")
|
|
11026
|
+
if isinstance(field_mappings, list):
|
|
11027
|
+
normalized["field_mappings"] = deepcopy(field_mappings)
|
|
11028
|
+
default_values = _first_present(value, "default_values", "defaultValues", "defaults")
|
|
11029
|
+
if isinstance(default_values, dict):
|
|
11030
|
+
normalized["default_values"] = deepcopy(default_values)
|
|
11031
|
+
return _compact_dict(normalized)
|
|
11032
|
+
|
|
11033
|
+
|
|
11034
|
+
def _normalize_custom_button_match_rule_for_public(value: dict[str, Any]) -> dict[str, Any]:
|
|
11035
|
+
normalized: dict[str, Any] = {}
|
|
11036
|
+
key_pairs = {
|
|
11037
|
+
"que_id": ("que_id", "queId"),
|
|
11038
|
+
"que_title": ("que_title", "queTitle"),
|
|
11039
|
+
"que_type": ("que_type", "queType"),
|
|
11040
|
+
"date_type": ("date_type", "dateType"),
|
|
11041
|
+
"judge_type": ("judge_type", "judgeType"),
|
|
11042
|
+
"match_type": ("match_type", "matchType"),
|
|
11043
|
+
"judge_que_type": ("judge_que_type", "judgeQueType"),
|
|
11044
|
+
"judge_que_id": ("judge_que_id", "judgeQueId"),
|
|
11045
|
+
"path_value": ("path_value", "pathValue"),
|
|
11046
|
+
"table_update_type": ("table_update_type", "tableUpdateType"),
|
|
11047
|
+
"multi_value": ("multi_value", "multiValue"),
|
|
11048
|
+
"add_rule": ("add_rule", "addRule"),
|
|
11049
|
+
"field_id_prefix": ("field_id_prefix", "fieldIdPrefix"),
|
|
11050
|
+
}
|
|
11051
|
+
for public_key, aliases in key_pairs.items():
|
|
11052
|
+
raw_value = _first_present(value, *aliases)
|
|
11053
|
+
if raw_value is not None:
|
|
11054
|
+
normalized[public_key] = raw_value
|
|
11055
|
+
judge_values = _first_present(value, "judge_values", "judgeValues")
|
|
11056
|
+
if isinstance(judge_values, list):
|
|
11057
|
+
normalized["judge_values"] = [str(item) for item in judge_values if item is not None]
|
|
11058
|
+
elif judge_values is not None:
|
|
11059
|
+
normalized["judge_values"] = [str(judge_values)]
|
|
11060
|
+
judge_que_detail = _first_present(value, "judge_que_detail", "judgeQueDetail")
|
|
11061
|
+
if isinstance(judge_que_detail, dict):
|
|
11062
|
+
normalized["judge_que_detail"] = _compact_dict(
|
|
11063
|
+
{
|
|
11064
|
+
"que_id": _first_present(judge_que_detail, "que_id", "queId"),
|
|
11065
|
+
"que_title": _first_present(judge_que_detail, "que_title", "queTitle"),
|
|
11066
|
+
"que_type": _first_present(judge_que_detail, "que_type", "queType"),
|
|
11067
|
+
}
|
|
11068
|
+
)
|
|
11069
|
+
judge_value_details = _first_present(value, "judge_value_details", "judgeValueDetails")
|
|
11070
|
+
if isinstance(judge_value_details, list):
|
|
11071
|
+
details: list[dict[str, Any]] = []
|
|
11072
|
+
for item in judge_value_details:
|
|
11073
|
+
if not isinstance(item, dict):
|
|
11074
|
+
continue
|
|
11075
|
+
detail = _compact_dict(
|
|
11076
|
+
{
|
|
11077
|
+
"id": _first_present(item, "id", "opt_id", "optId", "member_id", "memberId"),
|
|
11078
|
+
"value": _first_present(item, "value", "name", "label", "title"),
|
|
11079
|
+
}
|
|
11080
|
+
)
|
|
11081
|
+
if detail:
|
|
11082
|
+
details.append(detail)
|
|
11083
|
+
normalized["judge_value_details"] = details
|
|
11084
|
+
filter_condition = _first_present(value, "filter_condition", "filterCondition")
|
|
11085
|
+
if isinstance(filter_condition, list):
|
|
11086
|
+
normalized["filter_condition"] = [
|
|
11087
|
+
[
|
|
11088
|
+
_normalize_custom_button_match_rule_for_public(item)
|
|
11089
|
+
for item in group
|
|
11090
|
+
if isinstance(item, dict)
|
|
11091
|
+
]
|
|
11092
|
+
for group in filter_condition
|
|
11093
|
+
if isinstance(group, list)
|
|
11094
|
+
]
|
|
11095
|
+
return _compact_dict(normalized)
|
|
11096
|
+
|
|
11097
|
+
|
|
10157
11098
|
def _custom_button_selector_payload(selector: Any) -> dict[str, Any]:
|
|
10158
11099
|
if isinstance(selector, dict):
|
|
10159
11100
|
payload = dict(selector)
|
|
@@ -10171,11 +11112,83 @@ def _custom_button_selector_payload(selector: Any) -> dict[str, Any]:
|
|
|
10171
11112
|
raw = str(selector or "").strip()
|
|
10172
11113
|
if not raw:
|
|
10173
11114
|
return {}
|
|
10174
|
-
|
|
10175
|
-
|
|
11115
|
+
parsed_int = _coerce_any_int(raw)
|
|
11116
|
+
if parsed_int is not None:
|
|
11117
|
+
return {"que_id": parsed_int}
|
|
10176
11118
|
return {"name": raw}
|
|
10177
11119
|
|
|
10178
11120
|
|
|
11121
|
+
_SYSTEM_MATCH_FIELD_ALIASES: dict[str, int] = {
|
|
11122
|
+
"数据id": -17,
|
|
11123
|
+
"数据ID": -17,
|
|
11124
|
+
"数据_id": -17,
|
|
11125
|
+
"row_record_id": -17,
|
|
11126
|
+
"data_id": -17,
|
|
11127
|
+
"record_id": -17,
|
|
11128
|
+
"apply_id": -17,
|
|
11129
|
+
"_id": -17,
|
|
11130
|
+
"编号": 0,
|
|
11131
|
+
"数据编号": 0,
|
|
11132
|
+
"record_number": 0,
|
|
11133
|
+
"serial_number": 0,
|
|
11134
|
+
"apply_num": 0,
|
|
11135
|
+
"data_num": 0,
|
|
11136
|
+
}
|
|
11137
|
+
|
|
11138
|
+
|
|
11139
|
+
def _normalize_system_match_alias(value: Any) -> str:
|
|
11140
|
+
raw = str(value or "").strip()
|
|
11141
|
+
if raw.startswith("{{") and raw.endswith("}}"):
|
|
11142
|
+
raw = raw[2:-2].strip()
|
|
11143
|
+
return raw.replace(" ", "").lower()
|
|
11144
|
+
|
|
11145
|
+
|
|
11146
|
+
def _system_match_field_from_selector(selector: Any) -> dict[str, Any] | None:
|
|
11147
|
+
if isinstance(selector, dict):
|
|
11148
|
+
for key in ("field_id", "fieldId", "que_id", "queId"):
|
|
11149
|
+
parsed = _coerce_any_int(selector.get(key))
|
|
11150
|
+
if parsed in {-17, 0}:
|
|
11151
|
+
return _system_match_field(parsed)
|
|
11152
|
+
for key in ("name", "title", "label", "value", "field", "source_field", "target_field"):
|
|
11153
|
+
raw = str(selector.get(key) or "").strip()
|
|
11154
|
+
if raw:
|
|
11155
|
+
field = _system_match_field_from_selector(raw)
|
|
11156
|
+
if field is not None:
|
|
11157
|
+
return field
|
|
11158
|
+
return None
|
|
11159
|
+
parsed = _coerce_any_int(selector)
|
|
11160
|
+
if parsed in {-17, 0}:
|
|
11161
|
+
return _system_match_field(parsed)
|
|
11162
|
+
normalized = _normalize_system_match_alias(selector)
|
|
11163
|
+
if normalized in _SYSTEM_MATCH_FIELD_ALIASES:
|
|
11164
|
+
return _system_match_field(_SYSTEM_MATCH_FIELD_ALIASES[normalized])
|
|
11165
|
+
return None
|
|
11166
|
+
|
|
11167
|
+
|
|
11168
|
+
def _system_match_field(que_id: int) -> dict[str, Any] | None:
|
|
11169
|
+
if que_id == -17:
|
|
11170
|
+
return {
|
|
11171
|
+
"field_id": -17,
|
|
11172
|
+
"que_id": -17,
|
|
11173
|
+
"que_type": 8,
|
|
11174
|
+
"name": "数据ID",
|
|
11175
|
+
"type": "system_record_id",
|
|
11176
|
+
"system_field": True,
|
|
11177
|
+
"system_kind": "row_record_id",
|
|
11178
|
+
}
|
|
11179
|
+
if que_id == 0:
|
|
11180
|
+
return {
|
|
11181
|
+
"field_id": 0,
|
|
11182
|
+
"que_id": 0,
|
|
11183
|
+
"que_type": 8,
|
|
11184
|
+
"name": "编号",
|
|
11185
|
+
"type": "system_record_number",
|
|
11186
|
+
"system_field": True,
|
|
11187
|
+
"system_kind": "record_number",
|
|
11188
|
+
}
|
|
11189
|
+
return None
|
|
11190
|
+
|
|
11191
|
+
|
|
10179
11192
|
def _resolve_custom_button_schema_field(
|
|
10180
11193
|
*,
|
|
10181
11194
|
fields: list[dict[str, Any]],
|
|
@@ -10184,6 +11197,11 @@ def _resolve_custom_button_schema_field(
|
|
|
10184
11197
|
role: str,
|
|
10185
11198
|
) -> tuple[dict[str, Any] | None, dict[str, Any] | None]:
|
|
10186
11199
|
selector_payload = _custom_button_selector_payload(selector)
|
|
11200
|
+
allow_system = role in {"source", "match_source", "match_target"}
|
|
11201
|
+
if allow_system:
|
|
11202
|
+
system_field = _system_match_field_from_selector(selector_payload or selector)
|
|
11203
|
+
if system_field is not None:
|
|
11204
|
+
return system_field, None
|
|
10187
11205
|
if not selector_payload:
|
|
10188
11206
|
return None, {
|
|
10189
11207
|
"error_code": "CUSTOM_BUTTON_MAPPING_FIELD_NOT_FOUND",
|
|
@@ -10193,18 +11211,19 @@ def _resolve_custom_button_schema_field(
|
|
|
10193
11211
|
}
|
|
10194
11212
|
matched: list[dict[str, Any]] = []
|
|
10195
11213
|
field_id = selector_payload.get("field_id")
|
|
10196
|
-
que_id =
|
|
11214
|
+
que_id = _coerce_any_int(selector_payload.get("que_id"))
|
|
10197
11215
|
name = str(selector_payload.get("name") or "").strip()
|
|
10198
11216
|
if field_id is not None:
|
|
10199
11217
|
field_id_text = str(field_id).strip()
|
|
11218
|
+
field_id_int = _coerce_any_int(field_id_text)
|
|
10200
11219
|
matched = [
|
|
10201
11220
|
field
|
|
10202
11221
|
for field in fields
|
|
10203
11222
|
if str(field.get("field_id") or "").strip() == field_id_text
|
|
10204
|
-
or (
|
|
11223
|
+
or (field_id_int is not None and _coerce_any_int(field.get("que_id")) == field_id_int)
|
|
10205
11224
|
]
|
|
10206
11225
|
elif que_id is not None:
|
|
10207
|
-
matched = [field for field in fields if
|
|
11226
|
+
matched = [field for field in fields if _coerce_any_int(field.get("que_id")) == que_id]
|
|
10208
11227
|
elif name:
|
|
10209
11228
|
matched = [field for field in fields if str(field.get("name") or "").strip() == name]
|
|
10210
11229
|
if len(matched) == 1:
|
|
@@ -10222,45 +11241,244 @@ def _resolve_custom_button_schema_field(
|
|
|
10222
11241
|
}
|
|
10223
11242
|
|
|
10224
11243
|
|
|
10225
|
-
def _custom_button_mapping_type_issue(
|
|
11244
|
+
def _custom_button_mapping_type_issue(
|
|
11245
|
+
*,
|
|
11246
|
+
source_field: dict[str, Any],
|
|
11247
|
+
target_field: dict[str, Any],
|
|
11248
|
+
reason_path: str,
|
|
11249
|
+
source_app_key: str | None = None,
|
|
11250
|
+
error_code: str = "CUSTOM_BUTTON_MAPPING_TYPE_MISMATCH",
|
|
11251
|
+
context_label: str = "field mapping",
|
|
11252
|
+
) -> dict[str, Any] | None:
|
|
11253
|
+
source_type = str(source_field.get("type") or "")
|
|
11254
|
+
target_type = str(target_field.get("type") or "")
|
|
11255
|
+
compatible, reason = _match_mapping_types_compatible(
|
|
11256
|
+
source_field=source_field,
|
|
11257
|
+
target_field=target_field,
|
|
11258
|
+
source_app_key=source_app_key,
|
|
11259
|
+
)
|
|
11260
|
+
if compatible:
|
|
11261
|
+
return None
|
|
11262
|
+
if reason == "reference_source_mismatch":
|
|
11263
|
+
target_app_key = _relation_target_app_key(target_field)
|
|
11264
|
+
return {
|
|
11265
|
+
"error_code": "MATCH_RULE_REFERENCE_SOURCE_MISMATCH",
|
|
11266
|
+
"reason_path": reason_path,
|
|
11267
|
+
"source_app_key": source_app_key,
|
|
11268
|
+
"target_relation_app_key": target_app_key,
|
|
11269
|
+
"source_field": _match_field_summary(source_field),
|
|
11270
|
+
"target_field": _match_field_summary(target_field),
|
|
11271
|
+
"message": "数据ID represents the current source record, but the target relation field points to another app",
|
|
11272
|
+
"next_action": "choose a relation field that targets the current source app, or map a compatible non-relation field",
|
|
11273
|
+
}
|
|
11274
|
+
return {
|
|
11275
|
+
"error_code": error_code,
|
|
11276
|
+
"reason_path": reason_path,
|
|
11277
|
+
"source_field": _match_field_summary(source_field),
|
|
11278
|
+
"target_field": _match_field_summary(target_field),
|
|
11279
|
+
"message": f"source and target fields have incompatible types for {context_label}",
|
|
11280
|
+
"compatibility_reason": reason,
|
|
11281
|
+
"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",
|
|
11282
|
+
}
|
|
11283
|
+
|
|
11284
|
+
|
|
11285
|
+
def _retag_associated_resource_match_field_issue(issue: dict[str, Any]) -> dict[str, Any]:
|
|
11286
|
+
retagged = dict(issue)
|
|
11287
|
+
code = str(retagged.get("error_code") or "")
|
|
11288
|
+
if code == "CUSTOM_BUTTON_MAPPING_FIELD_NOT_FOUND":
|
|
11289
|
+
retagged["error_code"] = "ASSOCIATED_RESOURCE_MAPPING_FIELD_NOT_FOUND"
|
|
11290
|
+
elif code == "CUSTOM_BUTTON_MAPPING_FIELD_AMBIGUOUS":
|
|
11291
|
+
retagged["error_code"] = "ASSOCIATED_RESOURCE_MAPPING_FIELD_AMBIGUOUS"
|
|
11292
|
+
return retagged
|
|
11293
|
+
|
|
11294
|
+
|
|
11295
|
+
def _retag_associated_resource_static_value_issue(issue: dict[str, Any]) -> dict[str, Any]:
|
|
11296
|
+
retagged = dict(issue)
|
|
11297
|
+
if str(retagged.get("error_code") or "") == "CUSTOM_BUTTON_DEFAULT_VALUE_UNSUPPORTED":
|
|
11298
|
+
retagged["error_code"] = "ASSOCIATED_RESOURCE_STATIC_VALUE_UNSUPPORTED"
|
|
11299
|
+
retagged["message"] = str(retagged.get("message") or "static associated resource match value is unsupported")
|
|
11300
|
+
retagged["next_action"] = str(
|
|
11301
|
+
retagged.get("next_action")
|
|
11302
|
+
or "retry match_mappings[].value with a literal value supported by the target field type"
|
|
11303
|
+
)
|
|
11304
|
+
return retagged
|
|
11305
|
+
|
|
11306
|
+
|
|
11307
|
+
def _match_field_summary(field: dict[str, Any]) -> dict[str, Any]:
|
|
11308
|
+
return {
|
|
11309
|
+
"title": field.get("name"),
|
|
11310
|
+
"field_id": field.get("field_id"),
|
|
11311
|
+
"que_id": field.get("que_id"),
|
|
11312
|
+
"type": field.get("type"),
|
|
11313
|
+
"system_field": bool(field.get("system_field")),
|
|
11314
|
+
}
|
|
11315
|
+
|
|
11316
|
+
|
|
11317
|
+
def _relation_target_app_key(field: dict[str, Any]) -> str | None:
|
|
11318
|
+
for key in ("target_app_key", "targetAppKey", "refer_app_key", "referAppKey"):
|
|
11319
|
+
value = str(field.get(key) or "").strip()
|
|
11320
|
+
if value:
|
|
11321
|
+
return value
|
|
11322
|
+
for container_key in ("relation", "reference", "reference_config", "referenceConfig", "config"):
|
|
11323
|
+
container = field.get(container_key)
|
|
11324
|
+
if not isinstance(container, dict):
|
|
11325
|
+
continue
|
|
11326
|
+
nested = _relation_target_app_key(container)
|
|
11327
|
+
if nested:
|
|
11328
|
+
return nested
|
|
11329
|
+
template = field.get("_reference_config_template")
|
|
11330
|
+
if isinstance(template, dict):
|
|
11331
|
+
nested = _relation_target_app_key(template)
|
|
11332
|
+
if nested:
|
|
11333
|
+
return nested
|
|
11334
|
+
return None
|
|
11335
|
+
|
|
11336
|
+
|
|
11337
|
+
def _match_mapping_types_compatible(
|
|
10226
11338
|
*,
|
|
10227
11339
|
source_field: dict[str, Any],
|
|
10228
11340
|
target_field: dict[str, Any],
|
|
10229
|
-
|
|
10230
|
-
) ->
|
|
11341
|
+
source_app_key: str | None = None,
|
|
11342
|
+
) -> tuple[bool, str | None]:
|
|
10231
11343
|
source_type = str(source_field.get("type") or "")
|
|
10232
11344
|
target_type = str(target_field.get("type") or "")
|
|
11345
|
+
unsupported = {
|
|
11346
|
+
FieldType.attachment.value,
|
|
11347
|
+
FieldType.subtable.value,
|
|
11348
|
+
FieldType.code_block.value,
|
|
11349
|
+
FieldType.q_linker.value,
|
|
11350
|
+
FieldType.address.value,
|
|
11351
|
+
}
|
|
11352
|
+
if source_type in unsupported or target_type in unsupported:
|
|
11353
|
+
return False, "unsupported_field_type"
|
|
11354
|
+
|
|
11355
|
+
if target_type == FieldType.relation.value:
|
|
11356
|
+
if source_type == "system_record_id":
|
|
11357
|
+
target_app_key = _relation_target_app_key(target_field)
|
|
11358
|
+
if source_app_key and target_app_key and str(target_app_key).strip() != str(source_app_key).strip():
|
|
11359
|
+
return False, "reference_source_mismatch"
|
|
11360
|
+
return True, None
|
|
11361
|
+
if source_type == FieldType.relation.value:
|
|
11362
|
+
source_target = _relation_target_app_key(source_field)
|
|
11363
|
+
target_target = _relation_target_app_key(target_field)
|
|
11364
|
+
if source_target and target_target and source_target != target_target:
|
|
11365
|
+
return False, "relation_target_app_mismatch"
|
|
11366
|
+
return True, None
|
|
11367
|
+
return False, "relation_requires_record_id_or_relation"
|
|
11368
|
+
if source_type == FieldType.relation.value:
|
|
11369
|
+
if target_type == "system_record_id":
|
|
11370
|
+
return True, None
|
|
11371
|
+
return False, "relation_only_matches_relation_or_record_id"
|
|
11372
|
+
|
|
11373
|
+
if source_type == "system_record_id":
|
|
11374
|
+
if target_type in {
|
|
11375
|
+
"system_record_id",
|
|
11376
|
+
FieldType.text.value,
|
|
11377
|
+
FieldType.long_text.value,
|
|
11378
|
+
FieldType.number.value,
|
|
11379
|
+
FieldType.amount.value,
|
|
11380
|
+
}:
|
|
11381
|
+
return True, None
|
|
11382
|
+
return False, "record_id_requires_relation_or_id_compatible_target"
|
|
11383
|
+
if target_type == "system_record_id":
|
|
11384
|
+
if source_type in {
|
|
11385
|
+
"system_record_id",
|
|
11386
|
+
FieldType.text.value,
|
|
11387
|
+
FieldType.long_text.value,
|
|
11388
|
+
FieldType.number.value,
|
|
11389
|
+
FieldType.amount.value,
|
|
11390
|
+
}:
|
|
11391
|
+
return True, None
|
|
11392
|
+
return False, "record_id_requires_id_compatible_source"
|
|
11393
|
+
|
|
11394
|
+
if source_type == "system_record_number":
|
|
11395
|
+
if target_type in {
|
|
11396
|
+
"system_record_number",
|
|
11397
|
+
FieldType.text.value,
|
|
11398
|
+
FieldType.long_text.value,
|
|
11399
|
+
FieldType.number.value,
|
|
11400
|
+
FieldType.amount.value,
|
|
11401
|
+
}:
|
|
11402
|
+
return True, None
|
|
11403
|
+
return False, "record_number_requires_text_or_number_target"
|
|
11404
|
+
if target_type == "system_record_number":
|
|
11405
|
+
if source_type in {
|
|
11406
|
+
"system_record_number",
|
|
11407
|
+
FieldType.text.value,
|
|
11408
|
+
FieldType.long_text.value,
|
|
11409
|
+
FieldType.number.value,
|
|
11410
|
+
FieldType.amount.value,
|
|
11411
|
+
}:
|
|
11412
|
+
return True, None
|
|
11413
|
+
return False, "record_number_requires_text_or_number_source"
|
|
11414
|
+
|
|
11415
|
+
exact_only = {FieldType.member.value, FieldType.department.value}
|
|
11416
|
+
if source_type in exact_only or target_type in exact_only:
|
|
11417
|
+
return (source_type == target_type, None if source_type == target_type else "member_department_require_same_type")
|
|
11418
|
+
|
|
10233
11419
|
compatible_groups = [
|
|
10234
11420
|
{FieldType.text.value, FieldType.long_text.value, FieldType.phone.value, FieldType.email.value},
|
|
10235
11421
|
{FieldType.number.value, FieldType.amount.value},
|
|
10236
11422
|
{FieldType.date.value, FieldType.datetime.value},
|
|
10237
11423
|
{FieldType.single_select.value, FieldType.multi_select.value, FieldType.boolean.value},
|
|
10238
11424
|
]
|
|
10239
|
-
|
|
10240
|
-
|
|
10241
|
-
|
|
10242
|
-
|
|
10243
|
-
|
|
10244
|
-
|
|
10245
|
-
|
|
11425
|
+
if source_type == target_type or any(source_type in group and target_type in group for group in compatible_groups):
|
|
11426
|
+
return True, None
|
|
11427
|
+
return False, "different_type_family"
|
|
11428
|
+
|
|
11429
|
+
|
|
11430
|
+
def _match_mapping_operator_to_judge_type(operator: Any, *, reason_path: str) -> tuple[int, dict[str, Any] | None]:
|
|
11431
|
+
normalized = str(operator or "eq").strip().lower()
|
|
11432
|
+
aliases = {
|
|
11433
|
+
"eq": JUDGE_EQUAL,
|
|
11434
|
+
"equal": JUDGE_EQUAL,
|
|
11435
|
+
"=": JUDGE_EQUAL,
|
|
11436
|
+
"==": JUDGE_EQUAL,
|
|
11437
|
+
"neq": JUDGE_UNEQUAL,
|
|
11438
|
+
"ne": JUDGE_UNEQUAL,
|
|
11439
|
+
"!=": JUDGE_UNEQUAL,
|
|
11440
|
+
"not_eq": JUDGE_UNEQUAL,
|
|
11441
|
+
"not_equal": JUDGE_UNEQUAL,
|
|
11442
|
+
"gte": JUDGE_GREATER_OR_EQUAL,
|
|
11443
|
+
"ge": JUDGE_GREATER_OR_EQUAL,
|
|
11444
|
+
">=": JUDGE_GREATER_OR_EQUAL,
|
|
11445
|
+
"greater_or_equal": JUDGE_GREATER_OR_EQUAL,
|
|
11446
|
+
"lte": JUDGE_LESS_OR_EQUAL,
|
|
11447
|
+
"le": JUDGE_LESS_OR_EQUAL,
|
|
11448
|
+
"<=": JUDGE_LESS_OR_EQUAL,
|
|
11449
|
+
"less_or_equal": JUDGE_LESS_OR_EQUAL,
|
|
11450
|
+
"contains": JUDGE_FUZZY_MATCH,
|
|
11451
|
+
"like": JUDGE_FUZZY_MATCH,
|
|
11452
|
+
"fuzzy": JUDGE_FUZZY_MATCH,
|
|
11453
|
+
"fuzzy_match": JUDGE_FUZZY_MATCH,
|
|
11454
|
+
"in": JUDGE_EQUAL_ANY,
|
|
11455
|
+
"any": JUDGE_EQUAL_ANY,
|
|
11456
|
+
"equal_any": JUDGE_EQUAL_ANY,
|
|
11457
|
+
"include_any": JUDGE_INCLUDE_ANY,
|
|
11458
|
+
"includes_any": JUDGE_INCLUDE_ANY,
|
|
11459
|
+
}
|
|
11460
|
+
if normalized in aliases:
|
|
11461
|
+
return aliases[normalized], None
|
|
11462
|
+
return JUDGE_EQUAL, {
|
|
11463
|
+
"error_code": "INVALID_MATCH_MAPPING_OPERATOR",
|
|
10246
11464
|
"reason_path": reason_path,
|
|
10247
|
-
"
|
|
10248
|
-
"
|
|
10249
|
-
"message": "
|
|
10250
|
-
"next_action": "
|
|
11465
|
+
"operator": operator,
|
|
11466
|
+
"allowed_values": sorted(aliases),
|
|
11467
|
+
"message": "match_mappings[].operator is not supported",
|
|
11468
|
+
"next_action": "retry with one of the allowed operator values, such as eq, contains, in, gte, or lte",
|
|
10251
11469
|
}
|
|
10252
11470
|
|
|
10253
11471
|
|
|
10254
11472
|
def _custom_button_question_ref(field: dict[str, Any]) -> dict[str, Any]:
|
|
10255
11473
|
return {
|
|
10256
|
-
"que_id":
|
|
11474
|
+
"que_id": _coerce_any_int(field.get("que_id")),
|
|
10257
11475
|
"que_title": str(field.get("name") or ""),
|
|
10258
11476
|
"que_type": _coerce_nonnegative_int(field.get("que_type")),
|
|
10259
11477
|
}
|
|
10260
11478
|
|
|
10261
11479
|
|
|
10262
11480
|
def _custom_button_field_mapping_rule(*, source_field: dict[str, Any], target_field: dict[str, Any]) -> dict[str, Any]:
|
|
10263
|
-
source_que_id =
|
|
11481
|
+
source_que_id = _coerce_any_int(source_field.get("que_id"))
|
|
10264
11482
|
return {
|
|
10265
11483
|
**_custom_button_question_ref(target_field),
|
|
10266
11484
|
"match_type": MATCH_TYPE_QUESTION,
|
|
@@ -10285,6 +11503,36 @@ def _custom_button_default_value_rule(*, target_field: dict[str, Any], value_det
|
|
|
10285
11503
|
}
|
|
10286
11504
|
|
|
10287
11505
|
|
|
11506
|
+
def _summarize_compiled_match_rules(rules: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
11507
|
+
return [
|
|
11508
|
+
_compact_dict(
|
|
11509
|
+
{
|
|
11510
|
+
"target_field": {
|
|
11511
|
+
"title": rule.get("que_title"),
|
|
11512
|
+
"field_id": rule.get("que_id"),
|
|
11513
|
+
"que_id": rule.get("que_id"),
|
|
11514
|
+
"que_type": rule.get("que_type"),
|
|
11515
|
+
},
|
|
11516
|
+
"match_type": rule.get("match_type"),
|
|
11517
|
+
"judge_type": rule.get("judge_type"),
|
|
11518
|
+
"source_field": (
|
|
11519
|
+
{
|
|
11520
|
+
"title": (rule.get("judge_que_detail") or {}).get("que_title"),
|
|
11521
|
+
"field_id": rule.get("judge_que_id"),
|
|
11522
|
+
"que_id": rule.get("judge_que_id"),
|
|
11523
|
+
"que_type": rule.get("judge_que_type"),
|
|
11524
|
+
}
|
|
11525
|
+
if rule.get("match_type") == MATCH_TYPE_QUESTION
|
|
11526
|
+
else None
|
|
11527
|
+
),
|
|
11528
|
+
"static_values": rule.get("judge_value_details") or rule.get("judge_values") if rule.get("match_type") != MATCH_TYPE_QUESTION else None,
|
|
11529
|
+
}
|
|
11530
|
+
)
|
|
11531
|
+
for rule in rules
|
|
11532
|
+
if isinstance(rule, dict)
|
|
11533
|
+
]
|
|
11534
|
+
|
|
11535
|
+
|
|
10288
11536
|
def _resolve_custom_button_option_detail(*, target_field: dict[str, Any], value: Any) -> dict[str, Any] | None:
|
|
10289
11537
|
option_details = [item for item in target_field.get("option_details") or [] if isinstance(item, dict)]
|
|
10290
11538
|
value_id = _coerce_positive_int(value.get("id", value.get("opt_id", value.get("optId"))) if isinstance(value, dict) else value)
|
|
@@ -10450,7 +11698,7 @@ def _normalize_custom_button_detail(item: dict[str, Any]) -> dict[str, Any]:
|
|
|
10450
11698
|
if not isinstance(trigger_add_data_config, dict):
|
|
10451
11699
|
trigger_add_data_config = item.get("triggerAddDataConfig")
|
|
10452
11700
|
if isinstance(trigger_add_data_config, dict):
|
|
10453
|
-
normalized["trigger_add_data_config"] =
|
|
11701
|
+
normalized["trigger_add_data_config"] = _normalize_custom_button_add_data_config_for_public(trigger_add_data_config)
|
|
10454
11702
|
external_qrobot_config = item.get("external_qrobot_config")
|
|
10455
11703
|
if not isinstance(external_qrobot_config, dict):
|
|
10456
11704
|
external_qrobot_config = item.get("customButtonExternalQRobotRelationVO")
|
|
@@ -10464,6 +11712,148 @@ def _normalize_custom_button_detail(item: dict[str, Any]) -> dict[str, Any]:
|
|
|
10464
11712
|
return normalized
|
|
10465
11713
|
|
|
10466
11714
|
|
|
11715
|
+
_CUSTOM_BUTTON_PARTIAL_PATCH_KEY_ALIASES = {
|
|
11716
|
+
"button_text": "button_text",
|
|
11717
|
+
"buttonText": "button_text",
|
|
11718
|
+
"name": "button_text",
|
|
11719
|
+
"text": "button_text",
|
|
11720
|
+
"background_color": "background_color",
|
|
11721
|
+
"backgroundColor": "background_color",
|
|
11722
|
+
"text_color": "text_color",
|
|
11723
|
+
"textColor": "text_color",
|
|
11724
|
+
"button_icon": "button_icon",
|
|
11725
|
+
"buttonIcon": "button_icon",
|
|
11726
|
+
"style_preset": "style_preset",
|
|
11727
|
+
"stylePreset": "style_preset",
|
|
11728
|
+
"trigger_action": "trigger_action",
|
|
11729
|
+
"triggerAction": "trigger_action",
|
|
11730
|
+
"trigger_link_url": "trigger_link_url",
|
|
11731
|
+
"triggerLinkUrl": "trigger_link_url",
|
|
11732
|
+
"trigger_add_data_config": "trigger_add_data_config",
|
|
11733
|
+
"triggerAddDataConfig": "trigger_add_data_config",
|
|
11734
|
+
"add_data_config": "trigger_add_data_config",
|
|
11735
|
+
"addDataConfig": "trigger_add_data_config",
|
|
11736
|
+
"external_qrobot_config": "external_qrobot_config",
|
|
11737
|
+
"externalQrobotConfig": "external_qrobot_config",
|
|
11738
|
+
"customButtonExternalQRobotRelationVO": "external_qrobot_config",
|
|
11739
|
+
"trigger_wings_config": "trigger_wings_config",
|
|
11740
|
+
"triggerWingsConfig": "trigger_wings_config",
|
|
11741
|
+
}
|
|
11742
|
+
|
|
11743
|
+
_CUSTOM_BUTTON_PARTIAL_SET_KEYS = {
|
|
11744
|
+
"button_text",
|
|
11745
|
+
"background_color",
|
|
11746
|
+
"text_color",
|
|
11747
|
+
"button_icon",
|
|
11748
|
+
"style_preset",
|
|
11749
|
+
"trigger_action",
|
|
11750
|
+
"trigger_link_url",
|
|
11751
|
+
"trigger_add_data_config",
|
|
11752
|
+
"external_qrobot_config",
|
|
11753
|
+
"trigger_wings_config",
|
|
11754
|
+
}
|
|
11755
|
+
|
|
11756
|
+
_CUSTOM_BUTTON_PARTIAL_UNSET_KEYS = {
|
|
11757
|
+
"trigger_link_url",
|
|
11758
|
+
"trigger_add_data_config",
|
|
11759
|
+
"external_qrobot_config",
|
|
11760
|
+
"trigger_wings_config",
|
|
11761
|
+
}
|
|
11762
|
+
|
|
11763
|
+
|
|
11764
|
+
def _canonical_custom_button_partial_patch_key(key: Any) -> str:
|
|
11765
|
+
raw = str(key or "").strip()
|
|
11766
|
+
return _CUSTOM_BUTTON_PARTIAL_PATCH_KEY_ALIASES.get(raw, raw)
|
|
11767
|
+
|
|
11768
|
+
|
|
11769
|
+
def _normalize_custom_button_partial_set(raw_set: Any, *, reason_path: str) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
11770
|
+
if not isinstance(raw_set, dict):
|
|
11771
|
+
return {}, [{"error_code": "INVALID_CUSTOM_BUTTON_PATCH_SET", "reason_path": reason_path, "message": "patch_buttons[].set must be an object"}]
|
|
11772
|
+
normalized: dict[str, Any] = {}
|
|
11773
|
+
issues: list[dict[str, Any]] = []
|
|
11774
|
+
for raw_key, value in raw_set.items():
|
|
11775
|
+
key_path = [part for part in str(raw_key or "").strip().split(".") if part]
|
|
11776
|
+
if not key_path:
|
|
11777
|
+
continue
|
|
11778
|
+
root = _canonical_custom_button_partial_patch_key(key_path[0])
|
|
11779
|
+
if root not in _CUSTOM_BUTTON_PARTIAL_SET_KEYS:
|
|
11780
|
+
issues.append(
|
|
11781
|
+
{
|
|
11782
|
+
"error_code": "UNSUPPORTED_CUSTOM_BUTTON_PATCH_FIELD",
|
|
11783
|
+
"reason_path": f"{reason_path}.{raw_key}",
|
|
11784
|
+
"field": raw_key,
|
|
11785
|
+
"allowed_fields": sorted(_CUSTOM_BUTTON_PARTIAL_SET_KEYS),
|
|
11786
|
+
}
|
|
11787
|
+
)
|
|
11788
|
+
continue
|
|
11789
|
+
if len(key_path) == 1:
|
|
11790
|
+
normalized[root] = value
|
|
11791
|
+
continue
|
|
11792
|
+
target = normalized.setdefault(root, {})
|
|
11793
|
+
if not isinstance(target, dict):
|
|
11794
|
+
issues.append(
|
|
11795
|
+
{
|
|
11796
|
+
"error_code": "INVALID_CUSTOM_BUTTON_PATCH_PATH",
|
|
11797
|
+
"reason_path": f"{reason_path}.{raw_key}",
|
|
11798
|
+
"message": "cannot combine a scalar replacement and nested patch for the same field",
|
|
11799
|
+
}
|
|
11800
|
+
)
|
|
11801
|
+
continue
|
|
11802
|
+
cursor = target
|
|
11803
|
+
for part in key_path[1:-1]:
|
|
11804
|
+
next_value = cursor.setdefault(part, {})
|
|
11805
|
+
if not isinstance(next_value, dict):
|
|
11806
|
+
issues.append({"error_code": "INVALID_CUSTOM_BUTTON_PATCH_PATH", "reason_path": f"{reason_path}.{raw_key}"})
|
|
11807
|
+
break
|
|
11808
|
+
cursor = next_value
|
|
11809
|
+
else:
|
|
11810
|
+
cursor[key_path[-1]] = value
|
|
11811
|
+
return normalized, issues
|
|
11812
|
+
|
|
11813
|
+
|
|
11814
|
+
def _normalize_custom_button_partial_unset(raw_unset: Any, *, reason_path: str) -> tuple[set[str], list[dict[str, Any]]]:
|
|
11815
|
+
if not isinstance(raw_unset, list):
|
|
11816
|
+
return set(), [{"error_code": "INVALID_CUSTOM_BUTTON_PATCH_UNSET", "reason_path": reason_path, "message": "patch_buttons[].unset must be a list"}]
|
|
11817
|
+
normalized: set[str] = set()
|
|
11818
|
+
issues: list[dict[str, Any]] = []
|
|
11819
|
+
for index, raw_key in enumerate(raw_unset):
|
|
11820
|
+
root = _canonical_custom_button_partial_patch_key(str(raw_key or "").strip().split(".")[0])
|
|
11821
|
+
if root not in _CUSTOM_BUTTON_PARTIAL_UNSET_KEYS:
|
|
11822
|
+
issues.append(
|
|
11823
|
+
{
|
|
11824
|
+
"error_code": "UNSUPPORTED_CUSTOM_BUTTON_PATCH_UNSET_FIELD",
|
|
11825
|
+
"reason_path": f"{reason_path}[{index}]",
|
|
11826
|
+
"field": raw_key,
|
|
11827
|
+
"allowed_fields": sorted(_CUSTOM_BUTTON_PARTIAL_UNSET_KEYS),
|
|
11828
|
+
}
|
|
11829
|
+
)
|
|
11830
|
+
continue
|
|
11831
|
+
normalized.add(root)
|
|
11832
|
+
return normalized, issues
|
|
11833
|
+
|
|
11834
|
+
|
|
11835
|
+
def _custom_button_upsert_payload_from_detail(
|
|
11836
|
+
detail: dict[str, Any],
|
|
11837
|
+
*,
|
|
11838
|
+
button_id: int,
|
|
11839
|
+
client_key: str | None = None,
|
|
11840
|
+
) -> dict[str, Any]:
|
|
11841
|
+
payload = {
|
|
11842
|
+
"button_id": button_id,
|
|
11843
|
+
"client_key": client_key,
|
|
11844
|
+
"button_text": detail.get("button_text"),
|
|
11845
|
+
"background_color": detail.get("background_color"),
|
|
11846
|
+
"text_color": detail.get("text_color"),
|
|
11847
|
+
"button_icon": detail.get("button_icon"),
|
|
11848
|
+
"trigger_action": detail.get("trigger_action"),
|
|
11849
|
+
"trigger_link_url": detail.get("trigger_link_url"),
|
|
11850
|
+
"trigger_add_data_config": deepcopy(detail.get("trigger_add_data_config")) if isinstance(detail.get("trigger_add_data_config"), dict) else None,
|
|
11851
|
+
"external_qrobot_config": deepcopy(detail.get("external_qrobot_config")) if isinstance(detail.get("external_qrobot_config"), dict) else None,
|
|
11852
|
+
"trigger_wings_config": deepcopy(detail.get("trigger_wings_config")) if isinstance(detail.get("trigger_wings_config"), dict) else None,
|
|
11853
|
+
}
|
|
11854
|
+
return _compact_dict(payload)
|
|
11855
|
+
|
|
11856
|
+
|
|
10467
11857
|
def _failed(
|
|
10468
11858
|
error_code: str,
|
|
10469
11859
|
message: str,
|
|
@@ -10956,6 +12346,125 @@ def _map_public_chart_type_to_backend(chart_type: PublicChartType) -> str:
|
|
|
10956
12346
|
}[chart_type]
|
|
10957
12347
|
|
|
10958
12348
|
|
|
12349
|
+
_CHART_PARTIAL_PATCH_KEY_ALIASES = {
|
|
12350
|
+
"name": "name",
|
|
12351
|
+
"chart_name": "name",
|
|
12352
|
+
"chartName": "name",
|
|
12353
|
+
"type": "chart_type",
|
|
12354
|
+
"chart_type": "chart_type",
|
|
12355
|
+
"chartType": "chart_type",
|
|
12356
|
+
"dimension_fields": "dimension_field_ids",
|
|
12357
|
+
"dimension_field_ids": "dimension_field_ids",
|
|
12358
|
+
"dimensionFieldIds": "dimension_field_ids",
|
|
12359
|
+
"indicator_fields": "indicator_field_ids",
|
|
12360
|
+
"indicator_field_ids": "indicator_field_ids",
|
|
12361
|
+
"indicatorFieldIds": "indicator_field_ids",
|
|
12362
|
+
"metric_field_ids": "indicator_field_ids",
|
|
12363
|
+
"filters": "filters",
|
|
12364
|
+
"question_config": "question_config",
|
|
12365
|
+
"questionConfig": "question_config",
|
|
12366
|
+
"user_config": "user_config",
|
|
12367
|
+
"userConfig": "user_config",
|
|
12368
|
+
"config": "config",
|
|
12369
|
+
"visibility": "visibility",
|
|
12370
|
+
}
|
|
12371
|
+
|
|
12372
|
+
_CHART_PARTIAL_SET_KEYS = {
|
|
12373
|
+
"name",
|
|
12374
|
+
"chart_type",
|
|
12375
|
+
"dimension_field_ids",
|
|
12376
|
+
"indicator_field_ids",
|
|
12377
|
+
"filters",
|
|
12378
|
+
"question_config",
|
|
12379
|
+
"user_config",
|
|
12380
|
+
"config",
|
|
12381
|
+
"visibility",
|
|
12382
|
+
}
|
|
12383
|
+
|
|
12384
|
+
_CHART_PARTIAL_UNSET_KEYS = {"filters", "question_config", "user_config", "visibility"}
|
|
12385
|
+
|
|
12386
|
+
|
|
12387
|
+
def _canonical_chart_partial_patch_key(key: Any) -> str:
|
|
12388
|
+
raw = str(key or "").strip()
|
|
12389
|
+
return _CHART_PARTIAL_PATCH_KEY_ALIASES.get(raw, raw)
|
|
12390
|
+
|
|
12391
|
+
|
|
12392
|
+
def _normalize_chart_partial_set(raw_set: Any, *, reason_path: str) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
12393
|
+
if not isinstance(raw_set, dict):
|
|
12394
|
+
return {}, [{"error_code": "INVALID_CHART_PATCH_SET", "reason_path": reason_path, "message": "patch_charts[].set must be an object"}]
|
|
12395
|
+
normalized: dict[str, Any] = {}
|
|
12396
|
+
issues: list[dict[str, Any]] = []
|
|
12397
|
+
for raw_key, value in raw_set.items():
|
|
12398
|
+
key_path = [part for part in str(raw_key or "").strip().split(".") if part]
|
|
12399
|
+
if not key_path:
|
|
12400
|
+
continue
|
|
12401
|
+
root = _canonical_chart_partial_patch_key(key_path[0])
|
|
12402
|
+
if root not in _CHART_PARTIAL_SET_KEYS:
|
|
12403
|
+
issues.append(
|
|
12404
|
+
{
|
|
12405
|
+
"error_code": "UNSUPPORTED_CHART_PATCH_FIELD",
|
|
12406
|
+
"reason_path": f"{reason_path}.{raw_key}",
|
|
12407
|
+
"field": raw_key,
|
|
12408
|
+
"allowed_fields": sorted(_CHART_PARTIAL_SET_KEYS),
|
|
12409
|
+
}
|
|
12410
|
+
)
|
|
12411
|
+
continue
|
|
12412
|
+
if len(key_path) == 1:
|
|
12413
|
+
normalized[root] = value
|
|
12414
|
+
continue
|
|
12415
|
+
target = normalized.setdefault(root, {})
|
|
12416
|
+
if not isinstance(target, dict):
|
|
12417
|
+
issues.append({"error_code": "INVALID_CHART_PATCH_PATH", "reason_path": f"{reason_path}.{raw_key}"})
|
|
12418
|
+
continue
|
|
12419
|
+
cursor = target
|
|
12420
|
+
for part in key_path[1:-1]:
|
|
12421
|
+
next_value = cursor.setdefault(part, {})
|
|
12422
|
+
if not isinstance(next_value, dict):
|
|
12423
|
+
issues.append({"error_code": "INVALID_CHART_PATCH_PATH", "reason_path": f"{reason_path}.{raw_key}"})
|
|
12424
|
+
break
|
|
12425
|
+
cursor = next_value
|
|
12426
|
+
else:
|
|
12427
|
+
cursor[key_path[-1]] = value
|
|
12428
|
+
return normalized, issues
|
|
12429
|
+
|
|
12430
|
+
|
|
12431
|
+
def _normalize_chart_partial_unset(raw_unset: Any, *, reason_path: str) -> tuple[set[str], list[dict[str, Any]]]:
|
|
12432
|
+
if not isinstance(raw_unset, list):
|
|
12433
|
+
return set(), [{"error_code": "INVALID_CHART_PATCH_UNSET", "reason_path": reason_path, "message": "patch_charts[].unset must be a list"}]
|
|
12434
|
+
normalized: set[str] = set()
|
|
12435
|
+
issues: list[dict[str, Any]] = []
|
|
12436
|
+
for index, raw_key in enumerate(raw_unset):
|
|
12437
|
+
root = _canonical_chart_partial_patch_key(str(raw_key or "").strip().split(".")[0])
|
|
12438
|
+
if root not in _CHART_PARTIAL_UNSET_KEYS:
|
|
12439
|
+
issues.append(
|
|
12440
|
+
{
|
|
12441
|
+
"error_code": "UNSUPPORTED_CHART_PATCH_UNSET_FIELD",
|
|
12442
|
+
"reason_path": f"{reason_path}[{index}]",
|
|
12443
|
+
"field": raw_key,
|
|
12444
|
+
"allowed_fields": sorted(_CHART_PARTIAL_UNSET_KEYS),
|
|
12445
|
+
}
|
|
12446
|
+
)
|
|
12447
|
+
continue
|
|
12448
|
+
normalized.add(root)
|
|
12449
|
+
return normalized, issues
|
|
12450
|
+
|
|
12451
|
+
|
|
12452
|
+
def _chart_upsert_payload_from_existing(
|
|
12453
|
+
*,
|
|
12454
|
+
chart_id: str,
|
|
12455
|
+
base: dict[str, Any],
|
|
12456
|
+
config: dict[str, Any],
|
|
12457
|
+
) -> dict[str, Any]:
|
|
12458
|
+
return _compact_dict(
|
|
12459
|
+
{
|
|
12460
|
+
"chart_id": chart_id,
|
|
12461
|
+
"name": str(base.get("chartName") or config.get("chartName") or chart_id).strip() or chart_id,
|
|
12462
|
+
"chart_type": _public_chart_type_from_backend(base.get("chartType") or config.get("chartType")),
|
|
12463
|
+
"config": deepcopy(config),
|
|
12464
|
+
}
|
|
12465
|
+
)
|
|
12466
|
+
|
|
12467
|
+
|
|
10959
12468
|
def _qingbi_field_type_from_public_field(field_type: str | None) -> str:
|
|
10960
12469
|
return {
|
|
10961
12470
|
"single_select": "singleSelect",
|
|
@@ -11117,6 +12626,13 @@ def _build_public_chart_config_payload(
|
|
|
11117
12626
|
qingbi_fields_by_id: dict[str, dict[str, Any]],
|
|
11118
12627
|
) -> dict[str, Any]:
|
|
11119
12628
|
config = deepcopy(patch.config)
|
|
12629
|
+
explicit_fields = set(getattr(patch, "model_fields_set", set()) or set())
|
|
12630
|
+
if "dimension_field_ids" in explicit_fields:
|
|
12631
|
+
config.pop("selectedDimensions", None)
|
|
12632
|
+
if "indicator_field_ids" in explicit_fields:
|
|
12633
|
+
config.pop("selectedMetrics", None)
|
|
12634
|
+
if "filters" in explicit_fields:
|
|
12635
|
+
config.pop("beforeAggregationFilterMatrix", None)
|
|
11120
12636
|
aggregate = str(config.pop("aggregate", "count") or "count").lower()
|
|
11121
12637
|
before_filters = deepcopy(config.pop("beforeAggregationFilterMatrix", []))
|
|
11122
12638
|
after_filters = deepcopy(config.pop("afterAggregationFilterMatrix", []))
|
|
@@ -17350,7 +18866,9 @@ def _normalize_view_button_type(value: Any) -> str | None:
|
|
|
17350
18866
|
|
|
17351
18867
|
def _normalize_view_button_config_type(value: Any) -> str | None:
|
|
17352
18868
|
normalized = str(value or "").strip().upper()
|
|
17353
|
-
if normalized
|
|
18869
|
+
if normalized == "LIST":
|
|
18870
|
+
return "INSIDE"
|
|
18871
|
+
if normalized in {"TOP", "DETAIL", "INSIDE"}:
|
|
17354
18872
|
return normalized
|
|
17355
18873
|
return None
|
|
17356
18874
|
|
|
@@ -17436,7 +18954,7 @@ def _extract_view_button_entries(config: dict[str, Any]) -> tuple[list[dict[str,
|
|
|
17436
18954
|
if not isinstance(item, dict):
|
|
17437
18955
|
continue
|
|
17438
18956
|
entry = deepcopy(item)
|
|
17439
|
-
entry.setdefault("configType", "
|
|
18957
|
+
entry.setdefault("configType", "INSIDE")
|
|
17440
18958
|
entries.append(entry)
|
|
17441
18959
|
return entries, "buttonConfig"
|
|
17442
18960
|
|
|
@@ -17471,13 +18989,47 @@ def _normalize_view_button_entry(entry: dict[str, Any]) -> dict[str, Any]:
|
|
|
17471
18989
|
normalized[public_key] = deepcopy(value)
|
|
17472
18990
|
trigger_add_data_config = entry.get("triggerAddDataConfig")
|
|
17473
18991
|
if isinstance(trigger_add_data_config, dict):
|
|
17474
|
-
normalized["trigger_add_data_config"] =
|
|
18992
|
+
normalized["trigger_add_data_config"] = _normalize_custom_button_add_data_config_for_public(trigger_add_data_config)
|
|
17475
18993
|
trigger_wings_config = entry.get("triggerWingsConfig")
|
|
17476
18994
|
if isinstance(trigger_wings_config, dict):
|
|
17477
18995
|
normalized["trigger_wings_config"] = deepcopy(trigger_wings_config)
|
|
17478
18996
|
return normalized
|
|
17479
18997
|
|
|
17480
18998
|
|
|
18999
|
+
def _public_view_button_placement(config_type: str | None) -> str | None:
|
|
19000
|
+
normalized = _normalize_view_button_config_type(config_type)
|
|
19001
|
+
if normalized == "TOP":
|
|
19002
|
+
return "header"
|
|
19003
|
+
if normalized == "DETAIL":
|
|
19004
|
+
return "detail"
|
|
19005
|
+
if normalized == "INSIDE":
|
|
19006
|
+
return "list"
|
|
19007
|
+
return None
|
|
19008
|
+
|
|
19009
|
+
|
|
19010
|
+
def _extract_view_buttons_config(config: dict[str, Any]) -> dict[str, Any]:
|
|
19011
|
+
entries, source = _extract_view_button_entries(config)
|
|
19012
|
+
placements: dict[str, list[dict[str, Any]]] = {"header": [], "detail": [], "list": []}
|
|
19013
|
+
items: list[dict[str, Any]] = []
|
|
19014
|
+
for entry in entries:
|
|
19015
|
+
normalized = _normalize_view_button_entry(entry)
|
|
19016
|
+
placement = _public_view_button_placement(normalized.get("config_type"))
|
|
19017
|
+
normalized["placement"] = placement
|
|
19018
|
+
items.append(normalized)
|
|
19019
|
+
if placement:
|
|
19020
|
+
placements.setdefault(placement, []).append(normalized)
|
|
19021
|
+
status = "ok" if items else "none"
|
|
19022
|
+
result: dict[str, Any] = {
|
|
19023
|
+
"status": status,
|
|
19024
|
+
"button_count": len(items),
|
|
19025
|
+
"placements": placements,
|
|
19026
|
+
"items": items,
|
|
19027
|
+
}
|
|
19028
|
+
if source:
|
|
19029
|
+
result["read_source"] = source
|
|
19030
|
+
return result
|
|
19031
|
+
|
|
19032
|
+
|
|
17481
19033
|
def _normalize_view_buttons_for_compare(value: Any) -> list[dict[str, Any]]:
|
|
17482
19034
|
if isinstance(value, dict):
|
|
17483
19035
|
entries, _ = _extract_view_button_entries(value)
|
|
@@ -17612,7 +19164,7 @@ def _canonicalize_view_button_summary_order(items: list[dict[str, Any]]) -> list
|
|
|
17612
19164
|
detail_main_buttons.append(item)
|
|
17613
19165
|
elif config_type == "DETAIL":
|
|
17614
19166
|
detail_more_buttons.append(item)
|
|
17615
|
-
elif config_type == "
|
|
19167
|
+
elif config_type == "INSIDE":
|
|
17616
19168
|
list_buttons.append(item)
|
|
17617
19169
|
else:
|
|
17618
19170
|
other_buttons.append(item)
|
|
@@ -17765,7 +19317,7 @@ def _build_grouped_view_button_config(button_config_dtos: list[dict[str, Any]])
|
|
|
17765
19317
|
being_main = bool(item.get("beingMain", False))
|
|
17766
19318
|
if config_type == "TOP":
|
|
17767
19319
|
grouped["topButtonList"].append(item)
|
|
17768
|
-
elif config_type == "
|
|
19320
|
+
elif config_type == "INSIDE":
|
|
17769
19321
|
grouped["listButtonList"].append(item)
|
|
17770
19322
|
elif being_main:
|
|
17771
19323
|
grouped["mainButtonDetailList"].append(item)
|
|
@@ -18663,7 +20215,7 @@ def _associated_resources_config_matches(expected: dict[str, Any], actual: dict[
|
|
|
18663
20215
|
return True
|
|
18664
20216
|
|
|
18665
20217
|
|
|
18666
|
-
def _normalize_associated_resources_payload(payload: Any) -> list[dict[str, Any]]:
|
|
20218
|
+
def _normalize_associated_resources_payload(payload: Any, *, include_raw: bool = False) -> list[dict[str, Any]]:
|
|
18667
20219
|
if isinstance(payload, dict):
|
|
18668
20220
|
raw_items = (
|
|
18669
20221
|
payload.get("associated_resources")
|
|
@@ -18684,7 +20236,7 @@ def _normalize_associated_resources_payload(payload: Any) -> list[dict[str, Any]
|
|
|
18684
20236
|
normalized_items: list[dict[str, Any]] = []
|
|
18685
20237
|
seen_ids: set[int] = set()
|
|
18686
20238
|
for raw_item in raw_items:
|
|
18687
|
-
item = _normalize_associated_resource_item(raw_item)
|
|
20239
|
+
item = _normalize_associated_resource_item(raw_item, include_raw=include_raw)
|
|
18688
20240
|
item_id = _coerce_positive_int(item.get("associated_item_id"))
|
|
18689
20241
|
if item_id is None or item_id in seen_ids:
|
|
18690
20242
|
continue
|
|
@@ -18694,7 +20246,7 @@ def _normalize_associated_resources_payload(payload: Any) -> list[dict[str, Any]
|
|
|
18694
20246
|
return normalized_items
|
|
18695
20247
|
|
|
18696
20248
|
|
|
18697
|
-
def _normalize_associated_resource_item(raw_item: Any) -> dict[str, Any]:
|
|
20249
|
+
def _normalize_associated_resource_item(raw_item: Any, *, include_raw: bool = False) -> dict[str, Any]:
|
|
18698
20250
|
if not isinstance(raw_item, dict):
|
|
18699
20251
|
return {}
|
|
18700
20252
|
item_id = _first_present(raw_item, "associated_item_id", "associatedItemId", "asosChartId", "id")
|
|
@@ -18725,7 +20277,10 @@ def _normalize_associated_resource_item(raw_item: Any) -> dict[str, Any]:
|
|
|
18725
20277
|
if chart_type is not None:
|
|
18726
20278
|
item["chart_type"] = _public_chart_type_from_backend(chart_type)
|
|
18727
20279
|
item["report_source"] = _public_report_source_from_backend_source(source_type)
|
|
18728
|
-
|
|
20280
|
+
item = _compact_dict(item)
|
|
20281
|
+
if include_raw:
|
|
20282
|
+
item["_raw"] = deepcopy(raw_item)
|
|
20283
|
+
return item
|
|
18729
20284
|
|
|
18730
20285
|
|
|
18731
20286
|
def _normalize_associated_graph_type(raw_item: dict[str, Any], *, chart_key: Any, view_key: Any) -> str:
|
|
@@ -18752,6 +20307,14 @@ def _associated_resource_index(resources: list[dict[str, Any]]) -> dict[int, dic
|
|
|
18752
20307
|
return indexed
|
|
18753
20308
|
|
|
18754
20309
|
|
|
20310
|
+
def _strip_internal_associated_resource_raw(resources: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
20311
|
+
return [
|
|
20312
|
+
_compact_dict({key: deepcopy(value) for key, value in item.items() if key != "_raw"})
|
|
20313
|
+
for item in resources
|
|
20314
|
+
if isinstance(item, dict)
|
|
20315
|
+
]
|
|
20316
|
+
|
|
20317
|
+
|
|
18755
20318
|
def _public_associated_graph_type(value: Any) -> str:
|
|
18756
20319
|
normalized = str(value or "").strip().lower()
|
|
18757
20320
|
if normalized in {"view", "viewgraph"}:
|
|
@@ -18887,6 +20450,117 @@ def _associated_resource_matches_patch(item: dict[str, Any], patch: AssociatedRe
|
|
|
18887
20450
|
return _associated_resource_public_key(item) == _associated_resource_public_key(patch)
|
|
18888
20451
|
|
|
18889
20452
|
|
|
20453
|
+
_ASSOCIATED_RESOURCE_PARTIAL_PATCH_KEY_ALIASES = {
|
|
20454
|
+
"graph_type": "graph_type",
|
|
20455
|
+
"graphType": "graph_type",
|
|
20456
|
+
"resource_type": "graph_type",
|
|
20457
|
+
"resourceType": "graph_type",
|
|
20458
|
+
"target_app_key": "target_app_key",
|
|
20459
|
+
"targetAppKey": "target_app_key",
|
|
20460
|
+
"app_key": "target_app_key",
|
|
20461
|
+
"appKey": "target_app_key",
|
|
20462
|
+
"view_key": "view_key",
|
|
20463
|
+
"viewKey": "view_key",
|
|
20464
|
+
"viewgraphKey": "view_key",
|
|
20465
|
+
"viewGraphKey": "view_key",
|
|
20466
|
+
"chart_key": "chart_key",
|
|
20467
|
+
"chartKey": "chart_key",
|
|
20468
|
+
"report_source": "report_source",
|
|
20469
|
+
"reportSource": "report_source",
|
|
20470
|
+
"match_rules": "match_rules",
|
|
20471
|
+
"matchRules": "match_rules",
|
|
20472
|
+
"match_mappings": "match_mappings",
|
|
20473
|
+
"matchMappings": "match_mappings",
|
|
20474
|
+
}
|
|
20475
|
+
|
|
20476
|
+
_ASSOCIATED_RESOURCE_PARTIAL_SET_KEYS = {
|
|
20477
|
+
"graph_type",
|
|
20478
|
+
"target_app_key",
|
|
20479
|
+
"view_key",
|
|
20480
|
+
"chart_key",
|
|
20481
|
+
"report_source",
|
|
20482
|
+
"match_rules",
|
|
20483
|
+
"match_mappings",
|
|
20484
|
+
}
|
|
20485
|
+
|
|
20486
|
+
_ASSOCIATED_RESOURCE_PARTIAL_UNSET_KEYS = {"match_rules", "match_mappings"}
|
|
20487
|
+
|
|
20488
|
+
|
|
20489
|
+
def _canonical_associated_resource_partial_patch_key(key: Any) -> str:
|
|
20490
|
+
raw = str(key or "").strip()
|
|
20491
|
+
return _ASSOCIATED_RESOURCE_PARTIAL_PATCH_KEY_ALIASES.get(raw, raw)
|
|
20492
|
+
|
|
20493
|
+
|
|
20494
|
+
def _normalize_associated_resource_partial_set(raw_set: Any, *, reason_path: str) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
20495
|
+
if not isinstance(raw_set, dict):
|
|
20496
|
+
return {}, [{"error_code": "INVALID_ASSOCIATED_RESOURCE_PATCH_SET", "reason_path": reason_path, "message": "patch_resources[].set must be an object"}]
|
|
20497
|
+
normalized: dict[str, Any] = {}
|
|
20498
|
+
issues: list[dict[str, Any]] = []
|
|
20499
|
+
for raw_key, value in raw_set.items():
|
|
20500
|
+
root = _canonical_associated_resource_partial_patch_key(raw_key)
|
|
20501
|
+
if root not in _ASSOCIATED_RESOURCE_PARTIAL_SET_KEYS:
|
|
20502
|
+
issues.append(
|
|
20503
|
+
{
|
|
20504
|
+
"error_code": "UNSUPPORTED_ASSOCIATED_RESOURCE_PATCH_FIELD",
|
|
20505
|
+
"reason_path": f"{reason_path}.{raw_key}",
|
|
20506
|
+
"field": raw_key,
|
|
20507
|
+
"allowed_fields": sorted(_ASSOCIATED_RESOURCE_PARTIAL_SET_KEYS),
|
|
20508
|
+
}
|
|
20509
|
+
)
|
|
20510
|
+
continue
|
|
20511
|
+
normalized[root] = value
|
|
20512
|
+
return normalized, issues
|
|
20513
|
+
|
|
20514
|
+
|
|
20515
|
+
def _normalize_associated_resource_partial_unset(raw_unset: Any, *, reason_path: str) -> tuple[set[str], list[dict[str, Any]]]:
|
|
20516
|
+
if not isinstance(raw_unset, list):
|
|
20517
|
+
return set(), [{"error_code": "INVALID_ASSOCIATED_RESOURCE_PATCH_UNSET", "reason_path": reason_path, "message": "patch_resources[].unset must be a list"}]
|
|
20518
|
+
normalized: set[str] = set()
|
|
20519
|
+
issues: list[dict[str, Any]] = []
|
|
20520
|
+
for index, raw_key in enumerate(raw_unset):
|
|
20521
|
+
root = _canonical_associated_resource_partial_patch_key(raw_key)
|
|
20522
|
+
if root not in _ASSOCIATED_RESOURCE_PARTIAL_UNSET_KEYS:
|
|
20523
|
+
issues.append(
|
|
20524
|
+
{
|
|
20525
|
+
"error_code": "UNSUPPORTED_ASSOCIATED_RESOURCE_PATCH_UNSET_FIELD",
|
|
20526
|
+
"reason_path": f"{reason_path}[{index}]",
|
|
20527
|
+
"field": raw_key,
|
|
20528
|
+
"allowed_fields": sorted(_ASSOCIATED_RESOURCE_PARTIAL_UNSET_KEYS),
|
|
20529
|
+
}
|
|
20530
|
+
)
|
|
20531
|
+
continue
|
|
20532
|
+
normalized.add(root)
|
|
20533
|
+
return normalized, issues
|
|
20534
|
+
|
|
20535
|
+
|
|
20536
|
+
def _associated_resource_upsert_payload_from_existing_item(
|
|
20537
|
+
item: dict[str, Any],
|
|
20538
|
+
*,
|
|
20539
|
+
associated_item_id: int,
|
|
20540
|
+
client_key: str | None = None,
|
|
20541
|
+
) -> dict[str, Any]:
|
|
20542
|
+
graph_type = str(item.get("graph_type") or item.get("resource_type") or "").strip().lower()
|
|
20543
|
+
payload = {
|
|
20544
|
+
"associated_item_id": associated_item_id,
|
|
20545
|
+
"client_key": client_key,
|
|
20546
|
+
"graph_type": "view" if graph_type == "view" else "chart",
|
|
20547
|
+
"target_app_key": item.get("target_app_key"),
|
|
20548
|
+
"view_key": item.get("view_key"),
|
|
20549
|
+
"chart_key": item.get("chart_key"),
|
|
20550
|
+
"report_source": item.get("report_source"),
|
|
20551
|
+
"match_rules": [],
|
|
20552
|
+
}
|
|
20553
|
+
raw = item.get("_raw") if isinstance(item.get("_raw"), dict) else {}
|
|
20554
|
+
raw_match_rules = raw.get("matchRules") if isinstance(raw, dict) else None
|
|
20555
|
+
if isinstance(raw_match_rules, list):
|
|
20556
|
+
if raw_match_rules and all(isinstance(group, list) for group in raw_match_rules):
|
|
20557
|
+
flattened = [rule for group in raw_match_rules for rule in group if isinstance(rule, dict)]
|
|
20558
|
+
payload["match_rules"] = flattened
|
|
20559
|
+
else:
|
|
20560
|
+
payload["match_rules"] = [rule for rule in raw_match_rules if isinstance(rule, dict)]
|
|
20561
|
+
return _compact_dict(payload)
|
|
20562
|
+
|
|
20563
|
+
|
|
18890
20564
|
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
20565
|
return {
|
|
18892
20566
|
"error_code": "ASSOCIATED_RESOURCE_NOT_FOUND",
|
|
@@ -18907,14 +20581,33 @@ def _duplicate_associated_resource_issue(reason_path: str, associated_item_id: i
|
|
|
18907
20581
|
}
|
|
18908
20582
|
|
|
18909
20583
|
|
|
18910
|
-
def
|
|
20584
|
+
def _associated_resource_patch_has_match_config(patch: AssociatedResourceUpsertPatch) -> bool:
|
|
20585
|
+
fields_set = getattr(patch, "model_fields_set", set())
|
|
20586
|
+
return bool(patch.match_mappings) or bool(patch.match_rules) or "match_mappings" in fields_set
|
|
20587
|
+
|
|
20588
|
+
|
|
20589
|
+
def _serialize_associated_resource_match_rules(match_rules: list[Any]) -> list[list[dict[str, Any]]]:
|
|
18911
20590
|
if not match_rules:
|
|
18912
20591
|
return []
|
|
18913
|
-
|
|
20592
|
+
serialized: list[dict[str, Any]] = []
|
|
20593
|
+
for rule in match_rules:
|
|
20594
|
+
if isinstance(rule, CustomButtonMatchRulePatch):
|
|
20595
|
+
payload = rule.model_dump(mode="json", exclude_none=True)
|
|
20596
|
+
elif isinstance(rule, dict):
|
|
20597
|
+
payload = rule
|
|
20598
|
+
else:
|
|
20599
|
+
continue
|
|
20600
|
+
serialized.append(_serialize_custom_button_match_rule(payload))
|
|
20601
|
+
return [serialized] if serialized else []
|
|
18914
20602
|
|
|
18915
20603
|
|
|
18916
|
-
def _serialize_associated_resource_create_payload(
|
|
20604
|
+
def _serialize_associated_resource_create_payload(
|
|
20605
|
+
patch: AssociatedResourceUpsertPatch,
|
|
20606
|
+
*,
|
|
20607
|
+
match_rules_override: list[dict[str, Any]] | None = None,
|
|
20608
|
+
) -> dict[str, Any]:
|
|
18917
20609
|
graph_type = _public_associated_graph_type(patch.graph_type)
|
|
20610
|
+
match_rules = match_rules_override if match_rules_override is not None else patch.match_rules
|
|
18918
20611
|
return {
|
|
18919
20612
|
"appKey": patch.target_app_key,
|
|
18920
20613
|
"chartList": [
|
|
@@ -18924,22 +20617,32 @@ def _serialize_associated_resource_create_payload(patch: AssociatedResourceUpser
|
|
|
18924
20617
|
"graphType": _backend_associated_graph_type(graph_type),
|
|
18925
20618
|
}
|
|
18926
20619
|
],
|
|
18927
|
-
"matchRules": _serialize_associated_resource_match_rules(
|
|
20620
|
+
"matchRules": _serialize_associated_resource_match_rules(match_rules),
|
|
18928
20621
|
}
|
|
18929
20622
|
|
|
18930
20623
|
|
|
18931
|
-
def _serialize_associated_resource_update_payload(
|
|
20624
|
+
def _serialize_associated_resource_update_payload(
|
|
20625
|
+
patch: AssociatedResourceUpsertPatch,
|
|
20626
|
+
*,
|
|
20627
|
+
associated_item_id: int,
|
|
20628
|
+
existing_item: dict[str, Any] | None = None,
|
|
20629
|
+
match_rules_override: list[dict[str, Any]] | None = None,
|
|
20630
|
+
) -> dict[str, Any]:
|
|
18932
20631
|
graph_type = _public_associated_graph_type(patch.graph_type)
|
|
18933
|
-
|
|
20632
|
+
raw = existing_item.get("_raw") if isinstance(existing_item, dict) and isinstance(existing_item.get("_raw"), dict) else None
|
|
20633
|
+
payload = deepcopy(raw) if isinstance(raw, dict) else {}
|
|
20634
|
+
match_rules = match_rules_override if match_rules_override is not None else patch.match_rules
|
|
20635
|
+
payload.update(
|
|
18934
20636
|
{
|
|
18935
20637
|
"id": associated_item_id,
|
|
18936
20638
|
"appKey": patch.target_app_key,
|
|
18937
20639
|
"chartKey": patch.view_key if graph_type == "view" else patch.chart_key,
|
|
18938
20640
|
"sourceType": _backend_source_type_for_associated_resource_patch(patch),
|
|
18939
20641
|
"graphType": _backend_associated_graph_type(graph_type),
|
|
18940
|
-
"matchRules": _serialize_associated_resource_match_rules(
|
|
20642
|
+
"matchRules": _serialize_associated_resource_match_rules(match_rules),
|
|
18941
20643
|
}
|
|
18942
20644
|
)
|
|
20645
|
+
return _compact_dict(payload)
|
|
18943
20646
|
|
|
18944
20647
|
|
|
18945
20648
|
def _associated_resource_result_entry(
|
|
@@ -19014,6 +20717,204 @@ def _build_view_buttons_only_update_payload(config: Any, *, button_config_dtos:
|
|
|
19014
20717
|
return payload
|
|
19015
20718
|
|
|
19016
20719
|
|
|
20720
|
+
_VIEW_PARTIAL_PATCH_KEY_ALIASES = {
|
|
20721
|
+
"view_name": "name",
|
|
20722
|
+
"viewName": "name",
|
|
20723
|
+
"view_key": "view_key",
|
|
20724
|
+
"viewKey": "view_key",
|
|
20725
|
+
"viewgraphKey": "view_key",
|
|
20726
|
+
"type": "type",
|
|
20727
|
+
"view_type": "type",
|
|
20728
|
+
"viewType": "type",
|
|
20729
|
+
"columns": "columns",
|
|
20730
|
+
"column_names": "columns",
|
|
20731
|
+
"columnNames": "columns",
|
|
20732
|
+
"fields": "columns",
|
|
20733
|
+
"group_by": "group_by",
|
|
20734
|
+
"groupBy": "group_by",
|
|
20735
|
+
"filters": "filters",
|
|
20736
|
+
"filter_rules": "filters",
|
|
20737
|
+
"filterRules": "filters",
|
|
20738
|
+
"start_field": "start_field",
|
|
20739
|
+
"startField": "start_field",
|
|
20740
|
+
"end_field": "end_field",
|
|
20741
|
+
"endField": "end_field",
|
|
20742
|
+
"title_field": "title_field",
|
|
20743
|
+
"titleField": "title_field",
|
|
20744
|
+
"buttons": "buttons",
|
|
20745
|
+
"visibility": "visibility",
|
|
20746
|
+
"query_conditions": "query_conditions",
|
|
20747
|
+
"queryConditions": "query_conditions",
|
|
20748
|
+
"query_condition": "query_conditions",
|
|
20749
|
+
"queryCondition": "query_conditions",
|
|
20750
|
+
"associated_resources": "associated_resources",
|
|
20751
|
+
"associatedResources": "associated_resources",
|
|
20752
|
+
"associated_reports": "associated_resources",
|
|
20753
|
+
"associatedReports": "associated_resources",
|
|
20754
|
+
}
|
|
20755
|
+
|
|
20756
|
+
|
|
20757
|
+
def _canonical_view_partial_patch_key(key: Any) -> str:
|
|
20758
|
+
raw = str(key or "").strip()
|
|
20759
|
+
return _VIEW_PARTIAL_PATCH_KEY_ALIASES.get(raw, raw)
|
|
20760
|
+
|
|
20761
|
+
|
|
20762
|
+
def _normalize_view_partial_set(raw_set: Any, *, reason_path: str) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
20763
|
+
if not isinstance(raw_set, dict):
|
|
20764
|
+
return {}, [{"error_code": "INVALID_VIEW_PATCH_SET", "reason_path": reason_path, "message": "patch_views[].set must be an object"}]
|
|
20765
|
+
normalized: dict[str, Any] = {}
|
|
20766
|
+
issues: list[dict[str, Any]] = []
|
|
20767
|
+
for raw_key, value in raw_set.items():
|
|
20768
|
+
key_path = [part for part in str(raw_key or "").strip().split(".") if part]
|
|
20769
|
+
if not key_path:
|
|
20770
|
+
continue
|
|
20771
|
+
root = _canonical_view_partial_patch_key(key_path[0])
|
|
20772
|
+
if root not in _VIEW_PARTIAL_SET_KEYS:
|
|
20773
|
+
issues.append(
|
|
20774
|
+
{
|
|
20775
|
+
"error_code": "UNSUPPORTED_VIEW_PATCH_FIELD",
|
|
20776
|
+
"reason_path": f"{reason_path}.{raw_key}",
|
|
20777
|
+
"field": raw_key,
|
|
20778
|
+
"allowed_fields": sorted(_VIEW_PARTIAL_SET_KEYS),
|
|
20779
|
+
}
|
|
20780
|
+
)
|
|
20781
|
+
continue
|
|
20782
|
+
if len(key_path) == 1:
|
|
20783
|
+
normalized[root] = value
|
|
20784
|
+
continue
|
|
20785
|
+
target = normalized.setdefault(root, {})
|
|
20786
|
+
if not isinstance(target, dict):
|
|
20787
|
+
issues.append(
|
|
20788
|
+
{
|
|
20789
|
+
"error_code": "INVALID_VIEW_PATCH_PATH",
|
|
20790
|
+
"reason_path": f"{reason_path}.{raw_key}",
|
|
20791
|
+
"message": "cannot combine a scalar replacement and nested patch for the same field",
|
|
20792
|
+
}
|
|
20793
|
+
)
|
|
20794
|
+
continue
|
|
20795
|
+
cursor = target
|
|
20796
|
+
for part in key_path[1:-1]:
|
|
20797
|
+
next_value = cursor.setdefault(part, {})
|
|
20798
|
+
if not isinstance(next_value, dict):
|
|
20799
|
+
issues.append({"error_code": "INVALID_VIEW_PATCH_PATH", "reason_path": f"{reason_path}.{raw_key}"})
|
|
20800
|
+
break
|
|
20801
|
+
cursor = next_value
|
|
20802
|
+
else:
|
|
20803
|
+
cursor[key_path[-1]] = value
|
|
20804
|
+
return normalized, issues
|
|
20805
|
+
|
|
20806
|
+
|
|
20807
|
+
def _normalize_view_partial_unset(raw_unset: Any, *, reason_path: str) -> tuple[set[str], list[dict[str, Any]]]:
|
|
20808
|
+
if not isinstance(raw_unset, list):
|
|
20809
|
+
return set(), [{"error_code": "INVALID_VIEW_PATCH_UNSET", "reason_path": reason_path, "message": "patch_views[].unset must be a list"}]
|
|
20810
|
+
normalized: set[str] = set()
|
|
20811
|
+
issues: list[dict[str, Any]] = []
|
|
20812
|
+
for index, raw_key in enumerate(raw_unset):
|
|
20813
|
+
root = _canonical_view_partial_patch_key(str(raw_key or "").strip().split(".")[0])
|
|
20814
|
+
if root not in _VIEW_PARTIAL_UNSET_KEYS:
|
|
20815
|
+
issues.append(
|
|
20816
|
+
{
|
|
20817
|
+
"error_code": "UNSUPPORTED_VIEW_PATCH_UNSET_FIELD",
|
|
20818
|
+
"reason_path": f"{reason_path}[{index}]",
|
|
20819
|
+
"field": raw_key,
|
|
20820
|
+
"allowed_fields": sorted(_VIEW_PARTIAL_UNSET_KEYS),
|
|
20821
|
+
}
|
|
20822
|
+
)
|
|
20823
|
+
continue
|
|
20824
|
+
normalized.add(root)
|
|
20825
|
+
return normalized, issues
|
|
20826
|
+
|
|
20827
|
+
|
|
20828
|
+
_VIEW_PARTIAL_SET_KEYS = {
|
|
20829
|
+
"name",
|
|
20830
|
+
"type",
|
|
20831
|
+
"columns",
|
|
20832
|
+
"group_by",
|
|
20833
|
+
"filters",
|
|
20834
|
+
"start_field",
|
|
20835
|
+
"end_field",
|
|
20836
|
+
"title_field",
|
|
20837
|
+
"buttons",
|
|
20838
|
+
"visibility",
|
|
20839
|
+
"query_conditions",
|
|
20840
|
+
"associated_resources",
|
|
20841
|
+
}
|
|
20842
|
+
|
|
20843
|
+
_VIEW_PARTIAL_UNSET_KEYS = {"filters", "buttons", "visibility", "query_conditions", "associated_resources"}
|
|
20844
|
+
|
|
20845
|
+
|
|
20846
|
+
def _deep_merge_public_config(target: dict[str, Any], patch: dict[str, Any]) -> None:
|
|
20847
|
+
for key, value in patch.items():
|
|
20848
|
+
if isinstance(value, dict) and isinstance(target.get(key), dict):
|
|
20849
|
+
_deep_merge_public_config(cast(dict[str, Any], target[key]), value)
|
|
20850
|
+
else:
|
|
20851
|
+
target[key] = deepcopy(value)
|
|
20852
|
+
|
|
20853
|
+
|
|
20854
|
+
def _field_name_by_id(field_names_by_id: dict[int, str], field_id: Any) -> str | None:
|
|
20855
|
+
normalized_id = _coerce_nonnegative_int(field_id)
|
|
20856
|
+
if normalized_id is None:
|
|
20857
|
+
return None
|
|
20858
|
+
name = str(field_names_by_id.get(normalized_id) or "").strip()
|
|
20859
|
+
return name or None
|
|
20860
|
+
|
|
20861
|
+
|
|
20862
|
+
def _view_upsert_payload_from_existing_view(
|
|
20863
|
+
*,
|
|
20864
|
+
config: dict[str, Any],
|
|
20865
|
+
summary: dict[str, Any],
|
|
20866
|
+
view_key: str,
|
|
20867
|
+
field_names_by_id: dict[int, str],
|
|
20868
|
+
) -> dict[str, Any]:
|
|
20869
|
+
view_type = _normalize_view_type_name(config.get("viewgraphType") or summary.get("type"))
|
|
20870
|
+
configured_columns = [
|
|
20871
|
+
name
|
|
20872
|
+
for name in (_field_name_by_id(field_names_by_id, item) for item in (config.get("viewgraphQueIds") or []))
|
|
20873
|
+
if name and name not in _KNOWN_SYSTEM_VIEW_COLUMNS
|
|
20874
|
+
]
|
|
20875
|
+
columns = [
|
|
20876
|
+
str(name or "").strip()
|
|
20877
|
+
for name in (summary.get("apply_columns") or summary.get("columns") or [])
|
|
20878
|
+
if str(name or "").strip() and str(name or "").strip() not in _KNOWN_SYSTEM_VIEW_COLUMNS
|
|
20879
|
+
]
|
|
20880
|
+
if configured_columns and (not columns or len(configured_columns) >= len(columns)):
|
|
20881
|
+
columns = configured_columns
|
|
20882
|
+
display_config = summary.get("display_config") if isinstance(summary.get("display_config"), dict) else {}
|
|
20883
|
+
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"))
|
|
20884
|
+
title_field = str(display_config.get("title_field") or "").strip() or _field_name_by_id(field_names_by_id, config.get("titleQue"))
|
|
20885
|
+
gantt_config = config.get("viewgraphGanttConfigVO") if isinstance(config.get("viewgraphGanttConfigVO"), dict) else {}
|
|
20886
|
+
start_field = _field_name_by_id(field_names_by_id, gantt_config.get("startTimeQueId")) if isinstance(gantt_config, dict) else None
|
|
20887
|
+
end_field = _field_name_by_id(field_names_by_id, gantt_config.get("endTimeQueId")) if isinstance(gantt_config, dict) else None
|
|
20888
|
+
gantt_title = _field_name_by_id(field_names_by_id, gantt_config.get("titleQueId")) if isinstance(gantt_config, dict) else None
|
|
20889
|
+
query_conditions = _extract_view_query_conditions_config(config)
|
|
20890
|
+
query_conditions.pop("row_field_ids", None)
|
|
20891
|
+
associated_resources = _extract_view_associated_resources_config(config)
|
|
20892
|
+
payload: dict[str, Any] = {
|
|
20893
|
+
"name": str(summary.get("name") or config.get("viewgraphName") or config.get("viewName") or view_key).strip(),
|
|
20894
|
+
"view_key": view_key,
|
|
20895
|
+
"type": view_type or "table",
|
|
20896
|
+
"columns": columns,
|
|
20897
|
+
"group_by": group_by,
|
|
20898
|
+
"filters": [],
|
|
20899
|
+
"query_conditions": query_conditions,
|
|
20900
|
+
"associated_resources": {
|
|
20901
|
+
"visible": bool(associated_resources.get("visible", False)),
|
|
20902
|
+
"limit_type": associated_resources.get("limit_type"),
|
|
20903
|
+
"associated_item_ids": list(associated_resources.get("configured_associated_item_ids") or []),
|
|
20904
|
+
},
|
|
20905
|
+
}
|
|
20906
|
+
if title_field:
|
|
20907
|
+
payload["title_field"] = title_field
|
|
20908
|
+
if view_type == "gantt":
|
|
20909
|
+
if start_field:
|
|
20910
|
+
payload["start_field"] = start_field
|
|
20911
|
+
if end_field:
|
|
20912
|
+
payload["end_field"] = end_field
|
|
20913
|
+
if gantt_title:
|
|
20914
|
+
payload["title_field"] = gantt_title
|
|
20915
|
+
return payload
|
|
20916
|
+
|
|
20917
|
+
|
|
19017
20918
|
def _first_present(mapping: dict[str, Any], *keys: str) -> Any:
|
|
19018
20919
|
for key in keys:
|
|
19019
20920
|
if key in mapping and mapping[key] is not None:
|
|
@@ -19028,7 +20929,7 @@ def _build_view_create_payload(
|
|
|
19028
20929
|
schema: dict[str, Any],
|
|
19029
20930
|
patch: ViewUpsertPatch,
|
|
19030
20931
|
ordinal: int,
|
|
19031
|
-
view_filters: list[list[dict[str, Any]]],
|
|
20932
|
+
view_filters: list[list[dict[str, Any]]] | None,
|
|
19032
20933
|
current_fields_by_name: dict[str, dict[str, Any]],
|
|
19033
20934
|
auth_override: dict[str, Any] | None = None,
|
|
19034
20935
|
explicit_button_dtos: list[dict[str, Any]] | None = None,
|
|
@@ -19226,7 +21127,7 @@ def _build_view_update_payload(
|
|
|
19226
21127
|
source_viewgraph_key: str,
|
|
19227
21128
|
schema: dict[str, Any],
|
|
19228
21129
|
patch: ViewUpsertPatch,
|
|
19229
|
-
view_filters: list[list[dict[str, Any]]],
|
|
21130
|
+
view_filters: list[list[dict[str, Any]]] | None,
|
|
19230
21131
|
current_fields_by_name: dict[str, dict[str, Any]],
|
|
19231
21132
|
auth_override: dict[str, Any] | None = None,
|
|
19232
21133
|
explicit_button_dtos: list[dict[str, Any]] | None = None,
|
|
@@ -19334,7 +21235,7 @@ def _build_minimal_view_payload(
|
|
|
19334
21235
|
schema: dict[str, Any],
|
|
19335
21236
|
patch: ViewUpsertPatch,
|
|
19336
21237
|
ordinal: int,
|
|
19337
|
-
view_filters: list[list[dict[str, Any]]],
|
|
21238
|
+
view_filters: list[list[dict[str, Any]]] | None,
|
|
19338
21239
|
current_fields_by_name: dict[str, dict[str, Any]],
|
|
19339
21240
|
auth_override: dict[str, Any] | None = None,
|
|
19340
21241
|
explicit_button_dtos: list[dict[str, Any]] | None = None,
|
|
@@ -19403,9 +21304,14 @@ def _hydrate_view_backend_payload(
|
|
|
19403
21304
|
data.setdefault("beingImageAdaption", False)
|
|
19404
21305
|
data.setdefault("clippingMode", "default")
|
|
19405
21306
|
data.setdefault("frontCoverQueId", None)
|
|
19406
|
-
|
|
19407
|
-
|
|
19408
|
-
|
|
21307
|
+
if view_filters is None:
|
|
21308
|
+
data.setdefault("viewgraphLimitType", 1)
|
|
21309
|
+
data.setdefault("viewgraphLimit", [])
|
|
21310
|
+
data.setdefault("viewgraphLimitFormula", "")
|
|
21311
|
+
else:
|
|
21312
|
+
data["viewgraphLimitType"] = 1
|
|
21313
|
+
data["viewgraphLimit"] = deepcopy(view_filters)
|
|
21314
|
+
data["viewgraphLimitFormula"] = ""
|
|
19409
21315
|
data.setdefault("sortType", "defaultSort")
|
|
19410
21316
|
if not data.get("viewgraphSorts"):
|
|
19411
21317
|
sort_que_id = visible_que_id_values[0] if visible_que_id_values else 1
|
|
@@ -19417,8 +21323,11 @@ def _hydrate_view_backend_payload(
|
|
|
19417
21323
|
data.setdefault("printTpls", [])
|
|
19418
21324
|
data.setdefault("beingCommentStatus", False)
|
|
19419
21325
|
data.setdefault("usages", [])
|
|
19420
|
-
data
|
|
19421
|
-
|
|
21326
|
+
data.setdefault("dataPermissionType", "CUSTOM")
|
|
21327
|
+
if view_filters is None:
|
|
21328
|
+
data.setdefault("dataScope", "ALL")
|
|
21329
|
+
else:
|
|
21330
|
+
data["dataScope"] = "CUSTOM" if view_filters else "ALL"
|
|
19422
21331
|
data.setdefault("needPass", False)
|
|
19423
21332
|
data.setdefault("beingWorkflowNodeFutureListVisible", True)
|
|
19424
21333
|
data.setdefault("asosChartConfig", {"limitType": 1, "asosChartIdList": []})
|
|
@@ -19713,6 +21622,44 @@ def _normalize_view_filter_groups_for_compare(groups: Any) -> list[list[dict[str
|
|
|
19713
21622
|
return normalized_groups
|
|
19714
21623
|
|
|
19715
21624
|
|
|
21625
|
+
def _view_filter_rule_values_for_signature(rule: dict[str, Any]) -> list[str]:
|
|
21626
|
+
values = [str(value) for value in (rule.get("judgeValues") or [])]
|
|
21627
|
+
if values:
|
|
21628
|
+
return values
|
|
21629
|
+
fallback_values: list[str] = []
|
|
21630
|
+
for detail in rule.get("judgeValueDetails") or []:
|
|
21631
|
+
if not isinstance(detail, dict):
|
|
21632
|
+
continue
|
|
21633
|
+
item_id = detail.get("id")
|
|
21634
|
+
item_value = detail.get("value")
|
|
21635
|
+
if item_id is not None:
|
|
21636
|
+
fallback_values.append(str(item_id))
|
|
21637
|
+
elif item_value is not None:
|
|
21638
|
+
fallback_values.append(str(item_value))
|
|
21639
|
+
return fallback_values
|
|
21640
|
+
|
|
21641
|
+
|
|
21642
|
+
def _view_filter_groups_signature(groups: Any) -> list[list[dict[str, Any]]]:
|
|
21643
|
+
signature: list[list[dict[str, Any]]] = []
|
|
21644
|
+
for group in _normalize_view_filter_groups_for_compare(groups):
|
|
21645
|
+
group_signature: list[dict[str, Any]] = []
|
|
21646
|
+
for rule in group:
|
|
21647
|
+
group_signature.append(
|
|
21648
|
+
{
|
|
21649
|
+
"queId": rule.get("queId"),
|
|
21650
|
+
"judgeType": rule.get("judgeType"),
|
|
21651
|
+
"judgeValues": _view_filter_rule_values_for_signature(rule),
|
|
21652
|
+
}
|
|
21653
|
+
)
|
|
21654
|
+
if group_signature:
|
|
21655
|
+
signature.append(group_signature)
|
|
21656
|
+
return signature
|
|
21657
|
+
|
|
21658
|
+
|
|
21659
|
+
def _view_filter_groups_equivalent(expected: Any, actual: Any) -> bool:
|
|
21660
|
+
return _view_filter_groups_signature(expected) == _view_filter_groups_signature(actual)
|
|
21661
|
+
|
|
21662
|
+
|
|
19716
21663
|
def _infer_status_field_id(fields: list[dict[str, Any]]) -> str | None:
|
|
19717
21664
|
preferred_names = {"status", "状态", "订单状态", "审批状态", "流程状态"}
|
|
19718
21665
|
for field in fields:
|