@josephyan/qingflow-cli 0.2.0-beta.63 → 0.2.0-beta.65
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/src/qingflow_mcp/builder_facade/models.py +5 -0
- package/src/qingflow_mcp/builder_facade/service.py +560 -26
- package/src/qingflow_mcp/cli/commands/builder.py +4 -0
- package/src/qingflow_mcp/server_app_builder.py +4 -0
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +71 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +36 -2
- package/src/qingflow_mcp/tools/custom_button_tools.py +2 -0
|
@@ -16,7 +16,7 @@ from ..list_type_labels import RECORD_LIST_TYPE_LABELS, SYSTEM_VIEW_ID_TO_LIST_T
|
|
|
16
16
|
from ..solution.build_assembly_store import BuildAssemblyStore, default_artifacts, default_manifest
|
|
17
17
|
from ..solution.compiler.chart_compiler import qingbi_workspace_visible_auth
|
|
18
18
|
from ..solution.compiler.form_compiler import build_question, default_form_payload, default_member_auth
|
|
19
|
-
from ..solution.compiler.icon_utils import
|
|
19
|
+
from ..solution.compiler.icon_utils import encode_workspace_icon_with_defaults
|
|
20
20
|
from ..solution.compiler.view_compiler import VIEW_TYPE_MAP
|
|
21
21
|
from ..solution.executor import _build_viewgraph_questions, _compact_dict, extract_field_map
|
|
22
22
|
from ..solution.spec_models import FieldType, FormLayoutRowSpec, FormLayoutSectionSpec, ViewSpec
|
|
@@ -1406,6 +1406,15 @@ class AiBuilderFacade:
|
|
|
1406
1406
|
def finalize(response: JSONObject) -> JSONObject:
|
|
1407
1407
|
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
1408
1408
|
|
|
1409
|
+
edit_version_no, edit_context_error = self._ensure_app_edit_context(
|
|
1410
|
+
profile=profile,
|
|
1411
|
+
app_key=app_key,
|
|
1412
|
+
normalized_args=normalized_args,
|
|
1413
|
+
failure_code="CUSTOM_BUTTON_CREATE_FAILED",
|
|
1414
|
+
)
|
|
1415
|
+
if edit_context_error is not None:
|
|
1416
|
+
return finalize(edit_context_error)
|
|
1417
|
+
|
|
1409
1418
|
create_result = self.buttons.custom_button_create(
|
|
1410
1419
|
profile=profile,
|
|
1411
1420
|
app_key=app_key,
|
|
@@ -1419,7 +1428,7 @@ class AiBuilderFacade:
|
|
|
1419
1428
|
"CUSTOM_BUTTON_CREATE_FAILED",
|
|
1420
1429
|
"custom button create succeeded but no button_id was returned",
|
|
1421
1430
|
normalized_args=normalized_args,
|
|
1422
|
-
details={"app_key": app_key, "result": deepcopy(raw_result)},
|
|
1431
|
+
details={"app_key": app_key, "result": deepcopy(raw_result), "edit_version_no": edit_version_no},
|
|
1423
1432
|
suggested_next_call={"tool_name": "app_custom_button_list", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1424
1433
|
)
|
|
1425
1434
|
)
|
|
@@ -1444,6 +1453,7 @@ class AiBuilderFacade:
|
|
|
1444
1453
|
"details": {
|
|
1445
1454
|
"app_key": app_key,
|
|
1446
1455
|
"button_id": button_id,
|
|
1456
|
+
"edit_version_no": edit_version_no,
|
|
1447
1457
|
"transport_error": _transport_error_payload(api_error),
|
|
1448
1458
|
},
|
|
1449
1459
|
"request_id": api_error.request_id,
|
|
@@ -1487,6 +1497,7 @@ class AiBuilderFacade:
|
|
|
1487
1497
|
"verified": True,
|
|
1488
1498
|
"app_key": app_key,
|
|
1489
1499
|
"button_id": button_id,
|
|
1500
|
+
"edit_version_no": edit_version_no,
|
|
1490
1501
|
"button": button,
|
|
1491
1502
|
}
|
|
1492
1503
|
)
|
|
@@ -1514,6 +1525,15 @@ class AiBuilderFacade:
|
|
|
1514
1525
|
def finalize(response: JSONObject) -> JSONObject:
|
|
1515
1526
|
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
1516
1527
|
|
|
1528
|
+
edit_version_no, edit_context_error = self._ensure_app_edit_context(
|
|
1529
|
+
profile=profile,
|
|
1530
|
+
app_key=app_key,
|
|
1531
|
+
normalized_args=normalized_args,
|
|
1532
|
+
failure_code="CUSTOM_BUTTON_UPDATE_FAILED",
|
|
1533
|
+
)
|
|
1534
|
+
if edit_context_error is not None:
|
|
1535
|
+
return finalize(edit_context_error)
|
|
1536
|
+
|
|
1517
1537
|
self.buttons.custom_button_update(
|
|
1518
1538
|
profile=profile,
|
|
1519
1539
|
app_key=app_key,
|
|
@@ -1541,6 +1561,7 @@ class AiBuilderFacade:
|
|
|
1541
1561
|
"details": {
|
|
1542
1562
|
"app_key": app_key,
|
|
1543
1563
|
"button_id": button_id,
|
|
1564
|
+
"edit_version_no": edit_version_no,
|
|
1544
1565
|
"transport_error": _transport_error_payload(api_error),
|
|
1545
1566
|
},
|
|
1546
1567
|
"request_id": api_error.request_id,
|
|
@@ -1584,6 +1605,7 @@ class AiBuilderFacade:
|
|
|
1584
1605
|
"verified": True,
|
|
1585
1606
|
"app_key": app_key,
|
|
1586
1607
|
"button_id": button_id,
|
|
1608
|
+
"edit_version_no": edit_version_no,
|
|
1587
1609
|
"button": button,
|
|
1588
1610
|
}
|
|
1589
1611
|
)
|
|
@@ -1946,6 +1968,7 @@ class AiBuilderFacade:
|
|
|
1946
1968
|
response = AppReadSummaryResponse(
|
|
1947
1969
|
app_key=app_key,
|
|
1948
1970
|
title=state["base"].get("formTitle"),
|
|
1971
|
+
app_icon=str(state["base"].get("appIcon") or "").strip() or None,
|
|
1949
1972
|
tag_ids=_coerce_int_list(state["base"].get("tagIds")),
|
|
1950
1973
|
publish_status=state["base"].get("appPublishStatus"),
|
|
1951
1974
|
field_count=len(parsed["fields"]),
|
|
@@ -2567,7 +2590,17 @@ class AiBuilderFacade:
|
|
|
2567
2590
|
upsert_views = _build_views_preset(request.preset, list(field_names))
|
|
2568
2591
|
blocking_issues: list[dict[str, Any]] = []
|
|
2569
2592
|
for patch in upsert_views:
|
|
2570
|
-
|
|
2593
|
+
raw_columns = [str(name or "").strip() for name in (patch.get("columns") or []) if str(name or "").strip()]
|
|
2594
|
+
columns = _filter_known_system_view_columns(raw_columns)
|
|
2595
|
+
if patch.get("type") in {"table", "card"} and raw_columns and not columns:
|
|
2596
|
+
blocking_issues.append(
|
|
2597
|
+
{
|
|
2598
|
+
"error_code": "VALIDATION_ERROR",
|
|
2599
|
+
"view_name": patch.get("name"),
|
|
2600
|
+
"message": "view columns must include at least one real app field; system columns cannot be applied directly",
|
|
2601
|
+
"ignored_system_columns": [name for name in raw_columns if name in _KNOWN_SYSTEM_VIEW_COLUMNS],
|
|
2602
|
+
}
|
|
2603
|
+
)
|
|
2571
2604
|
missing_columns = [name for name in columns if name not in field_names]
|
|
2572
2605
|
if missing_columns:
|
|
2573
2606
|
blocking_issues.append({"error_code": "UNKNOWN_VIEW_FIELD", "view_name": patch.get("name"), "missing_fields": missing_columns})
|
|
@@ -2642,6 +2675,7 @@ class AiBuilderFacade:
|
|
|
2642
2675
|
"app": {
|
|
2643
2676
|
"app_key": app_key,
|
|
2644
2677
|
"title": base_result.get("formTitle"),
|
|
2678
|
+
"app_icon": str(base_result.get("appIcon") or "").strip() or None,
|
|
2645
2679
|
"tag_ids": _coerce_int_list(base_result.get("tagIds")),
|
|
2646
2680
|
"publish_status": base_result.get("appPublishStatus"),
|
|
2647
2681
|
},
|
|
@@ -2674,6 +2708,8 @@ class AiBuilderFacade:
|
|
|
2674
2708
|
app_key: str = "",
|
|
2675
2709
|
package_tag_id: int | None = None,
|
|
2676
2710
|
app_name: str = "",
|
|
2711
|
+
icon: str | None = None,
|
|
2712
|
+
color: str | None = None,
|
|
2677
2713
|
create_if_missing: bool = False,
|
|
2678
2714
|
publish: bool = True,
|
|
2679
2715
|
add_fields: list[FieldPatch],
|
|
@@ -2684,6 +2720,8 @@ class AiBuilderFacade:
|
|
|
2684
2720
|
"app_key": app_key,
|
|
2685
2721
|
"package_tag_id": package_tag_id,
|
|
2686
2722
|
"app_name": app_name,
|
|
2723
|
+
"icon": icon,
|
|
2724
|
+
"color": color,
|
|
2687
2725
|
"create_if_missing": create_if_missing,
|
|
2688
2726
|
"publish": publish,
|
|
2689
2727
|
"add_fields": [patch.model_dump(mode="json") for patch in add_fields],
|
|
@@ -2734,6 +2772,8 @@ class AiBuilderFacade:
|
|
|
2734
2772
|
profile=profile,
|
|
2735
2773
|
app_name=app_name,
|
|
2736
2774
|
package_tag_id=package_tag_id,
|
|
2775
|
+
icon=icon,
|
|
2776
|
+
color=color,
|
|
2737
2777
|
)
|
|
2738
2778
|
if resolved.get("status") == "failed":
|
|
2739
2779
|
if not isinstance(resolved.get("normalized_args"), dict) or not resolved.get("normalized_args"):
|
|
@@ -2757,6 +2797,23 @@ class AiBuilderFacade:
|
|
|
2757
2797
|
if permission_outcome.block is not None:
|
|
2758
2798
|
return permission_outcome.block
|
|
2759
2799
|
permission_outcomes.append(permission_outcome)
|
|
2800
|
+
visual_result = self._ensure_app_base_visuals(
|
|
2801
|
+
profile=profile,
|
|
2802
|
+
app_key=target.app_key,
|
|
2803
|
+
fallback_title=target.app_name,
|
|
2804
|
+
icon=icon,
|
|
2805
|
+
color=color,
|
|
2806
|
+
normalized_args=normalized_args,
|
|
2807
|
+
)
|
|
2808
|
+
if visual_result.get("status") == "failed":
|
|
2809
|
+
return finalize(visual_result)
|
|
2810
|
+
else:
|
|
2811
|
+
visual_result = {
|
|
2812
|
+
"status": "success",
|
|
2813
|
+
"updated": False,
|
|
2814
|
+
"app_icon": str(resolved.get("app_icon") or "").strip() or None,
|
|
2815
|
+
"request_id": None,
|
|
2816
|
+
}
|
|
2760
2817
|
if bool(resolved.get("created")) and not requested_field_changes:
|
|
2761
2818
|
return finalize({
|
|
2762
2819
|
"status": "success",
|
|
@@ -2775,9 +2832,11 @@ class AiBuilderFacade:
|
|
|
2775
2832
|
"fields_verified": True,
|
|
2776
2833
|
"package_attached": None if package_tag_id is None else package_tag_id in target.tag_ids,
|
|
2777
2834
|
"relation_field_limit_verified": True,
|
|
2835
|
+
"app_visuals_verified": True,
|
|
2778
2836
|
"publish_skipped": True,
|
|
2779
2837
|
},
|
|
2780
2838
|
"app_key": target.app_key,
|
|
2839
|
+
"app_icon": str(resolved.get("app_icon") or visual_result.get("app_icon") or "").strip() or None,
|
|
2781
2840
|
"created": True,
|
|
2782
2841
|
"field_diff": {"added": [], "updated": [], "removed": []},
|
|
2783
2842
|
"verified": True,
|
|
@@ -2899,17 +2958,22 @@ class AiBuilderFacade:
|
|
|
2899
2958
|
"status": "success",
|
|
2900
2959
|
"error_code": None,
|
|
2901
2960
|
"recoverable": False,
|
|
2902
|
-
"message": "schema already matches requested state",
|
|
2961
|
+
"message": "updated app visuals; schema already matches requested state" if bool(visual_result.get("updated")) else "schema already matches requested state",
|
|
2903
2962
|
"normalized_args": normalized_args,
|
|
2904
2963
|
"missing_fields": [],
|
|
2905
2964
|
"allowed_values": {"field_types": [item.value for item in PublicFieldType]},
|
|
2906
2965
|
"details": {},
|
|
2907
2966
|
"request_id": None,
|
|
2908
2967
|
"suggested_next_call": None if package_attached is not False else {"tool_name": "package_attach_app", "arguments": {"profile": profile, "tag_id": package_tag_id, "app_key": target.app_key}},
|
|
2909
|
-
"noop":
|
|
2968
|
+
"noop": not bool(visual_result.get("updated")),
|
|
2910
2969
|
"warnings": relation_warnings,
|
|
2911
|
-
"verification": {
|
|
2970
|
+
"verification": {
|
|
2971
|
+
"fields_verified": True,
|
|
2972
|
+
"relation_field_limit_verified": relation_limit_verified,
|
|
2973
|
+
"app_visuals_verified": True,
|
|
2974
|
+
},
|
|
2912
2975
|
"app_key": target.app_key,
|
|
2976
|
+
"app_icon": str(visual_result.get("app_icon") or "").strip() or None,
|
|
2913
2977
|
"created": False,
|
|
2914
2978
|
"field_diff": {"added": [], "updated": [], "removed": []},
|
|
2915
2979
|
"verified": True,
|
|
@@ -2981,9 +3045,11 @@ class AiBuilderFacade:
|
|
|
2981
3045
|
"verification": {
|
|
2982
3046
|
"fields_verified": False,
|
|
2983
3047
|
"package_attached": None,
|
|
3048
|
+
"app_visuals_verified": True,
|
|
2984
3049
|
"relation_field_limit_verified": relation_limit_verified,
|
|
2985
3050
|
},
|
|
2986
3051
|
"app_key": target.app_key,
|
|
3052
|
+
"app_icon": str(visual_result.get("app_icon") or resolved.get("app_icon") or "").strip() or None,
|
|
2987
3053
|
"created": bool(resolved.get("created")),
|
|
2988
3054
|
"field_diff": {
|
|
2989
3055
|
"added": added,
|
|
@@ -3644,6 +3710,7 @@ class AiBuilderFacade:
|
|
|
3644
3710
|
for patch in upsert_views
|
|
3645
3711
|
)
|
|
3646
3712
|
valid_custom_button_ids: set[int] = set()
|
|
3713
|
+
custom_button_details_by_id: dict[int, dict[str, Any]] = {}
|
|
3647
3714
|
if requires_custom_button_validation:
|
|
3648
3715
|
try:
|
|
3649
3716
|
button_listing = self.buttons.custom_button_list(
|
|
@@ -3668,6 +3735,26 @@ class AiBuilderFacade:
|
|
|
3668
3735
|
for item in (button_listing.get("items") or [])
|
|
3669
3736
|
if isinstance(item, dict) and (button_id := _coerce_positive_int(item.get("button_id"))) is not None
|
|
3670
3737
|
}
|
|
3738
|
+
referenced_custom_button_ids = {
|
|
3739
|
+
binding.button_id
|
|
3740
|
+
for patch in upsert_views
|
|
3741
|
+
for binding in (patch.buttons or [])
|
|
3742
|
+
if binding.button_type == PublicViewButtonType.custom and binding.button_id in valid_custom_button_ids
|
|
3743
|
+
}
|
|
3744
|
+
for button_id in sorted(referenced_custom_button_ids):
|
|
3745
|
+
try:
|
|
3746
|
+
detail = self.buttons.custom_button_get(
|
|
3747
|
+
profile=profile,
|
|
3748
|
+
app_key=app_key,
|
|
3749
|
+
button_id=button_id,
|
|
3750
|
+
being_draft=True,
|
|
3751
|
+
include_raw=False,
|
|
3752
|
+
)
|
|
3753
|
+
except (QingflowApiError, RuntimeError):
|
|
3754
|
+
continue
|
|
3755
|
+
detail_result = detail.get("result")
|
|
3756
|
+
if isinstance(detail_result, dict):
|
|
3757
|
+
custom_button_details_by_id[button_id] = _normalize_custom_button_detail(detail_result)
|
|
3671
3758
|
removed: list[str] = []
|
|
3672
3759
|
view_results: list[dict[str, Any]] = []
|
|
3673
3760
|
for name in remove_views:
|
|
@@ -3704,7 +3791,24 @@ class AiBuilderFacade:
|
|
|
3704
3791
|
and _extract_view_name(view) not in remove_views
|
|
3705
3792
|
]
|
|
3706
3793
|
for ordinal, patch in enumerate(upsert_views, start=1):
|
|
3707
|
-
|
|
3794
|
+
apply_columns = _resolve_view_visible_field_names(patch)
|
|
3795
|
+
ignored_system_columns = [
|
|
3796
|
+
name
|
|
3797
|
+
for name in [str(value or "").strip() for value in patch.columns]
|
|
3798
|
+
if name in _KNOWN_SYSTEM_VIEW_COLUMNS
|
|
3799
|
+
]
|
|
3800
|
+
if patch.type in {PublicViewType.table, PublicViewType.card} and patch.columns and not apply_columns:
|
|
3801
|
+
return _failed(
|
|
3802
|
+
"VALIDATION_ERROR",
|
|
3803
|
+
"view columns must include at least one real app field; system columns cannot be applied directly",
|
|
3804
|
+
normalized_args=normalized_args,
|
|
3805
|
+
details={
|
|
3806
|
+
"app_key": app_key,
|
|
3807
|
+
"view_name": patch.name,
|
|
3808
|
+
"ignored_system_columns": ignored_system_columns,
|
|
3809
|
+
},
|
|
3810
|
+
)
|
|
3811
|
+
missing_columns = [name for name in apply_columns if name not in field_names]
|
|
3708
3812
|
if missing_columns:
|
|
3709
3813
|
return _failed(
|
|
3710
3814
|
"UNKNOWN_VIEW_FIELD",
|
|
@@ -3714,6 +3818,7 @@ class AiBuilderFacade:
|
|
|
3714
3818
|
"app_key": app_key,
|
|
3715
3819
|
"view_name": patch.name,
|
|
3716
3820
|
"missing_fields": missing_columns,
|
|
3821
|
+
"ignored_system_columns": ignored_system_columns,
|
|
3717
3822
|
},
|
|
3718
3823
|
missing_fields=missing_columns,
|
|
3719
3824
|
suggested_next_call={"tool_name": "app_read_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
@@ -3784,7 +3889,10 @@ class AiBuilderFacade:
|
|
|
3784
3889
|
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]},
|
|
3785
3890
|
suggested_next_call={"tool_name": "app_custom_button_list", "arguments": {"profile": profile, "app_key": app_key}},
|
|
3786
3891
|
)
|
|
3787
|
-
expected_button_summary =
|
|
3892
|
+
expected_button_summary = _normalize_expected_view_buttons_for_compare(
|
|
3893
|
+
explicit_button_dtos or [],
|
|
3894
|
+
custom_button_details_by_id=custom_button_details_by_id,
|
|
3895
|
+
)
|
|
3788
3896
|
matched_existing_view: dict[str, Any] | None = None
|
|
3789
3897
|
existing_key: str | None = None
|
|
3790
3898
|
if patch.view_key:
|
|
@@ -3837,12 +3945,14 @@ class AiBuilderFacade:
|
|
|
3837
3945
|
system_view_sync: dict[str, Any] | None = None
|
|
3838
3946
|
if system_view_list_type is not None and patch.type.value == "table":
|
|
3839
3947
|
operation_phase = "default_view_apply_config_sync"
|
|
3840
|
-
system_view_sync = self.
|
|
3948
|
+
system_view_sync = self._sync_system_view_and_restore_buttons(
|
|
3841
3949
|
profile=profile,
|
|
3842
3950
|
app_key=app_key,
|
|
3951
|
+
viewgraph_key=existing_key,
|
|
3952
|
+
payload=payload,
|
|
3843
3953
|
list_type=system_view_list_type,
|
|
3844
3954
|
schema=schema,
|
|
3845
|
-
visible_field_names=
|
|
3955
|
+
visible_field_names=apply_columns,
|
|
3846
3956
|
)
|
|
3847
3957
|
if not bool(system_view_sync.get("verified")):
|
|
3848
3958
|
failure_entry = {
|
|
@@ -3864,6 +3974,7 @@ class AiBuilderFacade:
|
|
|
3864
3974
|
"list_type": system_view_list_type,
|
|
3865
3975
|
"expected_visible_order": system_view_sync.get("expected_visible_order"),
|
|
3866
3976
|
"actual_visible_order": system_view_sync.get("actual_visible_order"),
|
|
3977
|
+
"apply_columns": apply_columns,
|
|
3867
3978
|
},
|
|
3868
3979
|
}
|
|
3869
3980
|
failed_views.append(failure_entry)
|
|
@@ -3879,6 +3990,7 @@ class AiBuilderFacade:
|
|
|
3879
3990
|
"expected_filters": deepcopy(translated_filters),
|
|
3880
3991
|
"expected_buttons": deepcopy(expected_button_summary),
|
|
3881
3992
|
"system_view_sync": system_view_sync,
|
|
3993
|
+
"apply_columns": deepcopy(apply_columns),
|
|
3882
3994
|
}
|
|
3883
3995
|
)
|
|
3884
3996
|
else:
|
|
@@ -3956,6 +4068,49 @@ class AiBuilderFacade:
|
|
|
3956
4068
|
explicit_button_dtos=fallback_button_dtos,
|
|
3957
4069
|
)
|
|
3958
4070
|
self.views.view_update(profile=profile, viewgraph_key=target_key, payload=fallback_payload)
|
|
4071
|
+
system_view_sync: dict[str, Any] | None = None
|
|
4072
|
+
fallback_system_view_list_type = (
|
|
4073
|
+
_resolve_system_view_list_type(view_key=target_key, view_name=patch.name)
|
|
4074
|
+
if patch.type.value == "table" and target_key
|
|
4075
|
+
else None
|
|
4076
|
+
)
|
|
4077
|
+
if fallback_system_view_list_type is not None:
|
|
4078
|
+
operation_phase = "default_view_apply_config_sync"
|
|
4079
|
+
system_view_sync = self._sync_system_view_and_restore_buttons(
|
|
4080
|
+
profile=profile,
|
|
4081
|
+
app_key=app_key,
|
|
4082
|
+
viewgraph_key=target_key,
|
|
4083
|
+
payload=fallback_payload,
|
|
4084
|
+
list_type=fallback_system_view_list_type,
|
|
4085
|
+
schema=schema,
|
|
4086
|
+
visible_field_names=apply_columns,
|
|
4087
|
+
)
|
|
4088
|
+
if not bool(system_view_sync.get("verified")):
|
|
4089
|
+
failure_entry = {
|
|
4090
|
+
"name": patch.name,
|
|
4091
|
+
"view_key": target_key,
|
|
4092
|
+
"type": patch.type.value,
|
|
4093
|
+
"status": "failed",
|
|
4094
|
+
"error_code": "SYSTEM_VIEW_ORDER_SYNC_FAILED",
|
|
4095
|
+
"message": "default view column order did not verify through app apply/baseInfo readback",
|
|
4096
|
+
"request_id": None,
|
|
4097
|
+
"backend_code": None,
|
|
4098
|
+
"http_status": None,
|
|
4099
|
+
"operation": "sync_default_view",
|
|
4100
|
+
"details": {
|
|
4101
|
+
"app_key": app_key,
|
|
4102
|
+
"view_name": patch.name,
|
|
4103
|
+
"view_key": target_key,
|
|
4104
|
+
"view_type": patch.type.value,
|
|
4105
|
+
"list_type": fallback_system_view_list_type,
|
|
4106
|
+
"expected_visible_order": system_view_sync.get("expected_visible_order"),
|
|
4107
|
+
"actual_visible_order": system_view_sync.get("actual_visible_order"),
|
|
4108
|
+
"apply_columns": apply_columns,
|
|
4109
|
+
},
|
|
4110
|
+
}
|
|
4111
|
+
failed_views.append(failure_entry)
|
|
4112
|
+
view_results.append(failure_entry)
|
|
4113
|
+
continue
|
|
3959
4114
|
if existing_key:
|
|
3960
4115
|
updated.append(patch.name)
|
|
3961
4116
|
view_results.append(
|
|
@@ -3967,6 +4122,8 @@ class AiBuilderFacade:
|
|
|
3967
4122
|
"fallback_applied": True,
|
|
3968
4123
|
"expected_filters": deepcopy(translated_filters),
|
|
3969
4124
|
"expected_buttons": deepcopy(expected_button_summary),
|
|
4125
|
+
"system_view_sync": system_view_sync,
|
|
4126
|
+
"apply_columns": deepcopy(apply_columns),
|
|
3970
4127
|
}
|
|
3971
4128
|
)
|
|
3972
4129
|
else:
|
|
@@ -3980,6 +4137,8 @@ class AiBuilderFacade:
|
|
|
3980
4137
|
"fallback_applied": True,
|
|
3981
4138
|
"expected_filters": deepcopy(translated_filters),
|
|
3982
4139
|
"expected_buttons": deepcopy(expected_button_summary),
|
|
4140
|
+
"system_view_sync": system_view_sync,
|
|
4141
|
+
"apply_columns": deepcopy(apply_columns),
|
|
3983
4142
|
}
|
|
3984
4143
|
)
|
|
3985
4144
|
continue
|
|
@@ -4079,6 +4238,8 @@ class AiBuilderFacade:
|
|
|
4079
4238
|
filter_mismatches: list[dict[str, Any]] = []
|
|
4080
4239
|
button_readback_pending = False
|
|
4081
4240
|
button_mismatches: list[dict[str, Any]] = []
|
|
4241
|
+
custom_button_readback_pending = False
|
|
4242
|
+
custom_button_readback_pending_entries: list[dict[str, Any]] = []
|
|
4082
4243
|
for item in view_results:
|
|
4083
4244
|
status = str(item.get("status") or "")
|
|
4084
4245
|
name = str(item.get("name") or "")
|
|
@@ -4181,12 +4342,28 @@ class AiBuilderFacade:
|
|
|
4181
4342
|
config_response = self.views.view_get_config(profile=profile, viewgraph_key=verification_key)
|
|
4182
4343
|
config_result = (config_response.get("result") or {}) if isinstance(config_response.get("result"), dict) else {}
|
|
4183
4344
|
actual_buttons = _normalize_view_buttons_for_compare(config_result)
|
|
4184
|
-
|
|
4345
|
+
button_comparison = _compare_view_button_summaries(
|
|
4346
|
+
expected=expected_buttons,
|
|
4347
|
+
actual=actual_buttons,
|
|
4348
|
+
)
|
|
4349
|
+
buttons_verified = bool(button_comparison.get("verified"))
|
|
4185
4350
|
verification_entry["buttons_verified"] = buttons_verified
|
|
4186
4351
|
verification_entry["view_key"] = verification_key
|
|
4187
4352
|
verification_entry["expected_buttons"] = deepcopy(expected_buttons)
|
|
4188
4353
|
verification_entry["actual_buttons"] = actual_buttons
|
|
4189
|
-
if
|
|
4354
|
+
if button_comparison.get("custom_button_readback_pending"):
|
|
4355
|
+
verification_entry["custom_button_readback_pending"] = True
|
|
4356
|
+
verification_entry["pending_custom_buttons"] = deepcopy(button_comparison.get("pending_custom_buttons") or [])
|
|
4357
|
+
custom_button_readback_pending = True
|
|
4358
|
+
custom_button_readback_pending_entries.append(
|
|
4359
|
+
{
|
|
4360
|
+
"name": name,
|
|
4361
|
+
"type": item.get("type"),
|
|
4362
|
+
"view_key": verification_key,
|
|
4363
|
+
"pending_custom_buttons": deepcopy(button_comparison.get("pending_custom_buttons") or []),
|
|
4364
|
+
}
|
|
4365
|
+
)
|
|
4366
|
+
elif not buttons_verified:
|
|
4190
4367
|
button_mismatches.append(
|
|
4191
4368
|
{
|
|
4192
4369
|
"name": name,
|
|
@@ -4249,6 +4426,11 @@ class AiBuilderFacade:
|
|
|
4249
4426
|
"per_view_results": view_results,
|
|
4250
4427
|
"filter_mismatches": filter_mismatches,
|
|
4251
4428
|
"button_mismatches": button_mismatches,
|
|
4429
|
+
**(
|
|
4430
|
+
{"custom_button_readback_pending": deepcopy(custom_button_readback_pending_entries)}
|
|
4431
|
+
if custom_button_readback_pending_entries
|
|
4432
|
+
else {}
|
|
4433
|
+
),
|
|
4252
4434
|
},
|
|
4253
4435
|
"request_id": first_failure.get("request_id"),
|
|
4254
4436
|
"suggested_next_call": {"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
@@ -4256,10 +4438,21 @@ class AiBuilderFacade:
|
|
|
4256
4438
|
"http_status": first_failure.get("http_status"),
|
|
4257
4439
|
"noop": noop,
|
|
4258
4440
|
"warnings": (
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4441
|
+
(
|
|
4442
|
+
[_warning("VIEW_FILTERS_UNVERIFIED", "view definitions may exist, but saved filter behavior is not fully verified")]
|
|
4443
|
+
if (filter_readback_pending or filter_mismatches)
|
|
4444
|
+
else []
|
|
4445
|
+
)
|
|
4446
|
+
+ (
|
|
4447
|
+
[_warning("VIEW_BUTTONS_UNVERIFIED", "view definitions may exist, but saved button behavior is not fully verified")]
|
|
4448
|
+
if (button_readback_pending or button_mismatches)
|
|
4449
|
+
else []
|
|
4450
|
+
)
|
|
4451
|
+
+ (
|
|
4452
|
+
[_warning("VIEW_CUSTOM_BUTTON_READBACK_PENDING", "system buttons verified, but draft custom button bindings are not fully visible through view readback yet")]
|
|
4453
|
+
if custom_button_readback_pending
|
|
4454
|
+
else []
|
|
4455
|
+
)
|
|
4263
4456
|
),
|
|
4264
4457
|
"verification": {
|
|
4265
4458
|
"views_verified": verified,
|
|
@@ -4267,6 +4460,8 @@ class AiBuilderFacade:
|
|
|
4267
4460
|
"view_buttons_verified": view_buttons_verified,
|
|
4268
4461
|
"views_read_unavailable": verified_views_unavailable,
|
|
4269
4462
|
"by_view": verification_by_view,
|
|
4463
|
+
"custom_button_readback_pending": custom_button_readback_pending,
|
|
4464
|
+
"custom_button_readback_pending_entries": deepcopy(custom_button_readback_pending_entries),
|
|
4270
4465
|
},
|
|
4271
4466
|
"app_key": app_key,
|
|
4272
4467
|
"views_diff": {"created": created, "updated": updated, "removed": removed, "failed": failed_views},
|
|
@@ -4278,6 +4473,13 @@ class AiBuilderFacade:
|
|
|
4278
4473
|
warnings.append(_warning("VIEW_FILTERS_UNVERIFIED", "view definitions were applied, but saved filter behavior is not fully verified"))
|
|
4279
4474
|
if button_readback_pending or button_mismatches:
|
|
4280
4475
|
warnings.append(_warning("VIEW_BUTTONS_UNVERIFIED", "view definitions were applied, but saved button behavior is not fully verified"))
|
|
4476
|
+
if custom_button_readback_pending:
|
|
4477
|
+
warnings.append(
|
|
4478
|
+
_warning(
|
|
4479
|
+
"VIEW_CUSTOM_BUTTON_READBACK_PENDING",
|
|
4480
|
+
"system buttons verified, but draft custom button bindings are not fully visible through view readback yet",
|
|
4481
|
+
)
|
|
4482
|
+
)
|
|
4281
4483
|
response = {
|
|
4282
4484
|
"status": "success" if verified and view_filters_verified and view_buttons_verified else "partial_success",
|
|
4283
4485
|
"error_code": None if verified and view_filters_verified and view_buttons_verified else ("VIEW_BUTTON_READBACK_MISMATCH" if button_mismatches else "VIEW_FILTER_READBACK_MISMATCH" if filter_mismatches else "VIEWS_READBACK_PENDING"),
|
|
@@ -4297,6 +4499,11 @@ class AiBuilderFacade:
|
|
|
4297
4499
|
"details": {
|
|
4298
4500
|
**({"filter_mismatches": filter_mismatches} if filter_mismatches else {}),
|
|
4299
4501
|
**({"button_mismatches": button_mismatches} if button_mismatches else {}),
|
|
4502
|
+
**(
|
|
4503
|
+
{"custom_button_readback_pending": deepcopy(custom_button_readback_pending_entries)}
|
|
4504
|
+
if custom_button_readback_pending_entries
|
|
4505
|
+
else {}
|
|
4506
|
+
),
|
|
4300
4507
|
},
|
|
4301
4508
|
"request_id": None,
|
|
4302
4509
|
"suggested_next_call": None if verified and view_filters_verified and view_buttons_verified else {"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
@@ -4309,6 +4516,8 @@ class AiBuilderFacade:
|
|
|
4309
4516
|
"views_read_unavailable": verified_views_unavailable,
|
|
4310
4517
|
"filter_readback_pending": filter_readback_pending,
|
|
4311
4518
|
"button_readback_pending": button_readback_pending,
|
|
4519
|
+
"custom_button_readback_pending": custom_button_readback_pending,
|
|
4520
|
+
"custom_button_readback_pending_entries": deepcopy(custom_button_readback_pending_entries),
|
|
4312
4521
|
"by_view": verification_by_view,
|
|
4313
4522
|
},
|
|
4314
4523
|
"app_key": app_key,
|
|
@@ -5055,6 +5264,28 @@ class AiBuilderFacade:
|
|
|
5055
5264
|
version_result = {}
|
|
5056
5265
|
return _coerce_positive_int(version_result.get("editVersionNo") or version_result.get("versionNo")) or int(current_schema.get("editVersionNo") or 1)
|
|
5057
5266
|
|
|
5267
|
+
def _ensure_app_edit_context(
|
|
5268
|
+
self,
|
|
5269
|
+
*,
|
|
5270
|
+
profile: str,
|
|
5271
|
+
app_key: str,
|
|
5272
|
+
normalized_args: dict[str, Any],
|
|
5273
|
+
failure_code: str,
|
|
5274
|
+
) -> tuple[int | None, JSONObject | None]:
|
|
5275
|
+
try:
|
|
5276
|
+
version_result = self.apps.app_get_edit_version_no(profile=profile, app_key=app_key).get("result") or {}
|
|
5277
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
5278
|
+
api_error = _coerce_api_error(error)
|
|
5279
|
+
return None, _failed_from_api_error(
|
|
5280
|
+
failure_code,
|
|
5281
|
+
api_error,
|
|
5282
|
+
normalized_args=normalized_args,
|
|
5283
|
+
details={"app_key": app_key, "phase": "prepare_edit_context"},
|
|
5284
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
5285
|
+
)
|
|
5286
|
+
edit_version_no = _coerce_positive_int(version_result.get("editVersionNo") or version_result.get("versionNo")) or 1
|
|
5287
|
+
return edit_version_no, None
|
|
5288
|
+
|
|
5058
5289
|
def _append_publish_result(self, *, profile: str, app_key: str, publish: bool, response: JSONObject) -> JSONObject:
|
|
5059
5290
|
verification = response.get("verification")
|
|
5060
5291
|
if not isinstance(verification, dict):
|
|
@@ -5134,6 +5365,29 @@ class AiBuilderFacade:
|
|
|
5134
5365
|
"verified": verified,
|
|
5135
5366
|
}
|
|
5136
5367
|
|
|
5368
|
+
def _sync_system_view_and_restore_buttons(
|
|
5369
|
+
self,
|
|
5370
|
+
*,
|
|
5371
|
+
profile: str,
|
|
5372
|
+
app_key: str,
|
|
5373
|
+
viewgraph_key: str,
|
|
5374
|
+
payload: dict[str, Any],
|
|
5375
|
+
list_type: int,
|
|
5376
|
+
schema: dict[str, Any],
|
|
5377
|
+
visible_field_names: list[str],
|
|
5378
|
+
) -> dict[str, Any]:
|
|
5379
|
+
sync_result = self._sync_system_view_apply_config(
|
|
5380
|
+
profile=profile,
|
|
5381
|
+
app_key=app_key,
|
|
5382
|
+
list_type=list_type,
|
|
5383
|
+
schema=schema,
|
|
5384
|
+
visible_field_names=visible_field_names,
|
|
5385
|
+
)
|
|
5386
|
+
if bool(sync_result.get("verified")) and "buttonConfigDTOList" in payload:
|
|
5387
|
+
self.views.view_update(profile=profile, viewgraph_key=viewgraph_key, payload=payload)
|
|
5388
|
+
sync_result = {**sync_result, "button_config_restored": True}
|
|
5389
|
+
return sync_result
|
|
5390
|
+
|
|
5137
5391
|
def _load_views_result(self, *, profile: str, app_key: str, tolerate_404: bool) -> tuple[Any, bool]:
|
|
5138
5392
|
try:
|
|
5139
5393
|
views = self.views.view_list_flat(profile=profile, app_key=app_key)
|
|
@@ -5262,9 +5516,17 @@ class AiBuilderFacade:
|
|
|
5262
5516
|
profile: str,
|
|
5263
5517
|
app_name: str,
|
|
5264
5518
|
package_tag_id: int | None,
|
|
5519
|
+
icon: str | None,
|
|
5520
|
+
color: str | None,
|
|
5265
5521
|
) -> JSONObject:
|
|
5266
5522
|
payload: JSONObject = {
|
|
5267
5523
|
"appName": app_name or "未命名应用",
|
|
5524
|
+
"appIcon": encode_workspace_icon_with_defaults(
|
|
5525
|
+
icon=icon,
|
|
5526
|
+
color=color,
|
|
5527
|
+
title=app_name or "未命名应用",
|
|
5528
|
+
fallback_icon_name="template",
|
|
5529
|
+
),
|
|
5268
5530
|
"auth": default_member_auth(),
|
|
5269
5531
|
"tagIds": [package_tag_id] if package_tag_id and package_tag_id > 0 else [],
|
|
5270
5532
|
}
|
|
@@ -5310,6 +5572,7 @@ class AiBuilderFacade:
|
|
|
5310
5572
|
"suggested_next_call": None,
|
|
5311
5573
|
"app_key": new_app_key,
|
|
5312
5574
|
"app_name": app_name or "未命名应用",
|
|
5575
|
+
"app_icon": payload.get("appIcon"),
|
|
5313
5576
|
"tag_ids": [package_tag_id] if package_tag_id and package_tag_id > 0 else [],
|
|
5314
5577
|
"created": True,
|
|
5315
5578
|
}
|
|
@@ -5321,10 +5584,103 @@ class AiBuilderFacade:
|
|
|
5321
5584
|
"suggested_next_call": None,
|
|
5322
5585
|
"app_key": new_app_key,
|
|
5323
5586
|
"app_name": base.get("formTitle") or app_name or "未命名应用",
|
|
5587
|
+
"app_icon": str(base.get("appIcon") or payload.get("appIcon") or "").strip() or None,
|
|
5324
5588
|
"tag_ids": _coerce_int_list(base.get("tagIds")),
|
|
5325
5589
|
"created": True,
|
|
5326
5590
|
}
|
|
5327
5591
|
|
|
5592
|
+
def _build_app_base_update_payload(
|
|
5593
|
+
self,
|
|
5594
|
+
*,
|
|
5595
|
+
raw_base: dict[str, Any],
|
|
5596
|
+
form_title: str,
|
|
5597
|
+
app_icon: str,
|
|
5598
|
+
) -> JSONObject | None:
|
|
5599
|
+
auth = deepcopy(raw_base.get("auth"))
|
|
5600
|
+
if not isinstance(auth, dict):
|
|
5601
|
+
return None
|
|
5602
|
+
payload: JSONObject = {
|
|
5603
|
+
"formTitle": form_title,
|
|
5604
|
+
"auth": auth,
|
|
5605
|
+
"appIcon": app_icon,
|
|
5606
|
+
}
|
|
5607
|
+
tag_ids = _coerce_int_list(raw_base.get("tagIds"))
|
|
5608
|
+
if tag_ids:
|
|
5609
|
+
payload["tagIds"] = tag_ids
|
|
5610
|
+
return payload
|
|
5611
|
+
|
|
5612
|
+
def _ensure_app_base_visuals(
|
|
5613
|
+
self,
|
|
5614
|
+
*,
|
|
5615
|
+
profile: str,
|
|
5616
|
+
app_key: str,
|
|
5617
|
+
fallback_title: str,
|
|
5618
|
+
icon: str | None,
|
|
5619
|
+
color: str | None,
|
|
5620
|
+
normalized_args: dict[str, Any],
|
|
5621
|
+
) -> JSONObject:
|
|
5622
|
+
if not icon and not color:
|
|
5623
|
+
return {
|
|
5624
|
+
"status": "success",
|
|
5625
|
+
"updated": False,
|
|
5626
|
+
"app_icon": None,
|
|
5627
|
+
"request_id": None,
|
|
5628
|
+
}
|
|
5629
|
+
try:
|
|
5630
|
+
base_result = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True)
|
|
5631
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
5632
|
+
api_error = _coerce_api_error(error)
|
|
5633
|
+
return _failed_from_api_error(
|
|
5634
|
+
"APP_VISUAL_READ_FAILED",
|
|
5635
|
+
api_error,
|
|
5636
|
+
normalized_args=normalized_args,
|
|
5637
|
+
details={"app_key": app_key},
|
|
5638
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
5639
|
+
)
|
|
5640
|
+
raw_base = base_result.get("result") if isinstance(base_result.get("result"), dict) else {}
|
|
5641
|
+
effective_title = str(raw_base.get("formTitle") or fallback_title or "未命名应用").strip() or "未命名应用"
|
|
5642
|
+
existing_icon = str(raw_base.get("appIcon") or "").strip() or None
|
|
5643
|
+
desired_icon = encode_workspace_icon_with_defaults(
|
|
5644
|
+
icon=icon,
|
|
5645
|
+
color=color,
|
|
5646
|
+
title=effective_title,
|
|
5647
|
+
fallback_icon_name="template",
|
|
5648
|
+
existing_icon=existing_icon,
|
|
5649
|
+
)
|
|
5650
|
+
if desired_icon == existing_icon:
|
|
5651
|
+
return {
|
|
5652
|
+
"status": "success",
|
|
5653
|
+
"updated": False,
|
|
5654
|
+
"app_icon": desired_icon,
|
|
5655
|
+
"request_id": None,
|
|
5656
|
+
}
|
|
5657
|
+
payload = self._build_app_base_update_payload(raw_base=raw_base, form_title=effective_title, app_icon=desired_icon)
|
|
5658
|
+
if payload is None:
|
|
5659
|
+
return _failed(
|
|
5660
|
+
"APP_VISUAL_UPDATE_UNSUPPORTED",
|
|
5661
|
+
"app base info did not include editable auth payload required for icon update",
|
|
5662
|
+
normalized_args=normalized_args,
|
|
5663
|
+
details={"app_key": app_key},
|
|
5664
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
5665
|
+
)
|
|
5666
|
+
try:
|
|
5667
|
+
update_result = self.apps.app_update_base(profile=profile, app_key=app_key, payload=payload)
|
|
5668
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
5669
|
+
api_error = _coerce_api_error(error)
|
|
5670
|
+
return _failed_from_api_error(
|
|
5671
|
+
"APP_VISUAL_UPDATE_FAILED",
|
|
5672
|
+
api_error,
|
|
5673
|
+
normalized_args=normalized_args,
|
|
5674
|
+
details={"app_key": app_key, "app_icon": desired_icon},
|
|
5675
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
5676
|
+
)
|
|
5677
|
+
return {
|
|
5678
|
+
"status": "success",
|
|
5679
|
+
"updated": True,
|
|
5680
|
+
"app_icon": desired_icon,
|
|
5681
|
+
"request_id": update_result.get("request_id") if isinstance(update_result, dict) else None,
|
|
5682
|
+
}
|
|
5683
|
+
|
|
5328
5684
|
def _current_request_route(self, profile: str) -> JSONObject:
|
|
5329
5685
|
session_profile = self.apps.sessions.get_profile(profile)
|
|
5330
5686
|
backend_session = self.apps.sessions.get_backend_session(profile)
|
|
@@ -5507,11 +5863,15 @@ def _serialize_custom_button_payload(payload: CustomButtonPatch) -> dict[str, An
|
|
|
5507
5863
|
"buttonIcon": data["button_icon"],
|
|
5508
5864
|
"triggerAction": data["trigger_action"],
|
|
5509
5865
|
}
|
|
5866
|
+
if str(data.get("icon_color") or "").strip():
|
|
5867
|
+
serialized["iconColor"] = data["icon_color"]
|
|
5510
5868
|
if str(data.get("trigger_link_url") or "").strip():
|
|
5511
5869
|
serialized["triggerLinkUrl"] = data["trigger_link_url"]
|
|
5512
5870
|
trigger_add_data_config = data.get("trigger_add_data_config")
|
|
5513
5871
|
if isinstance(trigger_add_data_config, dict):
|
|
5514
5872
|
serialized["triggerAddDataConfig"] = _serialize_custom_button_add_data_config(trigger_add_data_config)
|
|
5873
|
+
else:
|
|
5874
|
+
serialized["triggerAddDataConfig"] = _serialize_custom_button_add_data_config({})
|
|
5515
5875
|
external_qrobot_config = data.get("external_qrobot_config")
|
|
5516
5876
|
if isinstance(external_qrobot_config, dict):
|
|
5517
5877
|
serialized["customButtonExternalQRobotRelationVO"] = _serialize_custom_button_external_qrobot_config(external_qrobot_config)
|
|
@@ -5594,6 +5954,7 @@ def _normalize_custom_button_summary(item: dict[str, Any]) -> dict[str, Any]:
|
|
|
5594
5954
|
"button_id": _coerce_positive_int(item.get("button_id") or item.get("buttonId") or item.get("id")),
|
|
5595
5955
|
"button_text": str(item.get("button_text") or item.get("buttonText") or "").strip() or None,
|
|
5596
5956
|
"button_icon": str(item.get("button_icon") or item.get("buttonIcon") or "").strip() or None,
|
|
5957
|
+
"icon_color": str(item.get("icon_color") or item.get("iconColor") or "").strip() or None,
|
|
5597
5958
|
"background_color": str(item.get("background_color") or item.get("backgroundColor") or "").strip() or None,
|
|
5598
5959
|
"text_color": str(item.get("text_color") or item.get("textColor") or "").strip() or None,
|
|
5599
5960
|
"used_in_chart_count": _coerce_nonnegative_int(item.get("used_in_chart_count") or item.get("userInChartCount")),
|
|
@@ -6350,11 +6711,12 @@ def _build_public_portal_base_payload(
|
|
|
6350
6711
|
effective_name = str(dash_name or data.get("dashName") or "").strip() or "未命名门户"
|
|
6351
6712
|
effective_hide_copyright = hide_copyright if hide_copyright is not None else bool(data.get("hideCopyright", False))
|
|
6352
6713
|
if icon or color or not data.get("dashIcon"):
|
|
6353
|
-
data["dashIcon"] =
|
|
6714
|
+
data["dashIcon"] = encode_workspace_icon_with_defaults(
|
|
6354
6715
|
icon=icon,
|
|
6355
6716
|
color=color,
|
|
6356
6717
|
title=effective_name,
|
|
6357
6718
|
fallback_icon_name="view-grid",
|
|
6719
|
+
existing_icon=str(data.get("dashIcon") or "").strip() or None,
|
|
6358
6720
|
)
|
|
6359
6721
|
data["dashName"] = effective_name
|
|
6360
6722
|
data["auth"] = deepcopy(auth if auth is not None else data.get("auth") or default_member_auth())
|
|
@@ -8072,6 +8434,36 @@ def _merge_view_summary_with_config(base: dict[str, Any], *, config: dict[str, A
|
|
|
8072
8434
|
for entry in display_entries
|
|
8073
8435
|
if str(entry.get("name") or "").strip()
|
|
8074
8436
|
]
|
|
8437
|
+
apply_entries = [
|
|
8438
|
+
entry
|
|
8439
|
+
for entry in display_entries
|
|
8440
|
+
if _coerce_nonnegative_int(entry.get("field_id")) is not None
|
|
8441
|
+
and str(entry.get("name") or "").strip()
|
|
8442
|
+
and str(entry.get("name") or "").strip() not in _KNOWN_SYSTEM_VIEW_COLUMNS
|
|
8443
|
+
]
|
|
8444
|
+
apply_column_ids = [
|
|
8445
|
+
field_id
|
|
8446
|
+
for field_id in (_coerce_nonnegative_int(entry.get("field_id")) for entry in apply_entries)
|
|
8447
|
+
if field_id is not None
|
|
8448
|
+
]
|
|
8449
|
+
apply_columns = [
|
|
8450
|
+
str(entry.get("name") or "").strip()
|
|
8451
|
+
for entry in apply_entries
|
|
8452
|
+
if str(entry.get("name") or "").strip()
|
|
8453
|
+
]
|
|
8454
|
+
if not apply_columns and configured_column_ids:
|
|
8455
|
+
apply_columns = [
|
|
8456
|
+
str((question_entries_by_id.get(field_id) or {}).get("name") or "").strip()
|
|
8457
|
+
for field_id in configured_column_ids
|
|
8458
|
+
if str((question_entries_by_id.get(field_id) or {}).get("name") or "").strip()
|
|
8459
|
+
and str((question_entries_by_id.get(field_id) or {}).get("name") or "").strip() not in _KNOWN_SYSTEM_VIEW_COLUMNS
|
|
8460
|
+
]
|
|
8461
|
+
apply_column_ids = [
|
|
8462
|
+
field_id
|
|
8463
|
+
for field_id in configured_column_ids
|
|
8464
|
+
if str((question_entries_by_id.get(field_id) or {}).get("name") or "").strip()
|
|
8465
|
+
and str((question_entries_by_id.get(field_id) or {}).get("name") or "").strip() not in _KNOWN_SYSTEM_VIEW_COLUMNS
|
|
8466
|
+
]
|
|
8075
8467
|
if not display_columns and configured_columns:
|
|
8076
8468
|
display_columns = configured_columns
|
|
8077
8469
|
display_column_ids = list(configured_column_ids)
|
|
@@ -8087,6 +8479,10 @@ def _merge_view_summary_with_config(base: dict[str, Any], *, config: dict[str, A
|
|
|
8087
8479
|
summary["configured_columns"] = configured_columns
|
|
8088
8480
|
summary["configured_column_ids"] = configured_column_ids
|
|
8089
8481
|
config_enriched = True
|
|
8482
|
+
if apply_columns:
|
|
8483
|
+
summary["apply_columns"] = apply_columns
|
|
8484
|
+
summary["apply_column_ids"] = apply_column_ids
|
|
8485
|
+
config_enriched = True
|
|
8090
8486
|
if question_entries:
|
|
8091
8487
|
summary["column_details"] = display_entries or _sort_view_question_entries(question_entries)
|
|
8092
8488
|
config_enriched = True
|
|
@@ -8337,6 +8733,7 @@ def _normalize_view_button_entry(entry: dict[str, Any]) -> dict[str, Any]:
|
|
|
8337
8733
|
for public_key, source_key in (
|
|
8338
8734
|
("default_button_text", "defaultButtonText"),
|
|
8339
8735
|
("button_icon", "buttonIcon"),
|
|
8736
|
+
("icon_color", "iconColor"),
|
|
8340
8737
|
("background_color", "backgroundColor"),
|
|
8341
8738
|
("text_color", "textColor"),
|
|
8342
8739
|
("trigger_link_url", "triggerLinkUrl"),
|
|
@@ -8366,17 +8763,21 @@ def _normalize_view_buttons_for_compare(value: Any) -> list[dict[str, Any]]:
|
|
|
8366
8763
|
normalized_entries: list[dict[str, Any]] = []
|
|
8367
8764
|
for entry in entries:
|
|
8368
8765
|
normalized = _normalize_view_button_entry(entry)
|
|
8766
|
+
is_custom = normalized.get("button_type") == "CUSTOM"
|
|
8369
8767
|
normalized_entries.append(
|
|
8370
8768
|
{
|
|
8371
8769
|
"button_type": normalized.get("button_type"),
|
|
8372
8770
|
"config_type": normalized.get("config_type"),
|
|
8373
|
-
"button_id": normalized.get("button_id"),
|
|
8771
|
+
"button_id": normalized.get("button_id") if is_custom else None,
|
|
8374
8772
|
"button_text": normalized.get("button_text"),
|
|
8375
8773
|
"button_icon": normalized.get("button_icon"),
|
|
8774
|
+
"icon_color": normalized.get("icon_color"),
|
|
8376
8775
|
"background_color": normalized.get("background_color"),
|
|
8377
8776
|
"text_color": normalized.get("text_color"),
|
|
8378
8777
|
"trigger_action": normalized.get("trigger_action"),
|
|
8379
|
-
|
|
8778
|
+
# Custom button trigger details are verified by dedicated button CRUD readback.
|
|
8779
|
+
# View binding verification should focus on binding/display semantics and tolerate resource-owned URLs.
|
|
8780
|
+
"trigger_link_url": None if is_custom else normalized.get("trigger_link_url"),
|
|
8380
8781
|
"being_main": bool(normalized.get("being_main", False)),
|
|
8381
8782
|
"print_tpls": _normalize_print_tpls_for_compare(normalized.get("print_tpls")),
|
|
8382
8783
|
"button_formula": str(normalized.get("button_formula") or ""),
|
|
@@ -8387,12 +8788,127 @@ def _normalize_view_buttons_for_compare(value: Any) -> list[dict[str, Any]]:
|
|
|
8387
8788
|
return normalized_entries
|
|
8388
8789
|
|
|
8389
8790
|
|
|
8791
|
+
_SYSTEM_VIEW_BUTTON_ID_BY_ACTION: dict[tuple[str, str], int] = {
|
|
8792
|
+
("TOP", "set"): 1,
|
|
8793
|
+
("TOP", "switchView"): 2,
|
|
8794
|
+
("TOP", "setRowHeight"): 3,
|
|
8795
|
+
("TOP", "search"): 6,
|
|
8796
|
+
("DETAIL", "share"): 7,
|
|
8797
|
+
("DETAIL", "edit"): 8,
|
|
8798
|
+
}
|
|
8799
|
+
|
|
8800
|
+
_SYSTEM_VIEW_BUTTON_ID_BY_TEXT: dict[tuple[str, str], int] = {
|
|
8801
|
+
("TOP", "字段管理"): 1,
|
|
8802
|
+
("TOP", "视图类型"): 2,
|
|
8803
|
+
("TOP", "行高"): 3,
|
|
8804
|
+
("TOP", "搜索"): 6,
|
|
8805
|
+
("DETAIL", "分享"): 7,
|
|
8806
|
+
("DETAIL", "修改"): 8,
|
|
8807
|
+
("DETAIL", "修改记录"): 8,
|
|
8808
|
+
}
|
|
8809
|
+
|
|
8810
|
+
|
|
8811
|
+
def _resolve_system_view_button_logical_id(entry: dict[str, Any]) -> int | None:
|
|
8812
|
+
config_type = _normalize_view_button_config_type(entry.get("configType") or entry.get("config_type")) or ""
|
|
8813
|
+
trigger_action = str(entry.get("triggerAction") or entry.get("trigger_action") or "").strip()
|
|
8814
|
+
if config_type and trigger_action:
|
|
8815
|
+
mapped = _SYSTEM_VIEW_BUTTON_ID_BY_ACTION.get((config_type, trigger_action))
|
|
8816
|
+
if mapped is not None:
|
|
8817
|
+
return mapped
|
|
8818
|
+
button_text = str(entry.get("buttonText") or entry.get("button_text") or "").strip()
|
|
8819
|
+
default_button_text = str(entry.get("defaultButtonText") or entry.get("default_button_text") or "").strip()
|
|
8820
|
+
for candidate in (button_text, default_button_text):
|
|
8821
|
+
if config_type and candidate:
|
|
8822
|
+
mapped = _SYSTEM_VIEW_BUTTON_ID_BY_TEXT.get((config_type, candidate))
|
|
8823
|
+
if mapped is not None:
|
|
8824
|
+
return mapped
|
|
8825
|
+
button_id = _coerce_positive_int(entry.get("buttonId") or entry.get("button_id") or entry.get("id"))
|
|
8826
|
+
if button_id is not None and button_id < 1000:
|
|
8827
|
+
return button_id
|
|
8828
|
+
return None
|
|
8829
|
+
|
|
8830
|
+
|
|
8831
|
+
def _normalize_expected_view_buttons_for_compare(
|
|
8832
|
+
value: Any,
|
|
8833
|
+
*,
|
|
8834
|
+
custom_button_details_by_id: dict[int, dict[str, Any]] | None = None,
|
|
8835
|
+
) -> list[dict[str, Any]]:
|
|
8836
|
+
normalized_entries = _normalize_view_buttons_for_compare(value)
|
|
8837
|
+
if not custom_button_details_by_id:
|
|
8838
|
+
return normalized_entries
|
|
8839
|
+
enriched_entries: list[dict[str, Any]] = []
|
|
8840
|
+
for item in normalized_entries:
|
|
8841
|
+
enriched = deepcopy(item)
|
|
8842
|
+
if enriched.get("button_type") == "CUSTOM":
|
|
8843
|
+
button_id = _coerce_positive_int(enriched.get("button_id"))
|
|
8844
|
+
detail = custom_button_details_by_id.get(button_id or -1)
|
|
8845
|
+
if isinstance(detail, dict):
|
|
8846
|
+
for key in (
|
|
8847
|
+
"button_text",
|
|
8848
|
+
"button_icon",
|
|
8849
|
+
"icon_color",
|
|
8850
|
+
"background_color",
|
|
8851
|
+
"text_color",
|
|
8852
|
+
"trigger_action",
|
|
8853
|
+
):
|
|
8854
|
+
value = detail.get(key)
|
|
8855
|
+
if enriched.get(key) in {None, ""} and value not in {None, ""}:
|
|
8856
|
+
enriched[key] = deepcopy(value)
|
|
8857
|
+
enriched_entries.append(enriched)
|
|
8858
|
+
return enriched_entries
|
|
8859
|
+
|
|
8860
|
+
|
|
8861
|
+
def _partition_view_button_summaries(
|
|
8862
|
+
items: list[dict[str, Any]],
|
|
8863
|
+
) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]:
|
|
8864
|
+
system_buttons: list[dict[str, Any]] = []
|
|
8865
|
+
custom_buttons: list[dict[str, Any]] = []
|
|
8866
|
+
other_buttons: list[dict[str, Any]] = []
|
|
8867
|
+
for item in items:
|
|
8868
|
+
button_type = str(item.get("button_type") or "").strip().upper()
|
|
8869
|
+
if button_type == "SYSTEM":
|
|
8870
|
+
system_buttons.append(item)
|
|
8871
|
+
elif button_type == "CUSTOM":
|
|
8872
|
+
custom_buttons.append(item)
|
|
8873
|
+
else:
|
|
8874
|
+
other_buttons.append(item)
|
|
8875
|
+
return system_buttons, custom_buttons, other_buttons
|
|
8876
|
+
|
|
8877
|
+
|
|
8878
|
+
def _compare_view_button_summaries(
|
|
8879
|
+
*,
|
|
8880
|
+
expected: list[dict[str, Any]],
|
|
8881
|
+
actual: list[dict[str, Any]],
|
|
8882
|
+
) -> dict[str, Any]:
|
|
8883
|
+
if actual == expected:
|
|
8884
|
+
return {
|
|
8885
|
+
"verified": True,
|
|
8886
|
+
"custom_button_readback_pending": False,
|
|
8887
|
+
"pending_custom_buttons": [],
|
|
8888
|
+
}
|
|
8889
|
+
expected_system, expected_custom, expected_other = _partition_view_button_summaries(expected)
|
|
8890
|
+
actual_system, actual_custom, actual_other = _partition_view_button_summaries(actual)
|
|
8891
|
+
custom_button_readback_pending = (
|
|
8892
|
+
bool(expected_custom)
|
|
8893
|
+
and not actual_custom
|
|
8894
|
+
and actual_system == expected_system
|
|
8895
|
+
and actual_other == expected_other
|
|
8896
|
+
)
|
|
8897
|
+
return {
|
|
8898
|
+
"verified": custom_button_readback_pending,
|
|
8899
|
+
"custom_button_readback_pending": custom_button_readback_pending,
|
|
8900
|
+
"pending_custom_buttons": deepcopy(expected_custom) if custom_button_readback_pending else [],
|
|
8901
|
+
}
|
|
8902
|
+
|
|
8903
|
+
|
|
8390
8904
|
def _serialize_existing_view_button_entry(entry: dict[str, Any]) -> dict[str, Any]:
|
|
8391
8905
|
dto: dict[str, Any] = {}
|
|
8906
|
+
button_type = _normalize_view_button_type(entry.get("buttonType") or entry.get("button_type"))
|
|
8392
8907
|
button_id = _coerce_positive_int(entry.get("buttonId") or entry.get("button_id") or entry.get("id"))
|
|
8908
|
+
if button_type == "SYSTEM":
|
|
8909
|
+
button_id = _resolve_system_view_button_logical_id(entry)
|
|
8393
8910
|
if button_id is not None:
|
|
8394
8911
|
dto["buttonId"] = button_id
|
|
8395
|
-
button_type = _normalize_view_button_type(entry.get("buttonType") or entry.get("button_type"))
|
|
8396
8912
|
if button_type is not None:
|
|
8397
8913
|
dto["buttonType"] = button_type
|
|
8398
8914
|
config_type = _normalize_view_button_config_type(entry.get("configType") or entry.get("config_type"))
|
|
@@ -8405,6 +8921,7 @@ def _serialize_existing_view_button_entry(entry: dict[str, Any]) -> dict[str, An
|
|
|
8405
8921
|
dto["printTpls"] = _serialize_print_tpl_ids(entry.get("printTpls"))
|
|
8406
8922
|
for source_key, target_key in (
|
|
8407
8923
|
("buttonText", "buttonText"),
|
|
8924
|
+
("defaultButtonText", "defaultButtonText"),
|
|
8408
8925
|
("buttonIcon", "buttonIcon"),
|
|
8409
8926
|
("backgroundColor", "backgroundColor"),
|
|
8410
8927
|
("textColor", "textColor"),
|
|
@@ -8423,10 +8940,9 @@ def _serialize_existing_view_button_entry(entry: dict[str, Any]) -> dict[str, An
|
|
|
8423
8940
|
def _extract_existing_view_button_dtos(config: dict[str, Any]) -> list[dict[str, Any]]:
|
|
8424
8941
|
if not isinstance(config, dict):
|
|
8425
8942
|
return []
|
|
8426
|
-
|
|
8427
|
-
if
|
|
8428
|
-
return [deepcopy(item) for item in
|
|
8429
|
-
entries, _ = _extract_view_button_entries(config)
|
|
8943
|
+
entries, source = _extract_view_button_entries(config)
|
|
8944
|
+
if source == "buttonConfigDTOList":
|
|
8945
|
+
return [deepcopy(item) for item in entries if isinstance(item, dict)]
|
|
8430
8946
|
return [_serialize_existing_view_button_entry(entry) for entry in entries if isinstance(entry, dict)]
|
|
8431
8947
|
|
|
8432
8948
|
|
|
@@ -8510,9 +9026,11 @@ def _serialize_view_button_binding(
|
|
|
8510
9026
|
"buttonFormulaType": binding.button_formula_type,
|
|
8511
9027
|
"printTpls": _serialize_print_tpl_ids(binding.print_tpls),
|
|
8512
9028
|
}
|
|
8513
|
-
if binding.button_type
|
|
9029
|
+
if binding.button_type in {PublicViewButtonType.system, PublicViewButtonType.custom}:
|
|
8514
9030
|
dto["buttonText"] = binding.button_text
|
|
8515
9031
|
dto["buttonIcon"] = binding.button_icon
|
|
9032
|
+
if str(binding.icon_color or "").strip():
|
|
9033
|
+
dto["iconColor"] = binding.icon_color
|
|
8516
9034
|
dto["backgroundColor"] = binding.background_color
|
|
8517
9035
|
dto["textColor"] = binding.text_color
|
|
8518
9036
|
dto["triggerAction"] = binding.trigger_action
|
|
@@ -9171,6 +9689,22 @@ def _build_view_update_payload(
|
|
|
9171
9689
|
)
|
|
9172
9690
|
|
|
9173
9691
|
|
|
9692
|
+
_KNOWN_SYSTEM_VIEW_COLUMNS = {
|
|
9693
|
+
"编号",
|
|
9694
|
+
"当前流程状态",
|
|
9695
|
+
"申请人",
|
|
9696
|
+
"申请时间",
|
|
9697
|
+
"更新时间",
|
|
9698
|
+
"流程标题",
|
|
9699
|
+
"当前处理人",
|
|
9700
|
+
"当前处理节点",
|
|
9701
|
+
}
|
|
9702
|
+
|
|
9703
|
+
|
|
9704
|
+
def _filter_known_system_view_columns(columns: list[str]) -> list[str]:
|
|
9705
|
+
return [name for name in columns if str(name or "").strip() and str(name).strip() not in _KNOWN_SYSTEM_VIEW_COLUMNS]
|
|
9706
|
+
|
|
9707
|
+
|
|
9174
9708
|
def _build_minimal_view_payload(
|
|
9175
9709
|
*,
|
|
9176
9710
|
app_key: str,
|
|
@@ -9299,7 +9833,7 @@ def _resolve_view_visible_field_names(patch: ViewUpsertPatch) -> list[str]:
|
|
|
9299
9833
|
ordered: list[str] = []
|
|
9300
9834
|
for value in [*patch.columns, patch.title_field, patch.start_field, patch.end_field, patch.group_by]:
|
|
9301
9835
|
name = str(value or "").strip()
|
|
9302
|
-
if name and name not in ordered:
|
|
9836
|
+
if name and name not in _KNOWN_SYSTEM_VIEW_COLUMNS and name not in ordered:
|
|
9303
9837
|
ordered.append(name)
|
|
9304
9838
|
return ordered
|
|
9305
9839
|
|