@qingflow-tech/qingflow-cli 1.1.50-beta.2 → 1.1.50-beta.3
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 +3 -3
- package/package.json +1 -1
- package/pyproject.toml +7 -2
- package/skills/qingflow-cli/SKILL.md +74 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md +3 -9
- package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +1 -0
- package/skills/qingflow-cli/reference/builder/50-views.md +176 -190
- package/skills/qingflow-cli/reference/builder/README.md +1 -1
- package/skills/qingflow-cli/reference/builder/reference/app-delivery-sop.md +3 -3
- package/skills/qingflow-cli/reference/examples/README.md +1 -1
- package/skills/qingflow-cli/reference/examples/views/views_apply_v1alpha1.example.json +14 -0
- package/skills/qingflow-cli/reference/examples/views/views_upsert_table_minimal.example.json +30 -36
- package/src/qingflow_mcp/builder_facade/models.py +208 -17
- package/src/qingflow_mcp/builder_facade/schemas/views/manifest.json +43 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/complete.json +1193 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/button-config.json +53 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/data-permission.json +60 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/display.json +71 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/operation-permission.json +48 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/query-condition.json +47 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/summary.json +274 -0
- package/src/qingflow_mcp/builder_facade/service.py +1427 -410
- package/src/qingflow_mcp/builder_facade/views_extract.py +572 -0
- package/src/qingflow_mcp/builder_facade/views_schema.py +400 -0
- package/src/qingflow_mcp/cli/commands/builder.py +72 -53
- package/src/qingflow_mcp/public_surface.py +3 -0
- package/src/qingflow_mcp/response_trim.py +11 -2
- package/src/qingflow_mcp/server_app_builder.py +32 -8
- package/src/qingflow_mcp/solution/executor.py +93 -7
- package/src/qingflow_mcp/tools/ai_builder_tools.py +211 -393
- package/src/qingflow_mcp/tools/solution_tools.py +2 -0
- package/src/qingflow_mcp/tools/view_tools.py +0 -6
- package/src/qingflow_mcp/tools/view_writer.py +44 -0
- package/skills/qingflow-cli/reference/examples/views/views_batch_full.example.json +0 -159
- package/skills/qingflow-cli/reference/views_upsert_table_minimal.example.json +0 -42
|
@@ -15,6 +15,8 @@ from typing import Any, NoReturn, cast
|
|
|
15
15
|
from urllib.parse import quote_plus, unquote_plus
|
|
16
16
|
from uuid import uuid4
|
|
17
17
|
|
|
18
|
+
from pydantic import ValidationError
|
|
19
|
+
|
|
18
20
|
from ..backend_client import BackendRequestContext
|
|
19
21
|
from ..config import get_flow_edit_maintenance_threshold
|
|
20
22
|
from ..errors import QingflowApiError, backend_code_int, backend_code_value_int, is_auth_like_error
|
|
@@ -26,7 +28,7 @@ from ..solution.compiler.chart_compiler import qingbi_workspace_visible_auth
|
|
|
26
28
|
from ..solution.compiler.form_compiler import build_question, default_form_payload, default_member_auth
|
|
27
29
|
from ..solution.compiler.icon_utils import encode_workspace_icon_with_defaults, workspace_icon_config
|
|
28
30
|
from ..solution.compiler.view_compiler import VIEW_TYPE_MAP
|
|
29
|
-
from ..solution.executor import _build_viewgraph_questions, _compact_dict, extract_field_map
|
|
31
|
+
from ..solution.executor import _build_viewgraph_questions, _compact_dict, _flatten_viewgraph_questions, extract_field_map
|
|
30
32
|
from ..solution.spec_models import FieldType, FormLayoutRowSpec, FormLayoutSectionSpec, ViewSpec
|
|
31
33
|
from ..tools.app_tools import AppTools
|
|
32
34
|
from ..tools.custom_button_tools import CustomButtonTools
|
|
@@ -41,6 +43,8 @@ from ..tools.workflow_tools import WorkflowTools
|
|
|
41
43
|
from . import flow_validation
|
|
42
44
|
from . import workflow_spec as workflow_spec_api
|
|
43
45
|
from .button_style_catalog import button_style_catalog_payload
|
|
46
|
+
from ..tools.view_writer import ViewWriter
|
|
47
|
+
from .views_schema import ViewsContractError, compact_none_values, views_schema_registry
|
|
44
48
|
from .models import (
|
|
45
49
|
AppChartsReadResponse,
|
|
46
50
|
AppFieldsReadResponse,
|
|
@@ -290,6 +294,7 @@ class AiBuilderFacade:
|
|
|
290
294
|
self.buttons = buttons
|
|
291
295
|
self.packages = packages
|
|
292
296
|
self.views = views
|
|
297
|
+
self._view_writer = ViewWriter(views.sessions, views.backend)
|
|
293
298
|
self.workflows = workflows
|
|
294
299
|
self.portals = portals
|
|
295
300
|
self.charts = charts
|
|
@@ -3072,14 +3077,27 @@ class AiBuilderFacade:
|
|
|
3072
3077
|
member_ids=visibility.external_selectors.member_ids,
|
|
3073
3078
|
member_emails=visibility.external_selectors.member_emails,
|
|
3074
3079
|
)
|
|
3075
|
-
|
|
3076
|
-
|
|
3080
|
+
# 冗余引用降级:提取器对同一成员同时产出 uid/email/name 三通道(uid 必有且直通解析,
|
|
3081
|
+
# 保证成员不丢);email/name 走后端通讯录搜索,受数据/权限边界影响解析可能失败。
|
|
3082
|
+
# 这类失败降级为非阻断——只有 uid/dept_id 直通也覆盖不了的引用才真正阻断。
|
|
3083
|
+
_SOFT_RESOLUTION_KINDS = {"member_email", "member_name", "department_name", "role", "external_member_email"}
|
|
3084
|
+
_NOT_FOUND_RESOLUTION_CODES = {"MEMBER_NOT_FOUND", "AMBIGUOUS_MEMBER", "ROLE_NOT_FOUND", "AMBIGUOUS_ROLE", "DEPARTMENT_NOT_FOUND", "AMBIGUOUS_DEPARTMENT"}
|
|
3085
|
+
blocking_issues = [
|
|
3086
|
+
issue
|
|
3087
|
+
for issue in [
|
|
3077
3088
|
*member_resolution["issues"],
|
|
3078
3089
|
*role_resolution["issues"],
|
|
3079
3090
|
*department_resolution["issues"],
|
|
3080
3091
|
*external_member_resolution["issues"],
|
|
3081
3092
|
]
|
|
3082
|
-
|
|
3093
|
+
# 冗余 email/name 引用「找不到」降级为非阻断(uid/id 直通已保主体不丢);
|
|
3094
|
+
# 权限拒绝/查询失败类(LOOKUP_FAILED/PERMISSION_DENIED)仍阻断
|
|
3095
|
+
if not (
|
|
3096
|
+
str(issue.get("kind") or "") in _SOFT_RESOLUTION_KINDS
|
|
3097
|
+
and str(issue.get("error_code") or "") in _NOT_FOUND_RESOLUTION_CODES
|
|
3098
|
+
)
|
|
3099
|
+
]
|
|
3100
|
+
self._raise_visibility_resolution_issues(blocking_issues)
|
|
3083
3101
|
|
|
3084
3102
|
external_depart_entries: list[dict[str, Any]] = []
|
|
3085
3103
|
for dept_id in visibility.external_selectors.dept_ids:
|
|
@@ -3220,7 +3238,7 @@ class AiBuilderFacade:
|
|
|
3220
3238
|
}
|
|
3221
3239
|
)
|
|
3222
3240
|
condition_matrix, condition_issues = _build_flow_condition_matrix(
|
|
3223
|
-
|
|
3241
|
+
fields_by_que_id=field_name_to_field,
|
|
3224
3242
|
node=normalized_node,
|
|
3225
3243
|
)
|
|
3226
3244
|
issues.extend({**issue, "node_id": node.get("id")} for issue in condition_issues)
|
|
@@ -8430,11 +8448,7 @@ class AiBuilderFacade:
|
|
|
8430
8448
|
try:
|
|
8431
8449
|
schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
8432
8450
|
parsed_schema = _parse_schema(schema)
|
|
8433
|
-
|
|
8434
|
-
str(field.get("name") or ""): field
|
|
8435
|
-
for field in parsed_schema.get("fields") or []
|
|
8436
|
-
if isinstance(field, dict) and str(field.get("name") or "")
|
|
8437
|
-
}
|
|
8451
|
+
fields_by_que_id = _collect_fields_by_que_id([], schema=schema)
|
|
8438
8452
|
existing_views, _views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=False)
|
|
8439
8453
|
except (QingflowApiError, RuntimeError) as error:
|
|
8440
8454
|
api_error = _coerce_api_error(error)
|
|
@@ -8525,7 +8539,7 @@ class AiBuilderFacade:
|
|
|
8525
8539
|
view_binding = _custom_button_view_binding_to_view_button_patch(binding=binding, button_id=button_id)
|
|
8526
8540
|
dto, binding_issues = _serialize_view_button_binding(
|
|
8527
8541
|
binding=view_binding,
|
|
8528
|
-
|
|
8542
|
+
fields_by_que_id=fields_by_que_id,
|
|
8529
8543
|
valid_custom_button_ids=valid_custom_button_ids,
|
|
8530
8544
|
allow_unverified_custom_button_id=allow_unverified_numeric_button_ids,
|
|
8531
8545
|
)
|
|
@@ -9455,49 +9469,37 @@ class AiBuilderFacade:
|
|
|
9455
9469
|
return fields_result
|
|
9456
9470
|
current_fields = fields_result.get("fields", [])
|
|
9457
9471
|
field_names = {str(field.get("name") or "") for field in current_fields}
|
|
9458
|
-
|
|
9459
|
-
str(field.get("name") or ""): field
|
|
9460
|
-
for field in current_fields
|
|
9461
|
-
if isinstance(field, dict) and str(field.get("name") or "")
|
|
9462
|
-
}
|
|
9472
|
+
fields_by_que_id = _collect_fields_by_que_id(current_fields)
|
|
9463
9473
|
upsert_views = [public_view_upsert_payload(view) for view in request.upsert_views]
|
|
9464
9474
|
patch_views = [public_view_partial_payload(patch) for patch in request.patch_views]
|
|
9465
9475
|
if request.preset is not None:
|
|
9466
9476
|
upsert_views = _build_views_preset(request.preset, list(field_names))
|
|
9467
9477
|
blocking_issues: list[dict[str, Any]] = []
|
|
9468
9478
|
for patch in upsert_views:
|
|
9469
|
-
|
|
9470
|
-
|
|
9471
|
-
|
|
9472
|
-
|
|
9473
|
-
{
|
|
9474
|
-
"error_code": "VALIDATION_ERROR",
|
|
9475
|
-
"view_name": patch.get("name"),
|
|
9476
|
-
"message": "view columns must include at least one real app field; system columns cannot be applied directly",
|
|
9477
|
-
"ignored_system_columns": [name for name in raw_columns if name in _KNOWN_SYSTEM_VIEW_COLUMNS],
|
|
9478
|
-
}
|
|
9479
|
-
)
|
|
9480
|
-
missing_columns = [name for name in columns if name not in field_names]
|
|
9479
|
+
# queId 形态:columns 是 int 数组,系统列 queId<=0 天然排除
|
|
9480
|
+
raw_columns = [que for que in (patch.get("columns") or []) if isinstance(que, int) and que > 0]
|
|
9481
|
+
columns = raw_columns
|
|
9482
|
+
missing_columns = [que for que in columns if que not in fields_by_que_id]
|
|
9481
9483
|
if missing_columns:
|
|
9482
9484
|
blocking_issues.append({"error_code": "UNKNOWN_VIEW_FIELD", "view_name": patch.get("name"), "missing_fields": missing_columns})
|
|
9483
|
-
|
|
9484
|
-
if
|
|
9485
|
-
blocking_issues.append({"error_code": "UNKNOWN_VIEW_FIELD", "view_name": patch.get("name"), "missing_fields": [
|
|
9486
|
-
|
|
9487
|
-
|
|
9488
|
-
|
|
9485
|
+
group_que_id = patch.get("group_que_id")
|
|
9486
|
+
if group_que_id and group_que_id not in fields_by_que_id:
|
|
9487
|
+
blocking_issues.append({"error_code": "UNKNOWN_VIEW_FIELD", "view_name": patch.get("name"), "missing_fields": [group_que_id]})
|
|
9488
|
+
start_que_id = patch.get("start_que_id")
|
|
9489
|
+
end_que_id = patch.get("end_que_id")
|
|
9490
|
+
title_que_id = patch.get("title_que_id")
|
|
9489
9491
|
if patch.get("type") == "gantt":
|
|
9490
9492
|
missing_required = []
|
|
9491
|
-
if not
|
|
9492
|
-
missing_required.append("
|
|
9493
|
-
if not
|
|
9494
|
-
missing_required.append("
|
|
9493
|
+
if not start_que_id:
|
|
9494
|
+
missing_required.append("start_que_id")
|
|
9495
|
+
if not end_que_id:
|
|
9496
|
+
missing_required.append("end_que_id")
|
|
9495
9497
|
if missing_required:
|
|
9496
9498
|
blocking_issues.append({"error_code": "INVALID_GANTT_CONFIG", "view_name": patch.get("name"), "missing_fields": missing_required})
|
|
9497
|
-
missing_gantt_fields = [
|
|
9499
|
+
missing_gantt_fields = [que for que in (start_que_id, end_que_id, title_que_id) if que and que not in fields_by_que_id]
|
|
9498
9500
|
if missing_gantt_fields:
|
|
9499
9501
|
blocking_issues.append({"error_code": "UNKNOWN_VIEW_FIELD", "view_name": patch.get("name"), "missing_fields": missing_gantt_fields})
|
|
9500
|
-
translated_filters, filter_issues = _build_view_filter_groups(
|
|
9502
|
+
translated_filters, filter_issues = _build_view_filter_groups(fields_by_que_id=fields_by_que_id, filters=patch.get("filters") or [])
|
|
9501
9503
|
if filter_issues:
|
|
9502
9504
|
blocking_issues.extend(
|
|
9503
9505
|
{
|
|
@@ -9509,7 +9511,7 @@ class AiBuilderFacade:
|
|
|
9509
9511
|
if translated_filters:
|
|
9510
9512
|
patch["filters"] = [dict(rule) for rule in (patch.get("filters") or [])]
|
|
9511
9513
|
_, _, query_condition_issues = _build_view_query_conditions_payload(
|
|
9512
|
-
|
|
9514
|
+
fields_by_que_id=fields_by_que_id,
|
|
9513
9515
|
query_conditions=patch.get("query_conditions"),
|
|
9514
9516
|
)
|
|
9515
9517
|
if query_condition_issues:
|
|
@@ -11796,6 +11798,26 @@ class AiBuilderFacade:
|
|
|
11796
11798
|
result["retry_payload"] = self._view_action_buttons_retry_payload(profile=profile, app_key=app_key, intents=retry_intents) if retry_intents else None
|
|
11797
11799
|
return result
|
|
11798
11800
|
|
|
11801
|
+
def _apply_view_create(self, *, profile: str, payload: JSONObject) -> str | None:
|
|
11802
|
+
"""创建一个视图并从后端响应中提取 view key。"""
|
|
11803
|
+
create_result = self._view_writer.create_view(profile=profile, payload=payload)
|
|
11804
|
+
raw_created = create_result.get("result")
|
|
11805
|
+
if isinstance(raw_created, dict):
|
|
11806
|
+
raw_view_key = raw_created.get("viewgraphKey") or raw_created.get("viewKey")
|
|
11807
|
+
return str(raw_view_key).strip() if raw_view_key is not None and str(raw_view_key).strip() else None
|
|
11808
|
+
if isinstance(raw_created, str):
|
|
11809
|
+
return raw_created.strip() or None
|
|
11810
|
+
return None
|
|
11811
|
+
|
|
11812
|
+
def _apply_view_update(self, *, profile: str, view_key: str, payload: JSONObject) -> None:
|
|
11813
|
+
"""更新一个已有视图。"""
|
|
11814
|
+
self.views.view_update(profile=profile, viewgraph_key=view_key, payload=payload)
|
|
11815
|
+
|
|
11816
|
+
def _apply_view_remove(self, *, profile: str, view_key: str) -> JSONObject:
|
|
11817
|
+
"""删除一个视图并执行单 view key 读回验证。"""
|
|
11818
|
+
self.views.view_delete(profile=profile, viewgraph_key=view_key)
|
|
11819
|
+
return self._verify_view_deleted_by_key(profile=profile, view_key=view_key)
|
|
11820
|
+
|
|
11799
11821
|
def app_views_apply(
|
|
11800
11822
|
self,
|
|
11801
11823
|
*,
|
|
@@ -11805,15 +11827,91 @@ class AiBuilderFacade:
|
|
|
11805
11827
|
patch_views: list[ViewPartialPatch] | None = None,
|
|
11806
11828
|
remove_views: list[str],
|
|
11807
11829
|
publish: bool = True,
|
|
11830
|
+
schema_version: str | None = None,
|
|
11808
11831
|
) -> JSONObject:
|
|
11809
11832
|
patch_views = patch_views or []
|
|
11833
|
+
operation_values = (
|
|
11834
|
+
("upsert", "upsert_views", upsert_views, "VIEW_BATCH_CREATE_UNSUPPORTED"),
|
|
11835
|
+
("patch", "patch_views", patch_views, "VIEW_BATCH_OPERATION_UNSUPPORTED"),
|
|
11836
|
+
("remove", "remove_views", remove_views, "VIEW_BATCH_OPERATION_UNSUPPORTED"),
|
|
11837
|
+
)
|
|
11838
|
+
selected_operation_types = [operation for operation, _field_name, values, _error_code in operation_values if values]
|
|
11839
|
+
selected_operations = [field_name for _operation, field_name, values, _error_code in operation_values if values]
|
|
11810
11840
|
normalized_args = {
|
|
11811
11841
|
"app_key": app_key,
|
|
11812
|
-
"
|
|
11842
|
+
"upsert_view": public_view_upsert_payload(upsert_views[0]) if upsert_views else None,
|
|
11813
11843
|
"patch_views": [public_view_partial_payload(patch) for patch in patch_views],
|
|
11814
11844
|
"remove_views": list(remove_views),
|
|
11815
11845
|
"publish": publish,
|
|
11846
|
+
"schema_version": schema_version,
|
|
11816
11847
|
}
|
|
11848
|
+
if len(selected_operations) > 1:
|
|
11849
|
+
return _failed(
|
|
11850
|
+
"VIEW_SINGLE_OPERATION_UNSUPPORTED",
|
|
11851
|
+
"app_views_apply supports one view operation per call; choose upsert_view, patch_views, or remove_views",
|
|
11852
|
+
normalized_args=normalized_args,
|
|
11853
|
+
details={"app_key": app_key, "operations": selected_operations},
|
|
11854
|
+
suggested_next_call=None,
|
|
11855
|
+
)
|
|
11856
|
+
operation = selected_operation_types[0] if selected_operation_types else None
|
|
11857
|
+
for operation_name, _field_name, values, error_code in operation_values:
|
|
11858
|
+
if len(values) <= 1:
|
|
11859
|
+
continue
|
|
11860
|
+
first_value = values[0]
|
|
11861
|
+
if operation_name == "upsert":
|
|
11862
|
+
suggested_value: Any = public_view_upsert_payload(first_value)
|
|
11863
|
+
elif operation_name == "patch":
|
|
11864
|
+
suggested_value = [public_view_partial_payload(first_value)]
|
|
11865
|
+
else:
|
|
11866
|
+
suggested_value = [str(first_value)]
|
|
11867
|
+
argument_name = "upsert_view" if operation_name == "upsert" else f"{operation_name}_views"
|
|
11868
|
+
suggested_arguments: JSONObject = {
|
|
11869
|
+
"profile": profile,
|
|
11870
|
+
"app_key": app_key,
|
|
11871
|
+
argument_name: suggested_value,
|
|
11872
|
+
"publish": publish,
|
|
11873
|
+
}
|
|
11874
|
+
if schema_version is not None:
|
|
11875
|
+
suggested_arguments["schema_version"] = schema_version
|
|
11876
|
+
return _failed(
|
|
11877
|
+
error_code,
|
|
11878
|
+
(
|
|
11879
|
+
"app_views_apply supports creating or fully updating one view per call"
|
|
11880
|
+
if operation_name == "upsert"
|
|
11881
|
+
else f"app_views_apply supports at most one {operation_name} per call"
|
|
11882
|
+
),
|
|
11883
|
+
normalized_args=normalized_args,
|
|
11884
|
+
details={
|
|
11885
|
+
"app_key": app_key,
|
|
11886
|
+
f"max_{operation_name}_views": 1,
|
|
11887
|
+
f"received_{operation_name}_views": len(values),
|
|
11888
|
+
},
|
|
11889
|
+
suggested_next_call={
|
|
11890
|
+
"tool_name": "app_views_apply",
|
|
11891
|
+
"arguments": suggested_arguments,
|
|
11892
|
+
},
|
|
11893
|
+
)
|
|
11894
|
+
schema_context = None
|
|
11895
|
+
if schema_version is not None:
|
|
11896
|
+
try:
|
|
11897
|
+
schema_context = views_schema_registry().resolve_schema_version(schema_version)
|
|
11898
|
+
for upsert in upsert_views:
|
|
11899
|
+
schema_context.validate_upsert(
|
|
11900
|
+
compact_none_values(public_view_upsert_payload(upsert))
|
|
11901
|
+
)
|
|
11902
|
+
except ViewsContractError as error:
|
|
11903
|
+
return _failed(
|
|
11904
|
+
error.error_code,
|
|
11905
|
+
str(error.message),
|
|
11906
|
+
normalized_args=normalized_args,
|
|
11907
|
+
details={
|
|
11908
|
+
"app_key": app_key,
|
|
11909
|
+
"schema_version": schema_version,
|
|
11910
|
+
"reason_path": error.path or "",
|
|
11911
|
+
**(error.details or {}),
|
|
11912
|
+
},
|
|
11913
|
+
suggested_next_call={"tool_name": "app_views_schema", "arguments": {"schema_version": schema_version, "schema_kind": "complete"}},
|
|
11914
|
+
)
|
|
11817
11915
|
apply_started_at = time.perf_counter()
|
|
11818
11916
|
permission_ms = 0
|
|
11819
11917
|
view_state_read_ms = 0
|
|
@@ -11828,6 +11926,8 @@ class AiBuilderFacade:
|
|
|
11828
11926
|
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
11829
11927
|
|
|
11830
11928
|
def finalize(response: JSONObject) -> JSONObject:
|
|
11929
|
+
if schema_context is not None:
|
|
11930
|
+
response["schemaVersion"] = schema_context.schema_version
|
|
11831
11931
|
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
11832
11932
|
|
|
11833
11933
|
def attach_duration(response: JSONObject) -> JSONObject:
|
|
@@ -12044,7 +12144,25 @@ class AiBuilderFacade:
|
|
|
12044
12144
|
return finish(create_permission_outcome.block)
|
|
12045
12145
|
permission_outcomes.append(create_permission_outcome)
|
|
12046
12146
|
parsed_schema = _parse_schema(schema)
|
|
12047
|
-
|
|
12147
|
+
|
|
12148
|
+
def _register_subfield_names(field: dict[str, Any], index: dict[str, dict[str, Any]]) -> None:
|
|
12149
|
+
# 表格字段(queType 18)的子字段是视图列/筛选/排序的合法引用面(viewgraph_question 有独立行);
|
|
12150
|
+
# _parse_schema 只把顶层字段入列,这里递归并入引用索引(不改 fields 布局面)
|
|
12151
|
+
for sub in field.get("subfields") or []:
|
|
12152
|
+
if isinstance(sub, dict) and str(sub.get("name") or "").strip():
|
|
12153
|
+
index.setdefault(str(sub["name"]), sub)
|
|
12154
|
+
_register_subfield_names(sub, index)
|
|
12155
|
+
|
|
12156
|
+
field_name_to_field: dict[str, dict[str, Any]] = {}
|
|
12157
|
+
for field in parsed_schema["fields"]:
|
|
12158
|
+
if isinstance(field, dict) and str(field.get("name") or "").strip():
|
|
12159
|
+
field_name_to_field[str(field["name"])] = field
|
|
12160
|
+
_register_subfield_names(field, field_name_to_field)
|
|
12161
|
+
field_names = set(field_name_to_field)
|
|
12162
|
+
# views 契约全 queId 化后,列/筛选/排序/配置引用的是 queId(int);
|
|
12163
|
+
# fields_by_que_id 必须以 que_id 为键,与 _build_view_filter_groups 等消费方对齐。
|
|
12164
|
+
# 递归收集(顶层+表格子字段;段落内字段经 _parse_schema 已展平到 fields)。
|
|
12165
|
+
fields_by_que_id = _collect_fields_by_que_id([], schema=schema)
|
|
12048
12166
|
if patch_views:
|
|
12049
12167
|
expanded_views, patch_issues, patch_results = self._expand_view_partial_patches(
|
|
12050
12168
|
profile=profile,
|
|
@@ -12064,14 +12182,18 @@ class AiBuilderFacade:
|
|
|
12064
12182
|
suggested_next_call={"tool_name": "view_get", "arguments": {"profile": profile, "view_key": patch_issues[0].get("view_key") or "VIEW_KEY"}},
|
|
12065
12183
|
)
|
|
12066
12184
|
)
|
|
12067
|
-
|
|
12068
|
-
|
|
12185
|
+
if len(expanded_views) > 1:
|
|
12186
|
+
return finalize(_failed(
|
|
12187
|
+
"VIEW_BATCH_OPERATION_UNSUPPORTED",
|
|
12188
|
+
"app_views_apply supports hydrating one patch view per call",
|
|
12189
|
+
normalized_args=normalized_args,
|
|
12190
|
+
details={"app_key": app_key, "received_patch_views": len(expanded_views)},
|
|
12191
|
+
suggested_next_call=None,
|
|
12192
|
+
))
|
|
12193
|
+
upsert_views = expanded_views
|
|
12194
|
+
operation = "upsert" if expanded_views else "patch"
|
|
12195
|
+
normalized_args["upsert_view"] = public_view_upsert_payload(expanded_views[0]) if expanded_views else None
|
|
12069
12196
|
normalized_args["patch_results"] = patch_results
|
|
12070
|
-
current_fields_by_name = {
|
|
12071
|
-
str(field.get("name") or ""): field
|
|
12072
|
-
for field in parsed_schema["fields"]
|
|
12073
|
-
if isinstance(field, dict) and str(field.get("name") or "")
|
|
12074
|
-
}
|
|
12075
12197
|
requires_custom_button_validation = any(
|
|
12076
12198
|
any(binding.button_type == PublicViewButtonType.custom for binding in (patch.buttons or []))
|
|
12077
12199
|
for patch in upsert_views
|
|
@@ -12167,8 +12289,8 @@ class AiBuilderFacade:
|
|
|
12167
12289
|
associated_resource_inventory_ms += _duration_ms(associated_resource_inventory_started_at)
|
|
12168
12290
|
removed: list[str] = []
|
|
12169
12291
|
removed_keys: set[str] = set()
|
|
12170
|
-
|
|
12171
|
-
|
|
12292
|
+
view_result: dict[str, Any] | None = None
|
|
12293
|
+
failure_result: dict[str, Any] | None = None
|
|
12172
12294
|
action_button_intents: list[dict[str, Any]] = list(patch_action_button_intents)
|
|
12173
12295
|
|
|
12174
12296
|
def record_action_button_intent(*, patch: ViewUpsertPatch, view_key: str | None, index: int) -> None:
|
|
@@ -12186,10 +12308,17 @@ class AiBuilderFacade:
|
|
|
12186
12308
|
)
|
|
12187
12309
|
|
|
12188
12310
|
view_mutation_started_at = time.perf_counter()
|
|
12189
|
-
|
|
12190
|
-
selector_text = str(
|
|
12311
|
+
if operation == "remove" and remove_views:
|
|
12312
|
+
selector_text = str(remove_views[0] or "").strip()
|
|
12191
12313
|
if not selector_text:
|
|
12192
|
-
|
|
12314
|
+
view_mutation_ms += _duration_ms(view_mutation_started_at)
|
|
12315
|
+
return finish(_failed(
|
|
12316
|
+
"VIEW_NOT_FOUND",
|
|
12317
|
+
"remove_views item did not match an existing view name or view_key",
|
|
12318
|
+
normalized_args=normalized_args,
|
|
12319
|
+
details={"app_key": app_key},
|
|
12320
|
+
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
12321
|
+
))
|
|
12193
12322
|
key_match = existing_by_key.get(selector_text)
|
|
12194
12323
|
matches = [key_match] if isinstance(key_match, dict) else existing_by_name.get(selector_text, [])
|
|
12195
12324
|
if len(matches) > 1:
|
|
@@ -12216,44 +12345,40 @@ class AiBuilderFacade:
|
|
|
12216
12345
|
"error_code": "VIEW_NOT_FOUND",
|
|
12217
12346
|
"message": "remove_views item did not match an existing view name or view_key",
|
|
12218
12347
|
}
|
|
12219
|
-
|
|
12220
|
-
|
|
12221
|
-
|
|
12222
|
-
if len(matches) == 1:
|
|
12348
|
+
failure_result = failed_view
|
|
12349
|
+
view_result = deepcopy(failed_view)
|
|
12350
|
+
else:
|
|
12223
12351
|
key = _extract_view_key(matches[0])
|
|
12224
12352
|
removed_name = _extract_view_name(matches[0]) or selector_text
|
|
12225
12353
|
try:
|
|
12226
|
-
self.
|
|
12227
|
-
delete_readback = self._verify_view_deleted_by_key(profile=profile, view_key=key)
|
|
12354
|
+
delete_readback = self._apply_view_remove(profile=profile, view_key=key)
|
|
12228
12355
|
removed.append(removed_name)
|
|
12229
12356
|
if key:
|
|
12230
12357
|
removed_keys.add(key)
|
|
12231
12358
|
existing_by_key.pop(key, None)
|
|
12232
12359
|
existing_by_name.pop(removed_name, None)
|
|
12233
|
-
|
|
12234
|
-
|
|
12235
|
-
|
|
12236
|
-
|
|
12237
|
-
|
|
12238
|
-
|
|
12239
|
-
|
|
12240
|
-
|
|
12241
|
-
|
|
12242
|
-
|
|
12243
|
-
|
|
12244
|
-
|
|
12245
|
-
|
|
12246
|
-
|
|
12247
|
-
|
|
12248
|
-
|
|
12249
|
-
|
|
12250
|
-
|
|
12251
|
-
|
|
12252
|
-
|
|
12253
|
-
|
|
12254
|
-
|
|
12255
|
-
}
|
|
12256
|
-
)
|
|
12360
|
+
view_result = {
|
|
12361
|
+
"name": removed_name,
|
|
12362
|
+
"view_key": key,
|
|
12363
|
+
"type": None,
|
|
12364
|
+
"status": delete_readback.get("status") or "readback_pending",
|
|
12365
|
+
"operation": "delete",
|
|
12366
|
+
"delete_executed": True,
|
|
12367
|
+
"readback_status": delete_readback.get("readback_status"),
|
|
12368
|
+
"safe_to_retry_delete": False,
|
|
12369
|
+
**(
|
|
12370
|
+
{
|
|
12371
|
+
"error_code": delete_readback.get("error_code"),
|
|
12372
|
+
"message": delete_readback.get("message"),
|
|
12373
|
+
"request_id": delete_readback.get("request_id"),
|
|
12374
|
+
"backend_code": delete_readback.get("backend_code"),
|
|
12375
|
+
"http_status": delete_readback.get("http_status"),
|
|
12376
|
+
"transport_error": delete_readback.get("transport_error"),
|
|
12377
|
+
}
|
|
12378
|
+
if delete_readback.get("readback_status") != "deleted"
|
|
12379
|
+
else {}
|
|
12380
|
+
),
|
|
12381
|
+
}
|
|
12257
12382
|
except (QingflowApiError, RuntimeError) as error:
|
|
12258
12383
|
api_error = _coerce_api_error(error)
|
|
12259
12384
|
failed_view = {
|
|
@@ -12270,8 +12395,8 @@ class AiBuilderFacade:
|
|
|
12270
12395
|
"backend_code": api_error.backend_code,
|
|
12271
12396
|
"http_status": None if api_error.http_status == 404 else api_error.http_status,
|
|
12272
12397
|
}
|
|
12273
|
-
|
|
12274
|
-
|
|
12398
|
+
failure_result = failed_view
|
|
12399
|
+
view_result = deepcopy(failed_view)
|
|
12275
12400
|
created: list[str] = []
|
|
12276
12401
|
updated: list[str] = []
|
|
12277
12402
|
existing_view_list = [
|
|
@@ -12281,25 +12406,19 @@ class AiBuilderFacade:
|
|
|
12281
12406
|
and _extract_view_name(view) not in removed
|
|
12282
12407
|
and _extract_view_key(view) not in removed_keys
|
|
12283
12408
|
]
|
|
12284
|
-
for ordinal, patch in
|
|
12285
|
-
apply_columns =
|
|
12286
|
-
ignored_system_columns = [
|
|
12287
|
-
name
|
|
12288
|
-
for name in [str(value or "").strip() for value in patch.columns]
|
|
12289
|
-
if name in _KNOWN_SYSTEM_VIEW_COLUMNS
|
|
12290
|
-
]
|
|
12409
|
+
for ordinal, patch in ((1, upsert_views[0]),) if operation == "upsert" and upsert_views else ():
|
|
12410
|
+
apply_columns = _resolve_view_visible_que_ids(patch)
|
|
12291
12411
|
if patch.type in {PublicViewType.table, PublicViewType.card} and patch.columns and not apply_columns:
|
|
12292
12412
|
return _failed(
|
|
12293
12413
|
"VALIDATION_ERROR",
|
|
12294
12414
|
"view columns must include at least one real app field; system columns cannot be applied directly",
|
|
12295
12415
|
normalized_args=normalized_args,
|
|
12296
|
-
details={
|
|
12297
|
-
"app_key": app_key,
|
|
12298
|
-
"view_name": patch.name,
|
|
12299
|
-
"ignored_system_columns": ignored_system_columns,
|
|
12300
|
-
},
|
|
12416
|
+
details={"app_key": app_key, "view_name": patch.name},
|
|
12301
12417
|
)
|
|
12302
|
-
missing_columns = [
|
|
12418
|
+
missing_columns = [
|
|
12419
|
+
que for que in apply_columns
|
|
12420
|
+
if not _is_known_system_view_que_id(que) and que not in fields_by_que_id
|
|
12421
|
+
]
|
|
12303
12422
|
if missing_columns:
|
|
12304
12423
|
return _failed(
|
|
12305
12424
|
"UNKNOWN_VIEW_FIELD",
|
|
@@ -12309,45 +12428,89 @@ class AiBuilderFacade:
|
|
|
12309
12428
|
"app_key": app_key,
|
|
12310
12429
|
"view_name": patch.name,
|
|
12311
12430
|
"missing_fields": missing_columns,
|
|
12312
|
-
"ignored_system_columns": ignored_system_columns,
|
|
12313
12431
|
},
|
|
12314
12432
|
missing_fields=missing_columns,
|
|
12315
12433
|
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
12316
12434
|
)
|
|
12317
|
-
if patch.
|
|
12435
|
+
if patch.group_que_id and patch.group_que_id not in fields_by_que_id:
|
|
12318
12436
|
return _failed(
|
|
12319
12437
|
"UNKNOWN_VIEW_FIELD",
|
|
12320
|
-
f"group_by references unknown field '{patch.
|
|
12438
|
+
f"group_by references unknown field '{patch.group_que_id}'",
|
|
12321
12439
|
normalized_args=normalized_args,
|
|
12322
12440
|
details={
|
|
12323
12441
|
"app_key": app_key,
|
|
12324
12442
|
"view_name": patch.name,
|
|
12325
|
-
"missing_fields": [patch.
|
|
12443
|
+
"missing_fields": [patch.group_que_id],
|
|
12326
12444
|
},
|
|
12327
|
-
missing_fields=[patch.
|
|
12445
|
+
missing_fields=[patch.group_que_id],
|
|
12328
12446
|
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
12329
12447
|
)
|
|
12330
|
-
for
|
|
12331
|
-
if
|
|
12448
|
+
for gantt_que_id in (patch.start_que_id, patch.end_que_id, patch.title_que_id):
|
|
12449
|
+
if gantt_que_id and gantt_que_id not in fields_by_que_id:
|
|
12332
12450
|
return _failed(
|
|
12333
12451
|
"UNKNOWN_VIEW_FIELD",
|
|
12334
|
-
f"gantt configuration references unknown field '{
|
|
12452
|
+
f"gantt configuration references unknown field '{gantt_que_id}'",
|
|
12335
12453
|
normalized_args=normalized_args,
|
|
12336
12454
|
details={
|
|
12337
12455
|
"app_key": app_key,
|
|
12338
12456
|
"view_name": patch.name,
|
|
12339
|
-
"missing_fields": [
|
|
12457
|
+
"missing_fields": [gantt_que_id],
|
|
12340
12458
|
},
|
|
12341
|
-
missing_fields=[
|
|
12459
|
+
missing_fields=[gantt_que_id],
|
|
12342
12460
|
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
12343
12461
|
)
|
|
12462
|
+
for group_name, config_que_ids in _iter_view_config_group_field_names(patch):
|
|
12463
|
+
missing_config_fields = [que for que in config_que_ids if que not in fields_by_que_id]
|
|
12464
|
+
if missing_config_fields:
|
|
12465
|
+
return _failed(
|
|
12466
|
+
"UNKNOWN_VIEW_FIELD",
|
|
12467
|
+
f"view {group_name} configuration references unknown fields",
|
|
12468
|
+
normalized_args=normalized_args,
|
|
12469
|
+
details={
|
|
12470
|
+
"app_key": app_key,
|
|
12471
|
+
"view_name": patch.name,
|
|
12472
|
+
"missing_fields": missing_config_fields,
|
|
12473
|
+
"reason_path": f"{group_name}",
|
|
12474
|
+
},
|
|
12475
|
+
missing_fields=missing_config_fields,
|
|
12476
|
+
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
12477
|
+
)
|
|
12478
|
+
hierarchy_root_filter_groups: list[list[dict[str, Any]]] | None = None
|
|
12479
|
+
if patch.hierarchy is not None and patch.hierarchy.root_filter:
|
|
12480
|
+
hierarchy_root_filter_groups = []
|
|
12481
|
+
for root_filter_group in patch.hierarchy.root_filter:
|
|
12482
|
+
translated_group, hierarchy_filter_issues = _build_view_filter_groups(
|
|
12483
|
+
fields_by_que_id=fields_by_que_id,
|
|
12484
|
+
filters=root_filter_group,
|
|
12485
|
+
)
|
|
12486
|
+
if hierarchy_filter_issues:
|
|
12487
|
+
first_issue = hierarchy_filter_issues[0]
|
|
12488
|
+
return _failed(
|
|
12489
|
+
str(first_issue.get("error_code") or "UNKNOWN_VIEW_FIELD"),
|
|
12490
|
+
"view hierarchy root_filter references invalid fields or values",
|
|
12491
|
+
normalized_args=normalized_args,
|
|
12492
|
+
details={
|
|
12493
|
+
"app_key": app_key,
|
|
12494
|
+
"view_name": patch.name,
|
|
12495
|
+
**first_issue,
|
|
12496
|
+
},
|
|
12497
|
+
missing_fields=list(first_issue.get("missing_fields") or []),
|
|
12498
|
+
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
12499
|
+
)
|
|
12500
|
+
hierarchy_root_filter_groups.extend(translated_group)
|
|
12344
12501
|
translated_filters: list[list[dict[str, Any]]] | None
|
|
12345
12502
|
filter_issues: list[dict[str, Any]]
|
|
12346
12503
|
if patch.preserve_filters:
|
|
12347
12504
|
translated_filters = None
|
|
12348
12505
|
filter_issues = []
|
|
12506
|
+
elif patch.filters and isinstance(patch.filters[0], list):
|
|
12507
|
+
# 嵌套形态 [[rule,...],[rule,...]]:组间 OR、组内 AND(viewgraphLimit 多组),
|
|
12508
|
+
# 复用 button_limit 的多组编译;后端简易模式会从多组规则自动派生公式
|
|
12509
|
+
translated_filters, filter_issues = _build_view_button_limit_groups(
|
|
12510
|
+
fields_by_que_id=fields_by_que_id, groups=patch.filters
|
|
12511
|
+
)
|
|
12349
12512
|
else:
|
|
12350
|
-
translated_filters, filter_issues = _build_view_filter_groups(
|
|
12513
|
+
translated_filters, filter_issues = _build_view_filter_groups(fields_by_que_id=fields_by_que_id, filters=patch.filters)
|
|
12351
12514
|
if filter_issues:
|
|
12352
12515
|
first_issue = filter_issues[0]
|
|
12353
12516
|
return _failed(
|
|
@@ -12369,7 +12532,7 @@ class AiBuilderFacade:
|
|
|
12369
12532
|
query_condition_issues = []
|
|
12370
12533
|
else:
|
|
12371
12534
|
query_condition_payload, expected_query_conditions, query_condition_issues = _build_view_query_conditions_payload(
|
|
12372
|
-
|
|
12535
|
+
fields_by_que_id=fields_by_que_id,
|
|
12373
12536
|
query_conditions=patch.query_conditions,
|
|
12374
12537
|
)
|
|
12375
12538
|
if query_condition_issues:
|
|
@@ -12426,7 +12589,7 @@ class AiBuilderFacade:
|
|
|
12426
12589
|
expected_button_summary = None
|
|
12427
12590
|
elif patch.buttons is not None:
|
|
12428
12591
|
explicit_button_dtos, button_issues = _build_view_button_dtos(
|
|
12429
|
-
|
|
12592
|
+
fields_by_que_id=fields_by_que_id,
|
|
12430
12593
|
bindings=patch.buttons,
|
|
12431
12594
|
valid_custom_button_ids=valid_custom_button_ids,
|
|
12432
12595
|
)
|
|
@@ -12484,6 +12647,10 @@ class AiBuilderFacade:
|
|
|
12484
12647
|
existing_key = _extract_view_key(matched_existing_view)
|
|
12485
12648
|
created_key: str | None = None
|
|
12486
12649
|
system_view_list_type = _resolve_system_view_list_type(view_key=existing_key, view_name=patch.name) if existing_key else None
|
|
12650
|
+
if system_view_list_type is not None and os.environ.get("QINGFLOW_VIEWS_APPLY_SKIP_SYSTEM_VIEW_SYNC", "").strip().lower() in {"1", "true", "yes", "on"}:
|
|
12651
|
+
# 环境受限开关:默认视图列序同步依赖 app apply/baseInfo 回读,在快照/脱敏环境上该读面可能不可用;
|
|
12652
|
+
# 视图写主体(view_update)不受影响,仅跳过同步与尾部校验。
|
|
12653
|
+
system_view_list_type = None
|
|
12487
12654
|
operation_phase = "view_update" if existing_key else "view_create"
|
|
12488
12655
|
query_condition_payload_for_apply = deepcopy(query_condition_payload)
|
|
12489
12656
|
expected_query_conditions_for_verify = deepcopy(expected_query_conditions)
|
|
@@ -12517,16 +12684,17 @@ class AiBuilderFacade:
|
|
|
12517
12684
|
views=self.views,
|
|
12518
12685
|
profile=profile,
|
|
12519
12686
|
source_viewgraph_key=existing_key,
|
|
12687
|
+
hierarchy_filter_groups=hierarchy_root_filter_groups,
|
|
12520
12688
|
schema=schema,
|
|
12521
12689
|
patch=patch,
|
|
12522
12690
|
view_filters=translated_filters,
|
|
12523
|
-
|
|
12691
|
+
fields_by_que_id=fields_by_que_id,
|
|
12524
12692
|
auth_override=view_auth_override,
|
|
12525
12693
|
explicit_button_dtos=explicit_button_dtos,
|
|
12526
12694
|
query_condition_payload=query_condition_payload_for_apply,
|
|
12527
12695
|
associated_resources_payload=associated_resources_payload_for_apply,
|
|
12528
12696
|
)
|
|
12529
|
-
self.
|
|
12697
|
+
self._apply_view_update(profile=profile, view_key=existing_key, payload=payload)
|
|
12530
12698
|
system_view_sync: dict[str, Any] | None = None
|
|
12531
12699
|
if system_view_list_type is not None and patch.type.value == "table":
|
|
12532
12700
|
operation_phase = "default_view_apply_config_sync"
|
|
@@ -12537,7 +12705,9 @@ class AiBuilderFacade:
|
|
|
12537
12705
|
payload=payload,
|
|
12538
12706
|
list_type=system_view_list_type,
|
|
12539
12707
|
schema=schema,
|
|
12540
|
-
visible_field_names=
|
|
12708
|
+
visible_field_names=[
|
|
12709
|
+
str((fields_by_que_id.get(que) or {}).get("name") or "") for que in apply_columns if que in fields_by_que_id
|
|
12710
|
+
],
|
|
12541
12711
|
)
|
|
12542
12712
|
if not bool(system_view_sync.get("verified")):
|
|
12543
12713
|
failure_entry = {
|
|
@@ -12562,24 +12732,23 @@ class AiBuilderFacade:
|
|
|
12562
12732
|
"apply_columns": apply_columns,
|
|
12563
12733
|
},
|
|
12564
12734
|
}
|
|
12565
|
-
|
|
12566
|
-
|
|
12735
|
+
failure_result = failure_entry
|
|
12736
|
+
view_result = failure_entry
|
|
12567
12737
|
continue
|
|
12568
12738
|
updated.append(patch.name)
|
|
12569
|
-
|
|
12570
|
-
|
|
12571
|
-
|
|
12572
|
-
|
|
12573
|
-
|
|
12574
|
-
|
|
12575
|
-
|
|
12576
|
-
|
|
12577
|
-
|
|
12578
|
-
|
|
12579
|
-
|
|
12580
|
-
|
|
12581
|
-
|
|
12582
|
-
)
|
|
12739
|
+
view_result = {
|
|
12740
|
+
"name": patch.name,
|
|
12741
|
+
"view_key": existing_key,
|
|
12742
|
+
"type": patch.type.value,
|
|
12743
|
+
"status": "updated",
|
|
12744
|
+
"expected_filters": deepcopy(translated_filters),
|
|
12745
|
+
"expected_buttons": deepcopy(expected_button_summary),
|
|
12746
|
+
"expected_query_conditions": deepcopy(expected_query_conditions_for_verify),
|
|
12747
|
+
"expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
|
|
12748
|
+
"expected_pinned": patch.pinned,
|
|
12749
|
+
"system_view_sync": system_view_sync,
|
|
12750
|
+
"apply_columns": deepcopy(apply_columns),
|
|
12751
|
+
}
|
|
12583
12752
|
record_action_button_intent(patch=patch, view_key=existing_key, index=ordinal - 1)
|
|
12584
12753
|
else:
|
|
12585
12754
|
template_key = _pick_view_template_key(existing_view_list, desired_type=patch.type.value)
|
|
@@ -12591,50 +12760,45 @@ class AiBuilderFacade:
|
|
|
12591
12760
|
views=self.views,
|
|
12592
12761
|
profile=profile,
|
|
12593
12762
|
source_viewgraph_key=created_key,
|
|
12763
|
+
hierarchy_filter_groups=hierarchy_root_filter_groups,
|
|
12594
12764
|
schema=schema,
|
|
12595
12765
|
patch=patch,
|
|
12596
12766
|
view_filters=translated_filters,
|
|
12597
|
-
|
|
12767
|
+
fields_by_que_id=fields_by_que_id,
|
|
12598
12768
|
auth_override=view_auth_override,
|
|
12599
12769
|
explicit_button_dtos=explicit_button_dtos,
|
|
12600
12770
|
query_condition_payload=query_condition_payload_for_apply,
|
|
12601
12771
|
associated_resources_payload=associated_resources_payload_for_apply,
|
|
12602
12772
|
)
|
|
12603
|
-
self.
|
|
12773
|
+
self._apply_view_update(profile=profile, view_key=created_key, payload=payload)
|
|
12604
12774
|
else:
|
|
12605
|
-
payload =
|
|
12775
|
+
payload = _build_view_creation_payload(
|
|
12606
12776
|
app_key=app_key,
|
|
12607
12777
|
base_info=base,
|
|
12608
12778
|
schema=schema,
|
|
12609
12779
|
patch=patch,
|
|
12610
12780
|
ordinal=ordinal,
|
|
12611
12781
|
view_filters=translated_filters,
|
|
12612
|
-
|
|
12782
|
+
fields_by_que_id=fields_by_que_id,
|
|
12613
12783
|
auth_override=view_auth_override,
|
|
12614
12784
|
explicit_button_dtos=explicit_button_dtos,
|
|
12615
12785
|
query_condition_payload=query_condition_payload_for_apply,
|
|
12616
12786
|
associated_resources_payload=associated_resources_payload_for_apply,
|
|
12787
|
+
hierarchy_filter_groups=hierarchy_root_filter_groups,
|
|
12617
12788
|
)
|
|
12618
|
-
|
|
12619
|
-
raw_created = create_result.get("result")
|
|
12620
|
-
if isinstance(raw_created, dict):
|
|
12621
|
-
raw_view_key = raw_created.get("viewgraphKey") or raw_created.get("viewKey")
|
|
12622
|
-
created_key = str(raw_view_key).strip() if raw_view_key is not None and str(raw_view_key).strip() else None
|
|
12623
|
-
elif isinstance(raw_created, str):
|
|
12624
|
-
created_key = raw_created.strip() or None
|
|
12789
|
+
created_key = self._apply_view_create(profile=profile, payload=payload)
|
|
12625
12790
|
created.append(patch.name)
|
|
12626
|
-
|
|
12627
|
-
|
|
12628
|
-
|
|
12629
|
-
|
|
12630
|
-
|
|
12631
|
-
|
|
12632
|
-
|
|
12633
|
-
|
|
12634
|
-
|
|
12635
|
-
|
|
12636
|
-
|
|
12637
|
-
)
|
|
12791
|
+
view_result = {
|
|
12792
|
+
"name": patch.name,
|
|
12793
|
+
"view_key": created_key,
|
|
12794
|
+
"type": patch.type.value,
|
|
12795
|
+
"status": "created",
|
|
12796
|
+
"expected_filters": deepcopy(translated_filters),
|
|
12797
|
+
"expected_buttons": deepcopy(expected_button_summary),
|
|
12798
|
+
"expected_query_conditions": deepcopy(expected_query_conditions_for_verify),
|
|
12799
|
+
"expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
|
|
12800
|
+
"expected_pinned": patch.pinned,
|
|
12801
|
+
}
|
|
12638
12802
|
record_action_button_intent(patch=patch, view_key=created_key, index=ordinal - 1)
|
|
12639
12803
|
except (QingflowApiError, RuntimeError) as error:
|
|
12640
12804
|
api_error = _coerce_api_error(error)
|
|
@@ -12661,17 +12825,20 @@ class AiBuilderFacade:
|
|
|
12661
12825
|
patch=patch,
|
|
12662
12826
|
ordinal=ordinal,
|
|
12663
12827
|
view_filters=translated_filters,
|
|
12664
|
-
|
|
12828
|
+
hierarchy_filter_groups=hierarchy_root_filter_groups,
|
|
12829
|
+
fields_by_que_id=fields_by_que_id,
|
|
12665
12830
|
auth_override=view_auth_override,
|
|
12666
12831
|
explicit_button_dtos=fallback_button_dtos,
|
|
12667
12832
|
query_condition_payload=query_condition_payload_for_apply,
|
|
12668
12833
|
associated_resources_payload=associated_resources_payload_for_apply,
|
|
12669
12834
|
)
|
|
12670
|
-
self.
|
|
12835
|
+
self._apply_view_update(profile=profile, view_key=target_key, payload=fallback_payload)
|
|
12671
12836
|
system_view_sync: dict[str, Any] | None = None
|
|
12672
12837
|
fallback_system_view_list_type = (
|
|
12673
12838
|
_resolve_system_view_list_type(view_key=target_key, view_name=patch.name)
|
|
12674
|
-
if patch.type.value == "table"
|
|
12839
|
+
if patch.type.value == "table"
|
|
12840
|
+
and target_key
|
|
12841
|
+
and os.environ.get("QINGFLOW_VIEWS_APPLY_SKIP_SYSTEM_VIEW_SYNC", "").strip().lower() not in {"1", "true", "yes", "on"}
|
|
12675
12842
|
else None
|
|
12676
12843
|
)
|
|
12677
12844
|
if fallback_system_view_list_type is not None:
|
|
@@ -12683,7 +12850,9 @@ class AiBuilderFacade:
|
|
|
12683
12850
|
payload=fallback_payload,
|
|
12684
12851
|
list_type=fallback_system_view_list_type,
|
|
12685
12852
|
schema=schema,
|
|
12686
|
-
visible_field_names=
|
|
12853
|
+
visible_field_names=[
|
|
12854
|
+
str((fields_by_que_id.get(que) or {}).get("name") or "") for que in apply_columns if que in fields_by_que_id
|
|
12855
|
+
],
|
|
12687
12856
|
)
|
|
12688
12857
|
if not bool(system_view_sync.get("verified")):
|
|
12689
12858
|
failure_entry = {
|
|
@@ -12708,44 +12877,42 @@ class AiBuilderFacade:
|
|
|
12708
12877
|
"apply_columns": apply_columns,
|
|
12709
12878
|
},
|
|
12710
12879
|
}
|
|
12711
|
-
|
|
12712
|
-
|
|
12880
|
+
failure_result = failure_entry
|
|
12881
|
+
view_result = failure_entry
|
|
12713
12882
|
continue
|
|
12714
12883
|
if existing_key:
|
|
12715
12884
|
updated.append(patch.name)
|
|
12716
|
-
|
|
12717
|
-
|
|
12718
|
-
|
|
12719
|
-
|
|
12720
|
-
|
|
12721
|
-
|
|
12722
|
-
|
|
12723
|
-
|
|
12724
|
-
|
|
12725
|
-
|
|
12726
|
-
|
|
12727
|
-
|
|
12728
|
-
|
|
12729
|
-
|
|
12730
|
-
)
|
|
12885
|
+
view_result = {
|
|
12886
|
+
"name": patch.name,
|
|
12887
|
+
"view_key": existing_key,
|
|
12888
|
+
"type": patch.type.value,
|
|
12889
|
+
"status": "updated",
|
|
12890
|
+
"fallback_applied": True,
|
|
12891
|
+
"expected_filters": deepcopy(translated_filters),
|
|
12892
|
+
"expected_buttons": deepcopy(expected_button_summary),
|
|
12893
|
+
"expected_query_conditions": deepcopy(expected_query_conditions_for_verify),
|
|
12894
|
+
"expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
|
|
12895
|
+
"expected_pinned": patch.pinned,
|
|
12896
|
+
"system_view_sync": system_view_sync,
|
|
12897
|
+
"apply_columns": deepcopy(apply_columns),
|
|
12898
|
+
}
|
|
12731
12899
|
record_action_button_intent(patch=patch, view_key=existing_key, index=ordinal - 1)
|
|
12732
12900
|
else:
|
|
12733
12901
|
created.append(patch.name)
|
|
12734
|
-
|
|
12735
|
-
|
|
12736
|
-
|
|
12737
|
-
|
|
12738
|
-
|
|
12739
|
-
|
|
12740
|
-
|
|
12741
|
-
|
|
12742
|
-
|
|
12743
|
-
|
|
12744
|
-
|
|
12745
|
-
|
|
12746
|
-
|
|
12747
|
-
|
|
12748
|
-
)
|
|
12902
|
+
view_result = {
|
|
12903
|
+
"name": patch.name,
|
|
12904
|
+
"view_key": created_key,
|
|
12905
|
+
"type": patch.type.value,
|
|
12906
|
+
"status": "created",
|
|
12907
|
+
"fallback_applied": True,
|
|
12908
|
+
"expected_filters": deepcopy(translated_filters),
|
|
12909
|
+
"expected_buttons": deepcopy(expected_button_summary),
|
|
12910
|
+
"expected_query_conditions": deepcopy(expected_query_conditions_for_verify),
|
|
12911
|
+
"expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
|
|
12912
|
+
"expected_pinned": patch.pinned,
|
|
12913
|
+
"system_view_sync": system_view_sync,
|
|
12914
|
+
"apply_columns": deepcopy(apply_columns),
|
|
12915
|
+
}
|
|
12749
12916
|
record_action_button_intent(patch=patch, view_key=created_key, index=ordinal - 1)
|
|
12750
12917
|
continue
|
|
12751
12918
|
fallback_payload = _build_minimal_view_payload(
|
|
@@ -12754,16 +12921,16 @@ class AiBuilderFacade:
|
|
|
12754
12921
|
patch=patch,
|
|
12755
12922
|
ordinal=ordinal,
|
|
12756
12923
|
view_filters=translated_filters,
|
|
12757
|
-
|
|
12924
|
+
hierarchy_filter_groups=hierarchy_root_filter_groups,
|
|
12925
|
+
fields_by_que_id=fields_by_que_id,
|
|
12758
12926
|
auth_override=view_auth_override,
|
|
12759
12927
|
explicit_button_dtos=explicit_button_dtos,
|
|
12760
12928
|
query_condition_payload=query_condition_payload_for_apply,
|
|
12761
12929
|
associated_resources_payload=associated_resources_payload_for_apply,
|
|
12762
12930
|
)
|
|
12763
|
-
self.
|
|
12931
|
+
self._apply_view_create(profile=profile, payload=fallback_payload)
|
|
12764
12932
|
created.append(patch.name)
|
|
12765
|
-
|
|
12766
|
-
{
|
|
12933
|
+
view_result = {
|
|
12767
12934
|
"name": patch.name,
|
|
12768
12935
|
"view_key": created_key,
|
|
12769
12936
|
"type": patch.type.value,
|
|
@@ -12773,7 +12940,6 @@ class AiBuilderFacade:
|
|
|
12773
12940
|
"expected_query_conditions": deepcopy(expected_query_conditions_for_verify),
|
|
12774
12941
|
"expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
|
|
12775
12942
|
}
|
|
12776
|
-
)
|
|
12777
12943
|
record_action_button_intent(patch=patch, view_key=created_key, index=ordinal - 1)
|
|
12778
12944
|
continue
|
|
12779
12945
|
except (QingflowApiError, RuntimeError) as fallback_error:
|
|
@@ -12799,11 +12965,16 @@ class AiBuilderFacade:
|
|
|
12799
12965
|
"view_name": patch.name,
|
|
12800
12966
|
"view_type": patch.type.value,
|
|
12801
12967
|
"columns": patch.columns,
|
|
12802
|
-
"
|
|
12803
|
-
"filters": [
|
|
12804
|
-
|
|
12805
|
-
|
|
12806
|
-
|
|
12968
|
+
"group_que_id": patch.group_que_id,
|
|
12969
|
+
"filters": [
|
|
12970
|
+
(item.model_dump(mode="json") if hasattr(item, "model_dump") else item)
|
|
12971
|
+
if not isinstance(item, list)
|
|
12972
|
+
else [sub.model_dump(mode="json") if hasattr(sub, "model_dump") else sub for sub in item]
|
|
12973
|
+
for item in patch.filters
|
|
12974
|
+
],
|
|
12975
|
+
"start_que_id": patch.start_que_id,
|
|
12976
|
+
"end_que_id": patch.end_que_id,
|
|
12977
|
+
"title_que_id": patch.title_que_id,
|
|
12807
12978
|
"operation": "sync_default_view" if operation_phase == "default_view_apply_config_sync" else ("update" if existing_key or created_key else "create"),
|
|
12808
12979
|
"list_type": system_view_list_type,
|
|
12809
12980
|
"transport_error": {
|
|
@@ -12813,24 +12984,69 @@ class AiBuilderFacade:
|
|
|
12813
12984
|
},
|
|
12814
12985
|
**_view_apply_failure_diagnostics(
|
|
12815
12986
|
patch=patch,
|
|
12816
|
-
|
|
12987
|
+
fields_by_que_id=fields_by_que_id,
|
|
12817
12988
|
backend_code=api_error.backend_code,
|
|
12818
12989
|
operation_phase=operation_phase,
|
|
12819
12990
|
),
|
|
12820
12991
|
},
|
|
12821
12992
|
}
|
|
12822
|
-
|
|
12823
|
-
|
|
12993
|
+
failure_result = failure_entry
|
|
12994
|
+
view_result = failure_entry
|
|
12824
12995
|
continue
|
|
12825
12996
|
view_mutation_ms += _duration_ms(view_mutation_started_at)
|
|
12997
|
+
for item in ((view_result,) if view_result is not None else ()):
|
|
12998
|
+
requested_pinned = item.get("expected_pinned")
|
|
12999
|
+
view_key = str(item.get("view_key") or "").strip()
|
|
13000
|
+
if (
|
|
13001
|
+
str(item.get("status") or "") not in {"created", "updated"}
|
|
13002
|
+
or not isinstance(requested_pinned, bool)
|
|
13003
|
+
or not view_key
|
|
13004
|
+
):
|
|
13005
|
+
continue
|
|
13006
|
+
try:
|
|
13007
|
+
pin_result = self._set_view_pinned(
|
|
13008
|
+
profile=profile,
|
|
13009
|
+
app_key=app_key,
|
|
13010
|
+
view_key=view_key,
|
|
13011
|
+
pinned=requested_pinned,
|
|
13012
|
+
)
|
|
13013
|
+
item["pinned_state"] = pin_result
|
|
13014
|
+
if not bool(pin_result.get("verified")):
|
|
13015
|
+
failure_result = {
|
|
13016
|
+
"name": item.get("name"),
|
|
13017
|
+
"view_key": view_key,
|
|
13018
|
+
"type": item.get("type"),
|
|
13019
|
+
"status": "failed",
|
|
13020
|
+
"error_code": "VIEW_PIN_STATE_READBACK_MISMATCH",
|
|
13021
|
+
"message": "view configuration was saved, but its pinned state did not verify",
|
|
13022
|
+
"operation": "set_pinned",
|
|
13023
|
+
"details": pin_result,
|
|
13024
|
+
}
|
|
13025
|
+
except (QingflowApiError, RuntimeError, ValueError) as error:
|
|
13026
|
+
api_error = _coerce_api_error(error)
|
|
13027
|
+
pin_failure = {
|
|
13028
|
+
"name": item.get("name"),
|
|
13029
|
+
"view_key": view_key,
|
|
13030
|
+
"type": item.get("type"),
|
|
13031
|
+
"status": "failed",
|
|
13032
|
+
"error_code": "VIEW_PIN_STATE_APPLY_FAILED",
|
|
13033
|
+
"message": "view configuration was saved, but updating its pinned state failed",
|
|
13034
|
+
"request_id": api_error.request_id,
|
|
13035
|
+
"backend_code": api_error.backend_code,
|
|
13036
|
+
"http_status": api_error.http_status,
|
|
13037
|
+
"operation": "set_pinned",
|
|
13038
|
+
"details": {"requested_pinned": requested_pinned},
|
|
13039
|
+
}
|
|
13040
|
+
item["pinned_state"] = {"requested_pinned": requested_pinned, "verified": False}
|
|
13041
|
+
failure_result = pin_failure
|
|
12826
13042
|
successful_action_view_keys = {
|
|
12827
13043
|
str(item.get("view_key") or "").strip()
|
|
12828
|
-
for item in
|
|
13044
|
+
for item in ((view_result,) if view_result is not None else ())
|
|
12829
13045
|
if str(item.get("status") or "") in {"created", "updated"} and str(item.get("view_key") or "").strip()
|
|
12830
13046
|
}
|
|
12831
13047
|
successful_action_view_names = {
|
|
12832
13048
|
str(item.get("name") or "").strip()
|
|
12833
|
-
for item in
|
|
13049
|
+
for item in ((view_result,) if view_result is not None else ())
|
|
12834
13050
|
if str(item.get("status") or "") in {"created", "updated"} and str(item.get("name") or "").strip()
|
|
12835
13051
|
}
|
|
12836
13052
|
skipped_action_button_intents = [
|
|
@@ -12881,12 +13097,12 @@ class AiBuilderFacade:
|
|
|
12881
13097
|
action_buttons_failed = bool(action_button_intents) and not (action_buttons_verified and view_action_button_bindings_verified)
|
|
12882
13098
|
action_buttons_retry_payload = action_buttons_result.get("retry_payload") if isinstance(action_buttons_result.get("retry_payload"), dict) else None
|
|
12883
13099
|
needs_view_list_readback = bool(created or updated)
|
|
12884
|
-
|
|
13100
|
+
verified_views: list[dict[str, Any]] | None = []
|
|
12885
13101
|
verified_views_unavailable = False
|
|
12886
13102
|
if needs_view_list_readback:
|
|
12887
13103
|
try:
|
|
12888
13104
|
view_readback_started_at = time.perf_counter()
|
|
12889
|
-
|
|
13105
|
+
verified_views, verified_views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
12890
13106
|
except (QingflowApiError, RuntimeError) as error:
|
|
12891
13107
|
view_readback_ms += _duration_ms(view_readback_started_at)
|
|
12892
13108
|
api_error = _coerce_api_error(error)
|
|
@@ -12900,23 +13116,23 @@ class AiBuilderFacade:
|
|
|
12900
13116
|
view_readback_ms += _duration_ms(view_readback_started_at)
|
|
12901
13117
|
verified_names = {
|
|
12902
13118
|
_extract_view_name(item)
|
|
12903
|
-
for item in (
|
|
13119
|
+
for item in (verified_views or [])
|
|
12904
13120
|
if isinstance(item, dict) and _extract_view_name(item)
|
|
12905
13121
|
}
|
|
12906
13122
|
verified_by_key = {
|
|
12907
13123
|
_extract_view_key(item): item
|
|
12908
|
-
for item in (
|
|
13124
|
+
for item in (verified_views or [])
|
|
12909
13125
|
if isinstance(item, dict) and _extract_view_key(item)
|
|
12910
13126
|
}
|
|
12911
13127
|
verified_view_keys_by_name: dict[str, list[str]] = {}
|
|
12912
|
-
for item in (
|
|
13128
|
+
for item in (verified_views or []):
|
|
12913
13129
|
if not isinstance(item, dict):
|
|
12914
13130
|
continue
|
|
12915
13131
|
name = _extract_view_name(item)
|
|
12916
13132
|
key = _extract_view_key(item)
|
|
12917
13133
|
if name and key:
|
|
12918
13134
|
verified_view_keys_by_name.setdefault(name, []).append(key)
|
|
12919
|
-
|
|
13135
|
+
verification_result: dict[str, Any] | None = None
|
|
12920
13136
|
filter_readback_pending = False
|
|
12921
13137
|
filter_mismatches: list[dict[str, Any]] = []
|
|
12922
13138
|
query_condition_readback_pending = False
|
|
@@ -12943,7 +13159,7 @@ class AiBuilderFacade:
|
|
|
12943
13159
|
return cached
|
|
12944
13160
|
|
|
12945
13161
|
view_verification_started_at = time.perf_counter()
|
|
12946
|
-
for item in
|
|
13162
|
+
for item in ((view_result,) if view_result is not None else ()):
|
|
12947
13163
|
status = str(item.get("status") or "")
|
|
12948
13164
|
name = str(item.get("name") or "")
|
|
12949
13165
|
item_view_key = str(item.get("view_key") or "").strip()
|
|
@@ -12986,7 +13202,7 @@ class AiBuilderFacade:
|
|
|
12986
13202
|
verification_entry["readback_ambiguous"] = True
|
|
12987
13203
|
verification_entry["matching_view_keys"] = matched_keys
|
|
12988
13204
|
filter_readback_pending = True
|
|
12989
|
-
|
|
13205
|
+
verification_result = verification_entry
|
|
12990
13206
|
continue
|
|
12991
13207
|
try:
|
|
12992
13208
|
config_result, config_error = read_view_config_for_verification(verification_key)
|
|
@@ -13004,11 +13220,11 @@ class AiBuilderFacade:
|
|
|
13004
13220
|
filters_verified = filters_semantically_equivalent and actual_data_scope == expected_data_scope
|
|
13005
13221
|
public_expected_filters = _public_view_filter_groups_from_match_rules(
|
|
13006
13222
|
expected_filter_summary,
|
|
13007
|
-
|
|
13223
|
+
fields_by_que_id=fields_by_que_id,
|
|
13008
13224
|
)
|
|
13009
13225
|
public_actual_filters = _public_view_filter_groups_from_match_rules(
|
|
13010
13226
|
semantic_actual_filters,
|
|
13011
|
-
|
|
13227
|
+
fields_by_que_id=fields_by_que_id,
|
|
13012
13228
|
)
|
|
13013
13229
|
verification_entry["filters_verified"] = filters_verified
|
|
13014
13230
|
verification_entry["view_key"] = verification_key
|
|
@@ -13058,7 +13274,7 @@ class AiBuilderFacade:
|
|
|
13058
13274
|
verification_entry["readback_ambiguous"] = True
|
|
13059
13275
|
verification_entry["matching_view_keys"] = matched_keys
|
|
13060
13276
|
query_condition_readback_pending = True
|
|
13061
|
-
|
|
13277
|
+
verification_result = verification_entry
|
|
13062
13278
|
continue
|
|
13063
13279
|
try:
|
|
13064
13280
|
config_result, config_error = read_view_config_for_verification(verification_key)
|
|
@@ -13107,7 +13323,7 @@ class AiBuilderFacade:
|
|
|
13107
13323
|
verification_entry["readback_ambiguous"] = True
|
|
13108
13324
|
verification_entry["matching_view_keys"] = matched_keys
|
|
13109
13325
|
associated_resource_readback_pending = True
|
|
13110
|
-
|
|
13326
|
+
verification_result = verification_entry
|
|
13111
13327
|
continue
|
|
13112
13328
|
try:
|
|
13113
13329
|
config_result, config_error = read_view_config_for_verification(verification_key)
|
|
@@ -13162,7 +13378,7 @@ class AiBuilderFacade:
|
|
|
13162
13378
|
verification_entry["readback_ambiguous"] = True
|
|
13163
13379
|
verification_entry["matching_view_keys"] = matched_keys
|
|
13164
13380
|
button_readback_pending = True
|
|
13165
|
-
|
|
13381
|
+
verification_result = verification_entry
|
|
13166
13382
|
continue
|
|
13167
13383
|
try:
|
|
13168
13384
|
config_result, config_error = read_view_config_for_verification(verification_key)
|
|
@@ -13210,51 +13426,45 @@ class AiBuilderFacade:
|
|
|
13210
13426
|
"category": api_error.category,
|
|
13211
13427
|
}
|
|
13212
13428
|
button_readback_pending = True
|
|
13213
|
-
|
|
13429
|
+
verification_result = verification_entry
|
|
13214
13430
|
elif status == "removed":
|
|
13215
|
-
|
|
13216
|
-
|
|
13217
|
-
|
|
13218
|
-
|
|
13219
|
-
|
|
13220
|
-
|
|
13221
|
-
|
|
13222
|
-
|
|
13223
|
-
|
|
13224
|
-
|
|
13225
|
-
|
|
13226
|
-
}
|
|
13227
|
-
)
|
|
13431
|
+
verification_result = {
|
|
13432
|
+
"name": name,
|
|
13433
|
+
"view_key": item.get("view_key"),
|
|
13434
|
+
"type": item.get("type"),
|
|
13435
|
+
"status": "removed",
|
|
13436
|
+
"present_in_readback": False,
|
|
13437
|
+
"removed_verified": True,
|
|
13438
|
+
"delete_executed": bool(item.get("delete_executed")),
|
|
13439
|
+
"readback_status": item.get("readback_status") or "deleted",
|
|
13440
|
+
"safe_to_retry_delete": False,
|
|
13441
|
+
}
|
|
13228
13442
|
elif status == "readback_pending" and item.get("operation") == "delete":
|
|
13229
13443
|
readback_status = str(item.get("readback_status") or "unavailable")
|
|
13230
|
-
|
|
13231
|
-
|
|
13232
|
-
|
|
13233
|
-
|
|
13234
|
-
|
|
13235
|
-
|
|
13236
|
-
|
|
13237
|
-
|
|
13238
|
-
|
|
13239
|
-
|
|
13240
|
-
|
|
13241
|
-
|
|
13242
|
-
}
|
|
13243
|
-
)
|
|
13444
|
+
verification_result = {
|
|
13445
|
+
"name": name,
|
|
13446
|
+
"view_key": item.get("view_key"),
|
|
13447
|
+
"type": item.get("type"),
|
|
13448
|
+
"status": "readback_pending",
|
|
13449
|
+
"present_in_readback": True if readback_status == "still_exists" else None,
|
|
13450
|
+
"removed_verified": False,
|
|
13451
|
+
"delete_executed": bool(item.get("delete_executed")),
|
|
13452
|
+
"readback_status": readback_status,
|
|
13453
|
+
"safe_to_retry_delete": False,
|
|
13454
|
+
"error_code": item.get("error_code"),
|
|
13455
|
+
}
|
|
13244
13456
|
else:
|
|
13245
|
-
|
|
13246
|
-
|
|
13247
|
-
|
|
13248
|
-
|
|
13249
|
-
|
|
13250
|
-
|
|
13251
|
-
|
|
13252
|
-
}
|
|
13253
|
-
)
|
|
13457
|
+
verification_result = {
|
|
13458
|
+
"name": item.get("name"),
|
|
13459
|
+
"type": item.get("type"),
|
|
13460
|
+
"status": "failed",
|
|
13461
|
+
"present_in_readback": None,
|
|
13462
|
+
"error_code": item.get("error_code"),
|
|
13463
|
+
}
|
|
13254
13464
|
view_verification_ms += _duration_ms(view_verification_started_at)
|
|
13255
13465
|
removed_delete_results = [
|
|
13256
13466
|
item
|
|
13257
|
-
for item in
|
|
13467
|
+
for item in ((view_result,) if view_result is not None else ())
|
|
13258
13468
|
if item.get("operation") == "delete" and bool(item.get("delete_executed"))
|
|
13259
13469
|
]
|
|
13260
13470
|
removed_verified = all(str(item.get("readback_status") or "") == "deleted" for item in removed_delete_results)
|
|
@@ -13269,19 +13479,19 @@ class AiBuilderFacade:
|
|
|
13269
13479
|
view_associated_resources_verified = verified and not associated_resource_readback_pending and not associated_resource_mismatches
|
|
13270
13480
|
view_buttons_verified = verified and not button_readback_pending and not button_mismatches and not custom_button_readback_pending
|
|
13271
13481
|
noop = not created and not updated and not removed and not action_button_write_executed
|
|
13272
|
-
if
|
|
13482
|
+
if failure_result is not None:
|
|
13273
13483
|
successful_changes = bool(created or updated or removed or action_button_write_succeeded)
|
|
13274
|
-
first_failure =
|
|
13484
|
+
first_failure = failure_result
|
|
13275
13485
|
response = {
|
|
13276
13486
|
"status": "partial_success" if successful_changes else "failed",
|
|
13277
13487
|
"error_code": "VIEW_APPLY_PARTIAL" if successful_changes else "VIEW_APPLY_FAILED",
|
|
13278
13488
|
"recoverable": True,
|
|
13279
|
-
"message": "
|
|
13489
|
+
"message": "view operation completed with a verification failure" if successful_changes else "view operation failed",
|
|
13280
13490
|
"normalized_args": normalized_args,
|
|
13281
13491
|
"missing_fields": [],
|
|
13282
13492
|
"allowed_values": {"view_types": [member.value for member in PublicViewType], "view.filter.operator": [member.value for member in ViewFilterOperator]},
|
|
13283
13493
|
"details": {
|
|
13284
|
-
"
|
|
13494
|
+
"view_result": deepcopy(view_result) if view_result is not None else None,
|
|
13285
13495
|
"filter_mismatches": filter_mismatches,
|
|
13286
13496
|
"query_condition_mismatches": query_condition_mismatches,
|
|
13287
13497
|
"associated_resource_mismatches": associated_resource_mismatches,
|
|
@@ -13339,13 +13549,14 @@ class AiBuilderFacade:
|
|
|
13339
13549
|
"action_buttons_verified": action_buttons_verified,
|
|
13340
13550
|
"view_button_bindings_verified": view_action_button_bindings_verified,
|
|
13341
13551
|
"views_read_unavailable": verified_views_unavailable,
|
|
13342
|
-
"
|
|
13552
|
+
"view": deepcopy(verification_result) if verification_result is not None else None,
|
|
13343
13553
|
"custom_button_readback_pending": custom_button_readback_pending,
|
|
13344
13554
|
"custom_button_readback_pending_entries": deepcopy(custom_button_readback_pending_entries),
|
|
13345
13555
|
},
|
|
13346
13556
|
"app_key": app_key,
|
|
13347
13557
|
"app_name": app_name,
|
|
13348
|
-
"
|
|
13558
|
+
"view_result": deepcopy(view_result) if view_result is not None else None,
|
|
13559
|
+
"views_diff": {"created": created, "updated": updated, "removed": removed, "failed": [failure_result]},
|
|
13349
13560
|
"verified": verified and view_filters_verified and view_query_conditions_verified and view_associated_resources_verified and view_buttons_verified and action_buttons_verified and view_action_button_bindings_verified,
|
|
13350
13561
|
"write_executed": bool(created or updated or removed or action_button_write_executed),
|
|
13351
13562
|
"write_succeeded": bool(created or updated or removed or action_button_write_succeeded),
|
|
@@ -13414,6 +13625,7 @@ class AiBuilderFacade:
|
|
|
13414
13625
|
**({"query_condition_mismatches": query_condition_mismatches} if query_condition_mismatches else {}),
|
|
13415
13626
|
**({"associated_resource_mismatches": associated_resource_mismatches} if associated_resource_mismatches else {}),
|
|
13416
13627
|
**({"button_mismatches": button_mismatches} if button_mismatches else {}),
|
|
13628
|
+
"view_result": deepcopy(view_result) if view_result is not None else None,
|
|
13417
13629
|
**({"action_buttons_result": action_buttons_result} if action_button_intents else {}),
|
|
13418
13630
|
**(
|
|
13419
13631
|
{"custom_button_readback_pending": deepcopy(custom_button_readback_pending_entries)}
|
|
@@ -13441,10 +13653,11 @@ class AiBuilderFacade:
|
|
|
13441
13653
|
"delete_readback_pending": delete_readback_pending,
|
|
13442
13654
|
"custom_button_readback_pending": custom_button_readback_pending,
|
|
13443
13655
|
"custom_button_readback_pending_entries": deepcopy(custom_button_readback_pending_entries),
|
|
13444
|
-
"
|
|
13656
|
+
"view": deepcopy(verification_result) if verification_result is not None else None,
|
|
13445
13657
|
},
|
|
13446
13658
|
"app_key": app_key,
|
|
13447
13659
|
"app_name": app_name,
|
|
13660
|
+
"view_result": deepcopy(view_result) if view_result is not None else None,
|
|
13448
13661
|
"views_diff": {"created": created, "updated": updated, "removed": removed, "failed": []},
|
|
13449
13662
|
"verified": all_verified,
|
|
13450
13663
|
"write_executed": bool(created or updated or removed or action_button_write_executed),
|
|
@@ -13453,6 +13666,305 @@ class AiBuilderFacade:
|
|
|
13453
13666
|
}
|
|
13454
13667
|
return finish_with_publish(response)
|
|
13455
13668
|
|
|
13669
|
+
def app_views_schema(
|
|
13670
|
+
self,
|
|
13671
|
+
*,
|
|
13672
|
+
schema_version: str | None = None,
|
|
13673
|
+
schema_kind: str | None = None,
|
|
13674
|
+
) -> JSONObject:
|
|
13675
|
+
"""读取版本化视图声明 schema;detail kind 用于获取专项配置定义。"""
|
|
13676
|
+
normalized_args = {"schema_version": schema_version, "schema_kind": schema_kind}
|
|
13677
|
+
try:
|
|
13678
|
+
payload = views_schema_registry().schema(schema_version=schema_version, schema_kind=schema_kind)
|
|
13679
|
+
except ViewsContractError as error:
|
|
13680
|
+
return _failed(
|
|
13681
|
+
error.error_code,
|
|
13682
|
+
str(error.message),
|
|
13683
|
+
normalized_args=normalized_args,
|
|
13684
|
+
details={"reason_path": error.path or "", **(error.details or {})},
|
|
13685
|
+
suggested_next_call={"tool_name": "app_views_schema", "arguments": {"schema_kind": "summary"}},
|
|
13686
|
+
)
|
|
13687
|
+
return {
|
|
13688
|
+
"status": "success",
|
|
13689
|
+
"error_code": None,
|
|
13690
|
+
"recoverable": False,
|
|
13691
|
+
"message": "read views declaration schema",
|
|
13692
|
+
"normalized_args": normalized_args,
|
|
13693
|
+
"missing_fields": [],
|
|
13694
|
+
"allowed_values": {"schema_kind": payload["supportedSchemaKinds"]},
|
|
13695
|
+
"details": {},
|
|
13696
|
+
"request_id": None,
|
|
13697
|
+
"suggested_next_call": None,
|
|
13698
|
+
"warnings": [],
|
|
13699
|
+
"verification": {"schema_loaded": True},
|
|
13700
|
+
**payload,
|
|
13701
|
+
}
|
|
13702
|
+
|
|
13703
|
+
def app_views_get(
|
|
13704
|
+
self,
|
|
13705
|
+
*,
|
|
13706
|
+
profile: str,
|
|
13707
|
+
app_key: str,
|
|
13708
|
+
schema_version: str | None = None,
|
|
13709
|
+
schema_kind: str = "summary",
|
|
13710
|
+
) -> JSONObject:
|
|
13711
|
+
"""读取应用的 canonical 视图声明数组(summary 级投影或 complete 反向编译)。"""
|
|
13712
|
+
normalized_args = {"app_key": app_key, "schema_version": schema_version, "schema_kind": schema_kind}
|
|
13713
|
+
try:
|
|
13714
|
+
context = views_schema_registry().resolve_schema_version(schema_version)
|
|
13715
|
+
except ViewsContractError as error:
|
|
13716
|
+
return _failed(
|
|
13717
|
+
error.error_code,
|
|
13718
|
+
str(error.message),
|
|
13719
|
+
normalized_args=normalized_args,
|
|
13720
|
+
details={"reason_path": error.path or "", **(error.details or {})},
|
|
13721
|
+
suggested_next_call={"tool_name": "app_views_schema", "arguments": {"schema_kind": "summary"}},
|
|
13722
|
+
)
|
|
13723
|
+
try:
|
|
13724
|
+
views, _views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=False)
|
|
13725
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
13726
|
+
api_error = _coerce_api_error(error)
|
|
13727
|
+
return _failed_from_api_error(
|
|
13728
|
+
"VIEWS_READ_FAILED",
|
|
13729
|
+
api_error,
|
|
13730
|
+
normalized_args=normalized_args,
|
|
13731
|
+
details=_with_state_read_blocked_details({"app_key": app_key}, resource="views", error=api_error),
|
|
13732
|
+
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
13733
|
+
)
|
|
13734
|
+
schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
13735
|
+
parsed_schema = _parse_schema(schema)
|
|
13736
|
+
# 读面 summary 面向 agent 展示字段名:从原始 schema 递归收集(同名不丢);
|
|
13737
|
+
# 系统列(queId<=5)用公开名映射,未知系统列以 queId 展示兜底
|
|
13738
|
+
name_by_que_id: dict[int, str] = {
|
|
13739
|
+
**{system_id: name for system_id, name in _SYSTEM_VIEW_QUE_NAMES.items()},
|
|
13740
|
+
**_collect_field_id_to_name(schema),
|
|
13741
|
+
}
|
|
13742
|
+
normalized_kind = str(schema_kind or "summary").strip().lower()
|
|
13743
|
+
if normalized_kind == "complete":
|
|
13744
|
+
return self._app_views_get_complete(
|
|
13745
|
+
profile=profile,
|
|
13746
|
+
app_key=app_key,
|
|
13747
|
+
views=views,
|
|
13748
|
+
parsed_schema=parsed_schema,
|
|
13749
|
+
normalized_args=normalized_args,
|
|
13750
|
+
schema_version=schema_version,
|
|
13751
|
+
)
|
|
13752
|
+
projected: list[dict[str, Any]] = []
|
|
13753
|
+
scope_map = {"ALL": "all", "ONESELF": "oneself", "DEPT": "dept", "SUBORDINATE": "subordinate", "CUSTOM": "custom"}
|
|
13754
|
+
for view in views if isinstance(views, list) else []:
|
|
13755
|
+
if not isinstance(view, dict):
|
|
13756
|
+
continue
|
|
13757
|
+
entry: dict[str, Any] = {
|
|
13758
|
+
"view_key": _extract_view_key(view),
|
|
13759
|
+
"name": _extract_view_name(view),
|
|
13760
|
+
"type": _normalize_view_type_name(view.get("viewgraphType") or view.get("type")),
|
|
13761
|
+
}
|
|
13762
|
+
columns = [name_by_que_id.get(que_id, str(que_id)) for que_id in view.get("viewgraphQueIds") or [] if isinstance(que_id, int)]
|
|
13763
|
+
if columns:
|
|
13764
|
+
entry["columns"] = columns
|
|
13765
|
+
group_que_id = _coerce_positive_int(view.get("groupQueId"))
|
|
13766
|
+
if group_que_id is not None and group_que_id in name_by_que_id:
|
|
13767
|
+
entry["group_by"] = name_by_que_id[group_que_id]
|
|
13768
|
+
gantt_config = view.get("viewgraphGanttConfigVO")
|
|
13769
|
+
if isinstance(gantt_config, dict):
|
|
13770
|
+
for source, target in (
|
|
13771
|
+
("startTimeQueId", "start_field"),
|
|
13772
|
+
("endTimeQueId", "end_field"),
|
|
13773
|
+
("titleQueId", "title_field"),
|
|
13774
|
+
):
|
|
13775
|
+
gantt_que_id = _coerce_positive_int(gantt_config.get(source))
|
|
13776
|
+
if gantt_que_id is not None and gantt_que_id in name_by_que_id:
|
|
13777
|
+
entry[target] = name_by_que_id[gantt_que_id]
|
|
13778
|
+
title_que_id = _coerce_positive_int(view.get("titleQue"))
|
|
13779
|
+
if title_que_id is not None and title_que_id in name_by_que_id:
|
|
13780
|
+
entry.setdefault("title_field", name_by_que_id[title_que_id])
|
|
13781
|
+
data_permission_type = view.get("dataPermissionType")
|
|
13782
|
+
if data_permission_type:
|
|
13783
|
+
entry["data_permission"] = {
|
|
13784
|
+
"type": "config" if str(data_permission_type).upper() == "CONFIG" else "custom",
|
|
13785
|
+
"scope": scope_map.get(str(view.get("dataScope") or "").upper(), "all"),
|
|
13786
|
+
}
|
|
13787
|
+
projected.append(entry)
|
|
13788
|
+
return {
|
|
13789
|
+
"status": "success",
|
|
13790
|
+
"error_code": None,
|
|
13791
|
+
"recoverable": False,
|
|
13792
|
+
"message": "read canonical views declarations",
|
|
13793
|
+
"normalized_args": normalized_args,
|
|
13794
|
+
"missing_fields": [],
|
|
13795
|
+
"allowed_values": {"view_types": [member.value for member in PublicViewType]},
|
|
13796
|
+
"details": {},
|
|
13797
|
+
"request_id": None,
|
|
13798
|
+
"suggested_next_call": None,
|
|
13799
|
+
"warnings": [],
|
|
13800
|
+
"verification": {"views_read": len(projected)},
|
|
13801
|
+
"schemaVersion": context.schema_version,
|
|
13802
|
+
"apiVersion": context.api_version,
|
|
13803
|
+
"latestVersion": views_schema_registry().latest_version,
|
|
13804
|
+
"app_key": app_key,
|
|
13805
|
+
"views": projected,
|
|
13806
|
+
}
|
|
13807
|
+
|
|
13808
|
+
def _app_views_get_complete(
|
|
13809
|
+
self,
|
|
13810
|
+
*,
|
|
13811
|
+
profile: str,
|
|
13812
|
+
app_key: str,
|
|
13813
|
+
views: Any,
|
|
13814
|
+
parsed_schema: dict[str, Any],
|
|
13815
|
+
normalized_args: JSONObject,
|
|
13816
|
+
schema_version: str | None,
|
|
13817
|
+
) -> JSONObject:
|
|
13818
|
+
"""complete 档 canonical:逐视图 GET /view/{key}/viewConfig → 反向编译。"""
|
|
13819
|
+
from .views_extract import extract_complete_view_declaration, summarize_gaps
|
|
13820
|
+
|
|
13821
|
+
fields_by_que_id = _collect_fields_by_que_id(parsed_schema["fields"])
|
|
13822
|
+
projected: list[dict[str, Any]] = []
|
|
13823
|
+
read_errors: list[dict[str, Any]] = []
|
|
13824
|
+
all_gaps: list[dict[str, Any]] = []
|
|
13825
|
+
for view in views if isinstance(views, list) else []:
|
|
13826
|
+
if not isinstance(view, dict):
|
|
13827
|
+
continue
|
|
13828
|
+
view_key = _extract_view_key(view)
|
|
13829
|
+
view_name = _extract_view_name(view)
|
|
13830
|
+
if not view_key:
|
|
13831
|
+
continue
|
|
13832
|
+
if _resolve_system_view_list_type(view_key=view_key, view_name=view_name) is not None and not _normalize_view_type_name(view.get("viewgraphType") or view.get("type")):
|
|
13833
|
+
# 系统视图(列表/申请页等,viewgraphKey 为固定数字 id):不在公开声明语义内,直接跳过。
|
|
13834
|
+
continue
|
|
13835
|
+
try:
|
|
13836
|
+
config = self.views.view_get_config(profile=profile, viewgraph_key=view_key).get("result") or {}
|
|
13837
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
13838
|
+
api_error = _coerce_api_error(error)
|
|
13839
|
+
read_errors.append(
|
|
13840
|
+
{
|
|
13841
|
+
"view_key": view_key,
|
|
13842
|
+
"name": view_name,
|
|
13843
|
+
"error_code": "VIEW_CONFIG_READ_FAILED",
|
|
13844
|
+
"http_status": api_error.http_status,
|
|
13845
|
+
"message": str(api_error.message or error),
|
|
13846
|
+
}
|
|
13847
|
+
)
|
|
13848
|
+
continue
|
|
13849
|
+
if not isinstance(config, dict) or not config:
|
|
13850
|
+
read_errors.append({"view_key": view_key, "name": view_name, "error_code": "VIEW_CONFIG_EMPTY", "message": "viewConfig read returned no data"})
|
|
13851
|
+
continue
|
|
13852
|
+
declaration, gaps = extract_complete_view_declaration(config, fields_by_que_id=fields_by_que_id)
|
|
13853
|
+
if not declaration:
|
|
13854
|
+
read_errors.append(
|
|
13855
|
+
{
|
|
13856
|
+
"view_key": view_key,
|
|
13857
|
+
"name": view_name,
|
|
13858
|
+
"error_code": "VIEW_NOT_EXTRACTABLE",
|
|
13859
|
+
"message": "view configuration cannot be represented completely by the public contract; see gaps",
|
|
13860
|
+
"gaps": gaps,
|
|
13861
|
+
}
|
|
13862
|
+
)
|
|
13863
|
+
continue
|
|
13864
|
+
if gaps:
|
|
13865
|
+
all_gaps.append({"view_key": view_key, "gaps": gaps})
|
|
13866
|
+
projected.append(declaration)
|
|
13867
|
+
gap_summary: dict[str, int] = {}
|
|
13868
|
+
for item in all_gaps:
|
|
13869
|
+
for kind, count in summarize_gaps(item["gaps"]).items():
|
|
13870
|
+
gap_summary[kind] = gap_summary.get(kind, 0) + count
|
|
13871
|
+
context = views_schema_registry().resolve_schema_version(schema_version)
|
|
13872
|
+
return {
|
|
13873
|
+
"status": "success",
|
|
13874
|
+
"error_code": None,
|
|
13875
|
+
"recoverable": False,
|
|
13876
|
+
"message": "read canonical views declarations (complete)",
|
|
13877
|
+
"normalized_args": normalized_args,
|
|
13878
|
+
"missing_fields": [],
|
|
13879
|
+
"allowed_values": {"view_types": [member.value for member in PublicViewType]},
|
|
13880
|
+
"details": {
|
|
13881
|
+
"extraction_gap_summary": gap_summary,
|
|
13882
|
+
"extraction_gaps": all_gaps[:50],
|
|
13883
|
+
"config_read_errors": read_errors[:50],
|
|
13884
|
+
"config_read_error_count": len(read_errors),
|
|
13885
|
+
},
|
|
13886
|
+
"request_id": None,
|
|
13887
|
+
"suggested_next_call": None,
|
|
13888
|
+
"warnings": [],
|
|
13889
|
+
"verification": {"views_read": len(projected), "views_with_gaps": len(all_gaps), "read_errors": len(read_errors)},
|
|
13890
|
+
"schemaVersion": context.schema_version,
|
|
13891
|
+
"apiVersion": context.api_version,
|
|
13892
|
+
"latestVersion": views_schema_registry().latest_version,
|
|
13893
|
+
"app_key": app_key,
|
|
13894
|
+
"views": projected,
|
|
13895
|
+
}
|
|
13896
|
+
|
|
13897
|
+
def app_views_validate(
|
|
13898
|
+
self,
|
|
13899
|
+
*,
|
|
13900
|
+
declaration: JSONObject,
|
|
13901
|
+
schema_version: str | None = None,
|
|
13902
|
+
) -> JSONObject:
|
|
13903
|
+
"""纯离线校验一条视图声明;不访问后端。"""
|
|
13904
|
+
normalized_args = {"schema_version": schema_version}
|
|
13905
|
+
resolved_version = str(schema_version or (declaration.get("schema_version") if isinstance(declaration, dict) else None) or "").strip() or None
|
|
13906
|
+
try:
|
|
13907
|
+
context = views_schema_registry().resolve_schema_version(resolved_version)
|
|
13908
|
+
except ViewsContractError as error:
|
|
13909
|
+
return _failed(
|
|
13910
|
+
error.error_code,
|
|
13911
|
+
str(error.message),
|
|
13912
|
+
normalized_args=normalized_args,
|
|
13913
|
+
details={"reason_path": error.path or "", **(error.details or {})},
|
|
13914
|
+
suggested_next_call={"tool_name": "app_views_schema", "arguments": {"schema_kind": "complete"}},
|
|
13915
|
+
)
|
|
13916
|
+
issues: list[dict[str, Any]] = []
|
|
13917
|
+
batch_keys = ("views", "upsert_views", "upsertViews", "apps")
|
|
13918
|
+
if not isinstance(declaration, dict):
|
|
13919
|
+
issues.append({"path": "/", "code": "INVALID_SPEC", "message": "views declaration must be a JSON object"})
|
|
13920
|
+
elif any(key in declaration for key in batch_keys):
|
|
13921
|
+
issues.append(
|
|
13922
|
+
{
|
|
13923
|
+
"path": "/",
|
|
13924
|
+
"code": "VIEW_BATCH_CREATE_UNSUPPORTED",
|
|
13925
|
+
"message": "views schema validates one view declaration; batch envelopes are unsupported",
|
|
13926
|
+
}
|
|
13927
|
+
)
|
|
13928
|
+
else:
|
|
13929
|
+
declaration_item = {
|
|
13930
|
+
key: value
|
|
13931
|
+
for key, value in declaration.items()
|
|
13932
|
+
if key not in {"schema_version", "schemaVersion", "apiVersion"}
|
|
13933
|
+
}
|
|
13934
|
+
try:
|
|
13935
|
+
parsed = ViewUpsertPatch.model_validate(declaration_item)
|
|
13936
|
+
context.validate_declaration(compact_none_values(public_view_upsert_payload(parsed)))
|
|
13937
|
+
except ValidationError as exc:
|
|
13938
|
+
first_error = exc.errors()[0]
|
|
13939
|
+
issues.append(
|
|
13940
|
+
{
|
|
13941
|
+
"path": f"/{'/'.join(str(part) for part in first_error.get('loc', ()))}".rstrip("/"),
|
|
13942
|
+
"code": "INVALID_SPEC",
|
|
13943
|
+
"message": str(first_error.get("msg") or exc),
|
|
13944
|
+
}
|
|
13945
|
+
)
|
|
13946
|
+
except ViewsContractError as error:
|
|
13947
|
+
issues.append({"path": error.path or "/", "code": error.error_code, "message": str(error.message)})
|
|
13948
|
+
ok = not issues
|
|
13949
|
+
return {
|
|
13950
|
+
"status": "success" if ok else "failed",
|
|
13951
|
+
"error_code": None if ok else issues[0]["code"],
|
|
13952
|
+
"recoverable": True,
|
|
13953
|
+
"message": "views declaration is valid" if ok else "views declaration failed validation",
|
|
13954
|
+
"normalized_args": normalized_args,
|
|
13955
|
+
"missing_fields": [],
|
|
13956
|
+
"allowed_values": {},
|
|
13957
|
+
"details": {"issues": issues},
|
|
13958
|
+
"request_id": None,
|
|
13959
|
+
"suggested_next_call": None,
|
|
13960
|
+
"warnings": [],
|
|
13961
|
+
"verification": {"validated_entries": 1 if ok else 0},
|
|
13962
|
+
"ok": ok,
|
|
13963
|
+
"schemaVersion": context.schema_version,
|
|
13964
|
+
"apiVersion": context.api_version,
|
|
13965
|
+
"issues": issues,
|
|
13966
|
+
}
|
|
13967
|
+
|
|
13456
13968
|
def app_publish_verify(
|
|
13457
13969
|
self,
|
|
13458
13970
|
*,
|
|
@@ -15429,6 +15941,76 @@ class AiBuilderFacade:
|
|
|
15429
15941
|
result = legacy_normalized or normalized_views
|
|
15430
15942
|
return (result, False, None) if include_error else (result, False)
|
|
15431
15943
|
|
|
15944
|
+
def _set_view_pinned(
|
|
15945
|
+
self,
|
|
15946
|
+
*,
|
|
15947
|
+
profile: str,
|
|
15948
|
+
app_key: str,
|
|
15949
|
+
view_key: str,
|
|
15950
|
+
pinned: bool,
|
|
15951
|
+
) -> dict[str, Any]:
|
|
15952
|
+
"""Move one view between the application-level fixed and ordinary lists."""
|
|
15953
|
+
target_key = str(view_key or "").strip()
|
|
15954
|
+
if not target_key:
|
|
15955
|
+
raise ValueError("view_key is required to update the pinned state")
|
|
15956
|
+
response = self.views.view_list(profile=profile, app_key=app_key)
|
|
15957
|
+
raw_groups = response.get("result") if isinstance(response, dict) else None
|
|
15958
|
+
if not isinstance(raw_groups, list):
|
|
15959
|
+
raise ValueError("view list does not expose ordinal groups")
|
|
15960
|
+
|
|
15961
|
+
group_keys: dict[str, list[str]] = {
|
|
15962
|
+
"FIXED_VIEW_LIST": [],
|
|
15963
|
+
"VIEW_LIST": [],
|
|
15964
|
+
}
|
|
15965
|
+
for group in raw_groups:
|
|
15966
|
+
if not isinstance(group, dict):
|
|
15967
|
+
continue
|
|
15968
|
+
ordinal_type = str(group.get("ordinalType") or "").strip()
|
|
15969
|
+
if ordinal_type not in group_keys:
|
|
15970
|
+
continue
|
|
15971
|
+
view_list = group.get("viewList")
|
|
15972
|
+
if not isinstance(view_list, list):
|
|
15973
|
+
continue
|
|
15974
|
+
group_keys[ordinal_type] = [
|
|
15975
|
+
key
|
|
15976
|
+
for item in view_list
|
|
15977
|
+
if isinstance(item, dict)
|
|
15978
|
+
and (key := _extract_view_key(item))
|
|
15979
|
+
and key != target_key
|
|
15980
|
+
]
|
|
15981
|
+
|
|
15982
|
+
target_group = "FIXED_VIEW_LIST" if pinned else "VIEW_LIST"
|
|
15983
|
+
group_keys[target_group].append(target_key)
|
|
15984
|
+
self.views.view_reorder(
|
|
15985
|
+
profile=profile,
|
|
15986
|
+
app_key=app_key,
|
|
15987
|
+
payload=[
|
|
15988
|
+
{"ordinalType": "FIXED_VIEW_LIST", "viewKeyList": group_keys["FIXED_VIEW_LIST"]},
|
|
15989
|
+
{"ordinalType": "VIEW_LIST", "viewKeyList": group_keys["VIEW_LIST"]},
|
|
15990
|
+
],
|
|
15991
|
+
)
|
|
15992
|
+
|
|
15993
|
+
readback = self.views.view_list(profile=profile, app_key=app_key)
|
|
15994
|
+
readback_groups = readback.get("result") if isinstance(readback, dict) else None
|
|
15995
|
+
actual_group: str | None = None
|
|
15996
|
+
if isinstance(readback_groups, list):
|
|
15997
|
+
for group in readback_groups:
|
|
15998
|
+
if not isinstance(group, dict):
|
|
15999
|
+
continue
|
|
16000
|
+
ordinal_type = str(group.get("ordinalType") or "").strip()
|
|
16001
|
+
for item in group.get("viewList") or []:
|
|
16002
|
+
if isinstance(item, dict) and _extract_view_key(item) == target_key:
|
|
16003
|
+
actual_group = ordinal_type
|
|
16004
|
+
break
|
|
16005
|
+
if actual_group is not None:
|
|
16006
|
+
break
|
|
16007
|
+
return {
|
|
16008
|
+
"requested_pinned": pinned,
|
|
16009
|
+
"actual_pinned": actual_group == "FIXED_VIEW_LIST" if actual_group is not None else None,
|
|
16010
|
+
"verified": actual_group == target_group,
|
|
16011
|
+
"actual_ordinal_type": actual_group,
|
|
16012
|
+
}
|
|
16013
|
+
|
|
15432
16014
|
def _load_workflow_result(
|
|
15433
16015
|
self,
|
|
15434
16016
|
*,
|
|
@@ -19226,40 +19808,44 @@ def _chart_apply_batching_context(
|
|
|
19226
19808
|
def _view_apply_failure_diagnostics(
|
|
19227
19809
|
*,
|
|
19228
19810
|
patch: ViewUpsertPatch,
|
|
19229
|
-
|
|
19811
|
+
fields_by_que_id: dict[int, dict[str, Any]],
|
|
19230
19812
|
backend_code: Any,
|
|
19231
19813
|
operation_phase: str,
|
|
19232
19814
|
) -> dict[str, Any]:
|
|
19233
|
-
references: list[tuple[
|
|
19815
|
+
references: list[tuple[int, str]] = []
|
|
19234
19816
|
|
|
19235
|
-
def add_reference(
|
|
19236
|
-
|
|
19237
|
-
if
|
|
19238
|
-
references.append((
|
|
19817
|
+
def add_reference(value: Any, source: str) -> None:
|
|
19818
|
+
que_id = _coerce_positive_int(value)
|
|
19819
|
+
if que_id is not None:
|
|
19820
|
+
references.append((que_id, source))
|
|
19239
19821
|
|
|
19240
19822
|
for column in patch.columns:
|
|
19241
19823
|
add_reference(column, "columns")
|
|
19242
|
-
add_reference(patch.
|
|
19243
|
-
add_reference(patch.
|
|
19244
|
-
add_reference(patch.
|
|
19245
|
-
add_reference(patch.
|
|
19246
|
-
for
|
|
19247
|
-
|
|
19824
|
+
add_reference(patch.group_que_id, "group_que_id")
|
|
19825
|
+
add_reference(patch.start_que_id, "start_que_id")
|
|
19826
|
+
add_reference(patch.end_que_id, "end_que_id")
|
|
19827
|
+
add_reference(patch.title_que_id, "title_que_id")
|
|
19828
|
+
for item in patch.filters:
|
|
19829
|
+
if isinstance(item, list):
|
|
19830
|
+
for rule in item:
|
|
19831
|
+
add_reference(getattr(rule, "que_id", None), "filters")
|
|
19832
|
+
else:
|
|
19833
|
+
add_reference(getattr(item, "que_id", None), "filters")
|
|
19248
19834
|
|
|
19249
|
-
seen: set[str] = set()
|
|
19835
|
+
seen: set[tuple[int, str]] = set()
|
|
19250
19836
|
field_entries: list[dict[str, Any]] = []
|
|
19251
|
-
for
|
|
19252
|
-
key =
|
|
19837
|
+
for que_id, source in references:
|
|
19838
|
+
key = (que_id, source)
|
|
19253
19839
|
if key in seen:
|
|
19254
19840
|
continue
|
|
19255
19841
|
seen.add(key)
|
|
19256
|
-
field =
|
|
19842
|
+
field = fields_by_que_id.get(que_id) or {}
|
|
19257
19843
|
field_entries.append(
|
|
19258
19844
|
{
|
|
19259
|
-
"field_name":
|
|
19845
|
+
"field_name": str(field.get("name") or que_id),
|
|
19260
19846
|
"source": source,
|
|
19261
19847
|
"field_id": field.get("field_id"),
|
|
19262
|
-
"que_id": field.get("que_id"),
|
|
19848
|
+
"que_id": field.get("que_id") or que_id,
|
|
19263
19849
|
"field_type": field.get("type"),
|
|
19264
19850
|
"que_type": field.get("que_type"),
|
|
19265
19851
|
"exists_in_schema": bool(field),
|
|
@@ -19277,7 +19863,7 @@ def _view_apply_failure_diagnostics(
|
|
|
19277
19863
|
"recommended_minimal_retry": {
|
|
19278
19864
|
"name": patch.name,
|
|
19279
19865
|
"type": patch.type.value,
|
|
19280
|
-
"columns": [field_entries[0]["
|
|
19866
|
+
"columns": [field_entries[0]["que_id"]] if field_entries else [],
|
|
19281
19867
|
**({"filters": [rule.model_dump(mode="json", exclude_none=True) for rule in patch.filters]} if patch.filters else {}),
|
|
19282
19868
|
},
|
|
19283
19869
|
}
|
|
@@ -21186,8 +21772,10 @@ def _infer_field_type(question: dict[str, Any]) -> str:
|
|
|
21186
21772
|
|
|
21187
21773
|
|
|
21188
21774
|
def _parse_field(question: dict[str, Any], *, field_id_hint: str | None = None) -> dict[str, Any]:
|
|
21189
|
-
name = str(question.get("queTitle") or "").strip()
|
|
21190
21775
|
que_id = _coerce_positive_int(question.get("queId"))
|
|
21776
|
+
# 无标题字段用「字段{que_id}」兜底命名,与提取器(eval_replay/views get)对称,
|
|
21777
|
+
# 否则视图列引用无标题字段时 UNKNOWN_VIEW_FIELD
|
|
21778
|
+
name = str(question.get("queTitle") or "").strip() or (f"字段{que_id}" if que_id else "")
|
|
21191
21779
|
que_type = _coerce_positive_int(question.get("queType"))
|
|
21192
21780
|
field_type = _infer_field_type(question)
|
|
21193
21781
|
field_id = field_id_hint or f"field_{que_id or _slugify(name, default='x')}"
|
|
@@ -21357,6 +21945,17 @@ def _parse_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
|
|
21357
21945
|
for question in inner_row or []:
|
|
21358
21946
|
if not isinstance(question, dict):
|
|
21359
21947
|
continue
|
|
21948
|
+
if _coerce_positive_int(question.get("queType")) == 24:
|
|
21949
|
+
# 嵌套段落:不注册段落本身,递归解析其 innerQuestions
|
|
21950
|
+
for nested_row in question.get("innerQuestions", []) or []:
|
|
21951
|
+
for nested_question in (nested_row if isinstance(nested_row, list) else [nested_row]):
|
|
21952
|
+
if not isinstance(nested_question, dict):
|
|
21953
|
+
continue
|
|
21954
|
+
nested_field = _parse_field(nested_question)
|
|
21955
|
+
if str(nested_field["name"] or "").strip():
|
|
21956
|
+
fields_by_name.setdefault(nested_field["name"], nested_field)
|
|
21957
|
+
labels.append(nested_field["name"])
|
|
21958
|
+
continue
|
|
21360
21959
|
field = _parse_field(question)
|
|
21361
21960
|
fields_by_name.setdefault(field["name"], field)
|
|
21362
21961
|
labels.append(field["name"])
|
|
@@ -21382,8 +21981,16 @@ def _parse_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
|
|
21382
21981
|
for question in row:
|
|
21383
21982
|
if not isinstance(question, dict):
|
|
21384
21983
|
continue
|
|
21984
|
+
if _coerce_positive_int(question.get("queType")) == 24:
|
|
21985
|
+
# 同行混入段落容器:不注册段落本身(它的子字段在自己 row),跳过避免同名占位挡住真实字段
|
|
21986
|
+
continue
|
|
21385
21987
|
field = _parse_field(question)
|
|
21386
|
-
fields_by_name.
|
|
21988
|
+
existing = fields_by_name.get(field["name"])
|
|
21989
|
+
# 同名冲突:保留信息更丰富的那个(表格字段带 subfields,被同名普通字段覆盖会丢子字段引用面);
|
|
21990
|
+
# 但 queId 索引必须收录全部同名字段(_collect_fields_by_que_id 按 queId 消费,同名不同 id 各自有效)
|
|
21991
|
+
if existing is None or (not existing.get("subfields") and field.get("subfields")):
|
|
21992
|
+
fields_by_name[field["name"]] = field
|
|
21993
|
+
|
|
21387
21994
|
labels.append(field["name"])
|
|
21388
21995
|
if labels:
|
|
21389
21996
|
root_rows.append(labels)
|
|
@@ -26066,7 +26673,11 @@ def _merge_view_summary_with_config(
|
|
|
26066
26673
|
return summary
|
|
26067
26674
|
if isinstance(config.get("auth"), dict):
|
|
26068
26675
|
summary["visibility_summary"] = _visibility_summary(_public_visibility_from_member_auth(config.get("auth")))
|
|
26069
|
-
legacy_columns = [
|
|
26676
|
+
legacy_columns = [
|
|
26677
|
+
_SYSTEM_VIEW_QUE_NAMES.get(value if isinstance(value, int) else None, str(value))
|
|
26678
|
+
for value in (summary.get("columns") or [])
|
|
26679
|
+
if str(value or "").strip()
|
|
26680
|
+
]
|
|
26070
26681
|
question_entries = _extract_view_question_entries(config.get("viewgraphQuestions"))
|
|
26071
26682
|
canonical_question_entries = _extract_view_question_entries(question_list)
|
|
26072
26683
|
question_entries_by_id = {
|
|
@@ -26167,7 +26778,7 @@ def _merge_view_summary_with_config(
|
|
|
26167
26778
|
and str(entry.get("name") or "").strip()
|
|
26168
26779
|
and str(entry.get("name") or "").strip() not in _KNOWN_SYSTEM_VIEW_COLUMNS
|
|
26169
26780
|
]
|
|
26170
|
-
summary["columns"] =
|
|
26781
|
+
summary["columns"] = canonical_display_column_ids
|
|
26171
26782
|
summary["display_columns"] = canonical_display_columns
|
|
26172
26783
|
summary["display_column_ids"] = canonical_display_column_ids
|
|
26173
26784
|
summary["column_details"] = canonical_display_entries
|
|
@@ -26212,7 +26823,7 @@ def _merge_view_summary_with_config(
|
|
|
26212
26823
|
)
|
|
26213
26824
|
summary["filters"] = _public_view_filter_groups_from_match_rules(
|
|
26214
26825
|
config.get("viewgraphLimit"),
|
|
26215
|
-
|
|
26826
|
+
fields_by_que_id=field_lookup,
|
|
26216
26827
|
)
|
|
26217
26828
|
config_enriched = True
|
|
26218
26829
|
if any(key in config for key in ("asosChartVisible", "asosChartConfig", "asosChartIdList", "limitType")):
|
|
@@ -26455,12 +27066,16 @@ def _normalize_print_tpls(value: Any) -> list[dict[str, Any]]:
|
|
|
26455
27066
|
items: list[dict[str, Any]] = []
|
|
26456
27067
|
for item in value:
|
|
26457
27068
|
if isinstance(item, dict):
|
|
26458
|
-
tpl_id = item.get("
|
|
27069
|
+
tpl_id = item.get("printKey")
|
|
27070
|
+
if tpl_id is None:
|
|
27071
|
+
tpl_id = item.get("printTplId")
|
|
26459
27072
|
if tpl_id is None:
|
|
26460
27073
|
tpl_id = item.get("templateId")
|
|
26461
27074
|
if tpl_id is None:
|
|
26462
27075
|
tpl_id = item.get("id")
|
|
26463
|
-
tpl_name = item.get("
|
|
27076
|
+
tpl_name = item.get("printName")
|
|
27077
|
+
if tpl_name is None:
|
|
27078
|
+
tpl_name = item.get("printTplName")
|
|
26464
27079
|
if tpl_name is None:
|
|
26465
27080
|
tpl_name = item.get("templateName")
|
|
26466
27081
|
if tpl_name is None:
|
|
@@ -26904,7 +27519,7 @@ def _build_grouped_view_button_config(button_config_dtos: list[dict[str, Any]])
|
|
|
26904
27519
|
|
|
26905
27520
|
def _build_view_button_dtos(
|
|
26906
27521
|
*,
|
|
26907
|
-
|
|
27522
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
26908
27523
|
bindings: list[ViewButtonBindingPatch],
|
|
26909
27524
|
valid_custom_button_ids: set[int],
|
|
26910
27525
|
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
@@ -26913,7 +27528,7 @@ def _build_view_button_dtos(
|
|
|
26913
27528
|
for binding in bindings:
|
|
26914
27529
|
dto, binding_issues = _serialize_view_button_binding(
|
|
26915
27530
|
binding=binding,
|
|
26916
|
-
|
|
27531
|
+
fields_by_que_id=fields_by_que_id,
|
|
26917
27532
|
valid_custom_button_ids=valid_custom_button_ids,
|
|
26918
27533
|
)
|
|
26919
27534
|
if binding_issues:
|
|
@@ -26926,7 +27541,7 @@ def _build_view_button_dtos(
|
|
|
26926
27541
|
def _serialize_view_button_binding(
|
|
26927
27542
|
*,
|
|
26928
27543
|
binding: ViewButtonBindingPatch,
|
|
26929
|
-
|
|
27544
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
26930
27545
|
valid_custom_button_ids: set[int],
|
|
26931
27546
|
allow_unverified_custom_button_id: bool = False,
|
|
26932
27547
|
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
@@ -26944,7 +27559,7 @@ def _serialize_view_button_binding(
|
|
|
26944
27559
|
}
|
|
26945
27560
|
]
|
|
26946
27561
|
translated_limits, limit_issues = _build_view_button_limit_groups(
|
|
26947
|
-
|
|
27562
|
+
fields_by_que_id=fields_by_que_id,
|
|
26948
27563
|
groups=binding.button_limit,
|
|
26949
27564
|
)
|
|
26950
27565
|
if limit_issues:
|
|
@@ -26970,7 +27585,7 @@ def _serialize_view_button_binding(
|
|
|
26970
27585
|
|
|
26971
27586
|
def _build_view_button_limit_groups(
|
|
26972
27587
|
*,
|
|
26973
|
-
|
|
27588
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
26974
27589
|
groups: list[list[ViewFilterRulePatch]],
|
|
26975
27590
|
) -> tuple[list[list[dict[str, Any]]], list[dict[str, Any]]]:
|
|
26976
27591
|
translated_groups: list[list[dict[str, Any]]] = []
|
|
@@ -26984,14 +27599,14 @@ def _build_view_button_limit_groups(
|
|
|
26984
27599
|
raw_rule = raw_rule.model_dump(mode="json")
|
|
26985
27600
|
if not isinstance(raw_rule, dict):
|
|
26986
27601
|
continue
|
|
26987
|
-
|
|
26988
|
-
field =
|
|
27602
|
+
rule_que_id = _coerce_positive_int(raw_rule.get("que_id"))
|
|
27603
|
+
field = fields_by_que_id.get(rule_que_id) if rule_que_id is not None else None
|
|
26989
27604
|
if field is None:
|
|
26990
27605
|
issues.append(
|
|
26991
27606
|
{
|
|
26992
27607
|
"error_code": "UNKNOWN_VIEW_FIELD",
|
|
26993
|
-
"missing_fields": [
|
|
26994
|
-
"reason_path": "buttons[].button_limit[].
|
|
27608
|
+
"missing_fields": [str(raw_rule.get("que_id") or "")],
|
|
27609
|
+
"reason_path": "buttons[].button_limit[].que_id",
|
|
26995
27610
|
}
|
|
26996
27611
|
)
|
|
26997
27612
|
continue
|
|
@@ -27162,7 +27777,7 @@ def _custom_button_view_configs_from_view_summaries(views: list[dict[str, Any]])
|
|
|
27162
27777
|
placement = _public_view_button_placement(entry.get("config_type")) or entry.get("placement")
|
|
27163
27778
|
raw_button_limit = deepcopy(entry.get("button_limit") or [])
|
|
27164
27779
|
public_button_limit = (
|
|
27165
|
-
_public_view_filter_groups_from_match_rules(raw_button_limit,
|
|
27780
|
+
_public_view_filter_groups_from_match_rules(raw_button_limit, fields_by_que_id=field_lookup)
|
|
27166
27781
|
if raw_button_limit
|
|
27167
27782
|
else []
|
|
27168
27783
|
)
|
|
@@ -27360,6 +27975,11 @@ def _field_id_for_name(fields: list[dict[str, Any]], name: str) -> str:
|
|
|
27360
27975
|
for field in fields:
|
|
27361
27976
|
if field["name"] == name:
|
|
27362
27977
|
return field["field_id"]
|
|
27978
|
+
# 表格子字段是视图列/筛选的合法引用面:顶层未命中时递归 subfields
|
|
27979
|
+
for field in fields:
|
|
27980
|
+
for sub in field.get("subfields") or []:
|
|
27981
|
+
if isinstance(sub, dict) and sub.get("name") == name:
|
|
27982
|
+
return str(sub.get("field_id") or f"field_{sub.get('que_id') or ''}")
|
|
27363
27983
|
raise KeyError(name)
|
|
27364
27984
|
|
|
27365
27985
|
|
|
@@ -27459,7 +28079,7 @@ def _build_public_workflow_spec(*, nodes: list[dict[str, Any]], transitions: lis
|
|
|
27459
28079
|
|
|
27460
28080
|
def _build_flow_condition_matrix(
|
|
27461
28081
|
*,
|
|
27462
|
-
|
|
28082
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
27463
28083
|
node: dict[str, Any],
|
|
27464
28084
|
) -> tuple[list[list[dict[str, Any]]], list[dict[str, Any]]]:
|
|
27465
28085
|
if str(node.get("type") or "") != "condition":
|
|
@@ -27477,7 +28097,7 @@ def _build_flow_condition_matrix(
|
|
|
27477
28097
|
if not isinstance(raw_rule, dict):
|
|
27478
28098
|
continue
|
|
27479
28099
|
field_name = str(raw_rule.get("field_name") or "").strip()
|
|
27480
|
-
field =
|
|
28100
|
+
field = fields_by_que_id.get(field_name)
|
|
27481
28101
|
if field is None:
|
|
27482
28102
|
issues.append(
|
|
27483
28103
|
{
|
|
@@ -27524,7 +28144,9 @@ def _translate_flow_condition_rule(*, field: dict[str, Any], rule: dict[str, Any
|
|
|
27524
28144
|
base["judgeType"] = JUDGE_LESS_OR_EQUAL
|
|
27525
28145
|
base["judgeValues"] = [_stringify_condition_value(values[0])]
|
|
27526
28146
|
elif operator == "is_empty":
|
|
27527
|
-
|
|
28147
|
+
# 选项/成员/部门类字段的空值,后端历史形态是 EQUAL_ANY(9)+空值(该类字段用
|
|
28148
|
+
# ANY 语义选值,"没选任何值"也是 9 判型);文本/数字类才是 EQUAL(0)+空值
|
|
28149
|
+
base["judgeType"] = JUDGE_EQUAL_ANY if field_type in INCLUDE_ANY_FLOW_FIELD_TYPES or field_type in {FieldType.single_select.value} else JUDGE_EQUAL
|
|
27528
28150
|
base["judgeValues"] = []
|
|
27529
28151
|
elif operator == "not_empty":
|
|
27530
28152
|
base["judgeType"] = JUDGE_UNEQUAL
|
|
@@ -27553,7 +28175,7 @@ def _empty_view_query_conditions_payload() -> dict[str, Any]:
|
|
|
27553
28175
|
|
|
27554
28176
|
def _build_view_query_conditions_payload(
|
|
27555
28177
|
*,
|
|
27556
|
-
|
|
28178
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
27557
28179
|
query_conditions: Any,
|
|
27558
28180
|
) -> tuple[dict[str, Any] | None, dict[str, Any] | None, list[dict[str, Any]]]:
|
|
27559
28181
|
if query_conditions is None:
|
|
@@ -27593,7 +28215,7 @@ def _build_view_query_conditions_payload(
|
|
|
27593
28215
|
compiled_row: list[int] = []
|
|
27594
28216
|
for column_index, selector in enumerate(row):
|
|
27595
28217
|
field, issue = _resolve_query_condition_field(
|
|
27596
|
-
|
|
28218
|
+
fields_by_que_id=fields_by_que_id,
|
|
27597
28219
|
selector=selector,
|
|
27598
28220
|
reason_path=f"query_conditions.rows[{row_index}][{column_index}]",
|
|
27599
28221
|
)
|
|
@@ -27611,14 +28233,16 @@ def _build_view_query_conditions_payload(
|
|
|
27611
28233
|
"queryConditionStatus": enabled,
|
|
27612
28234
|
"queryConditionExact": exact,
|
|
27613
28235
|
"hideBeforeQueryCondition": hide_before_query,
|
|
27614
|
-
|
|
28236
|
+
# 后端 ViewgraphInfo.queryCondition 是 List<ViewgraphQueryConditionConfig>({title?,queIds}),
|
|
28237
|
+
# 不能发裸 [[queId]](fastjson expect {, actual [ → 发现 #7)
|
|
28238
|
+
"queryCondition": [{"queIds": row} for row in rows] if enabled else [],
|
|
27615
28239
|
}
|
|
27616
28240
|
return payload, _normalize_view_query_conditions_for_compare(payload), []
|
|
27617
28241
|
|
|
27618
28242
|
|
|
27619
28243
|
def _resolve_query_condition_field(
|
|
27620
28244
|
*,
|
|
27621
|
-
|
|
28245
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
27622
28246
|
selector: Any,
|
|
27623
28247
|
reason_path: str,
|
|
27624
28248
|
) -> tuple[dict[str, Any], dict[str, Any] | None]:
|
|
@@ -27630,8 +28254,8 @@ def _resolve_query_condition_field(
|
|
|
27630
28254
|
"missing_fields": [],
|
|
27631
28255
|
"message": "query condition field selector cannot be empty",
|
|
27632
28256
|
}
|
|
27633
|
-
fields = [field for field in
|
|
27634
|
-
field =
|
|
28257
|
+
fields = [field for field in fields_by_que_id.values() if isinstance(field, dict)]
|
|
28258
|
+
field = fields_by_que_id.get(raw)
|
|
27635
28259
|
if field is None:
|
|
27636
28260
|
for candidate in fields:
|
|
27637
28261
|
if raw == str(candidate.get("field_id") or "").strip():
|
|
@@ -27642,7 +28266,7 @@ def _resolve_query_condition_field(
|
|
|
27642
28266
|
field = candidate
|
|
27643
28267
|
break
|
|
27644
28268
|
if field is None:
|
|
27645
|
-
subfield_parent = _find_subtable_subfield_parent(
|
|
28269
|
+
subfield_parent = _find_subtable_subfield_parent(fields_by_que_id=fields_by_que_id, selector=raw)
|
|
27646
28270
|
if subfield_parent is not None:
|
|
27647
28271
|
return {}, {
|
|
27648
28272
|
"error_code": "INVALID_QUERY_CONDITION_FIELD",
|
|
@@ -27690,10 +28314,10 @@ def _resolve_query_condition_field(
|
|
|
27690
28314
|
|
|
27691
28315
|
def _find_subtable_subfield_parent(
|
|
27692
28316
|
*,
|
|
27693
|
-
|
|
28317
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
27694
28318
|
selector: str,
|
|
27695
28319
|
) -> dict[str, Any] | None:
|
|
27696
|
-
for field in
|
|
28320
|
+
for field in fields_by_que_id.values():
|
|
27697
28321
|
if not isinstance(field, dict) or str(field.get("type") or "") != FieldType.subtable.value:
|
|
27698
28322
|
continue
|
|
27699
28323
|
for subfield in field.get("subfields") or []:
|
|
@@ -27754,6 +28378,9 @@ def _normalize_view_query_conditions_for_compare(config: Any) -> dict[str, Any]:
|
|
|
27754
28378
|
rows: list[list[int]] = []
|
|
27755
28379
|
if isinstance(raw_rows, list):
|
|
27756
28380
|
for raw_row in raw_rows:
|
|
28381
|
+
if isinstance(raw_row, dict):
|
|
28382
|
+
# ViewgraphQueryConditionConfig 形态({title?,queIds})
|
|
28383
|
+
raw_row = raw_row.get("queIds") or []
|
|
27757
28384
|
if not isinstance(raw_row, list):
|
|
27758
28385
|
continue
|
|
27759
28386
|
compiled_row = [
|
|
@@ -27816,11 +28443,18 @@ def _build_view_associated_resources_payload(
|
|
|
27816
28443
|
]
|
|
27817
28444
|
available_by_id = _associated_resource_index(available_resources)
|
|
27818
28445
|
if limit_type == "all":
|
|
28446
|
+
# limitType=ALL 时后端保留原 idList 不清(语义忽略但存储保留);
|
|
28447
|
+
# 透传声明的 id 列表,避免 apply 清空→回读 idList 不匹配→READBACK_MISMATCH
|
|
28448
|
+
all_ids = [
|
|
28449
|
+
item_id
|
|
28450
|
+
for item_id in (_coerce_positive_int(raw) for raw in raw_item_ids)
|
|
28451
|
+
if item_id is not None
|
|
28452
|
+
]
|
|
27819
28453
|
payload = {
|
|
27820
28454
|
"asosChartVisible": True,
|
|
27821
28455
|
"asosChartConfig": {
|
|
27822
28456
|
"limitType": ASSOCIATED_RESOURCE_LIMIT_TYPE_ALL,
|
|
27823
|
-
"asosChartIdList":
|
|
28457
|
+
"asosChartIdList": all_ids,
|
|
27824
28458
|
},
|
|
27825
28459
|
}
|
|
27826
28460
|
expected = _extract_view_associated_resources_config(payload, available_resources=available_resources)
|
|
@@ -27841,26 +28475,19 @@ def _build_view_associated_resources_payload(
|
|
|
27841
28475
|
]
|
|
27842
28476
|
item_ids.append(item_id)
|
|
27843
28477
|
missing_ids = [item_id for item_id in item_ids if item_id not in available_by_id]
|
|
27844
|
-
|
|
27845
|
-
|
|
27846
|
-
|
|
27847
|
-
"error_code": "ASSOCIATED_RESOURCE_NOT_FOUND",
|
|
27848
|
-
"reason_path": "associated_resources.associated_item_ids",
|
|
27849
|
-
"missing_fields": [],
|
|
27850
|
-
"invalid_associated_item_ids": missing_ids,
|
|
27851
|
-
"available_associated_item_ids": sorted(available_by_id),
|
|
27852
|
-
"message": "associated_resource references ids that are not in the app-level associated resource pool",
|
|
27853
|
-
"next_action": "prefer app_associated_resources_apply for associated report/view display; it can resolve associated_item_id, chart_id/chart_key, or view_key",
|
|
27854
|
-
}
|
|
27855
|
-
]
|
|
28478
|
+
# 兼容存量脏引用:关联资源 id 指向闭包外/已下架的图表或视图时,剔除后继续写而非整视图失败;
|
|
28479
|
+
# 剔除明细记入 expected 供回读校验与响应展示
|
|
28480
|
+
resolvable_ids = [item_id for item_id in item_ids if item_id in available_by_id]
|
|
27856
28481
|
payload = {
|
|
27857
28482
|
"asosChartVisible": True,
|
|
27858
28483
|
"asosChartConfig": {
|
|
27859
28484
|
"limitType": ASSOCIATED_RESOURCE_LIMIT_TYPE_SELECT,
|
|
27860
|
-
"asosChartIdList":
|
|
28485
|
+
"asosChartIdList": resolvable_ids,
|
|
27861
28486
|
},
|
|
27862
28487
|
}
|
|
27863
28488
|
expected = _extract_view_associated_resources_config(payload, available_resources=available_resources)
|
|
28489
|
+
if isinstance(expected, dict) and missing_ids:
|
|
28490
|
+
expected["dropped_unresolvable_associated_item_ids"] = missing_ids
|
|
27864
28491
|
return payload, expected, []
|
|
27865
28492
|
|
|
27866
28493
|
|
|
@@ -28597,6 +29224,10 @@ _VIEW_PARTIAL_PATCH_KEY_ALIASES = {
|
|
|
28597
29224
|
"view_key": "view_key",
|
|
28598
29225
|
"viewKey": "view_key",
|
|
28599
29226
|
"viewgraphKey": "view_key",
|
|
29227
|
+
"pinned": "pinned",
|
|
29228
|
+
"fixed": "pinned",
|
|
29229
|
+
"is_pinned": "pinned",
|
|
29230
|
+
"isPinned": "pinned",
|
|
28600
29231
|
"type": "type",
|
|
28601
29232
|
"view_type": "type",
|
|
28602
29233
|
"viewType": "type",
|
|
@@ -28604,17 +29235,25 @@ _VIEW_PARTIAL_PATCH_KEY_ALIASES = {
|
|
|
28604
29235
|
"column_names": "columns",
|
|
28605
29236
|
"columnNames": "columns",
|
|
28606
29237
|
"fields": "columns",
|
|
28607
|
-
"
|
|
28608
|
-
"
|
|
29238
|
+
"group_que_id": "group_que_id",
|
|
29239
|
+
"groupQueId": "group_que_id",
|
|
29240
|
+
"group_by": "group_que_id",
|
|
29241
|
+
"groupBy": "group_que_id",
|
|
28609
29242
|
"filters": "filters",
|
|
28610
29243
|
"filter_rules": "filters",
|
|
28611
29244
|
"filterRules": "filters",
|
|
28612
|
-
"
|
|
28613
|
-
"
|
|
28614
|
-
"
|
|
28615
|
-
"
|
|
28616
|
-
"
|
|
28617
|
-
"
|
|
29245
|
+
"start_que_id": "start_que_id",
|
|
29246
|
+
"startQueId": "start_que_id",
|
|
29247
|
+
"start_field": "start_que_id",
|
|
29248
|
+
"startField": "start_que_id",
|
|
29249
|
+
"end_que_id": "end_que_id",
|
|
29250
|
+
"endQueId": "end_que_id",
|
|
29251
|
+
"end_field": "end_que_id",
|
|
29252
|
+
"endField": "end_que_id",
|
|
29253
|
+
"title_que_id": "title_que_id",
|
|
29254
|
+
"titleQueId": "title_que_id",
|
|
29255
|
+
"title_field": "title_que_id",
|
|
29256
|
+
"titleField": "title_que_id",
|
|
28618
29257
|
"buttons": "buttons",
|
|
28619
29258
|
"visibility": "visibility",
|
|
28620
29259
|
"query_conditions": "query_conditions",
|
|
@@ -28701,13 +29340,14 @@ def _normalize_view_partial_unset(raw_unset: Any, *, reason_path: str) -> tuple[
|
|
|
28701
29340
|
|
|
28702
29341
|
_VIEW_PARTIAL_SET_KEYS = {
|
|
28703
29342
|
"name",
|
|
29343
|
+
"pinned",
|
|
28704
29344
|
"type",
|
|
28705
29345
|
"columns",
|
|
28706
|
-
"
|
|
29346
|
+
"group_que_id",
|
|
28707
29347
|
"filters",
|
|
28708
|
-
"
|
|
28709
|
-
"
|
|
28710
|
-
"
|
|
29348
|
+
"start_que_id",
|
|
29349
|
+
"end_que_id",
|
|
29350
|
+
"title_que_id",
|
|
28711
29351
|
"buttons",
|
|
28712
29352
|
"visibility",
|
|
28713
29353
|
"query_conditions",
|
|
@@ -28733,6 +29373,108 @@ def _field_name_by_id(field_names_by_id: dict[int, str], field_id: Any) -> str |
|
|
|
28733
29373
|
return name or None
|
|
28734
29374
|
|
|
28735
29375
|
|
|
29376
|
+
def _collect_fields_by_que_id(fields: list[dict[str, Any]], *, schema: dict[str, Any] | None = None) -> dict[int, dict[str, Any]]:
|
|
29377
|
+
"""构建 queId→field 索引。
|
|
29378
|
+
|
|
29379
|
+
传 schema 时直接从原始 formQues/baseQues 递归收集(同名冲突不丢字段——名字索引
|
|
29380
|
+
只保留第一个,但同名不同 queId 的字段对 queId 引用各自有效);
|
|
29381
|
+
不传 schema 时从解析后的 fields 列表递归 subfields 构建。
|
|
29382
|
+
"""
|
|
29383
|
+
index: dict[int, dict[str, Any]] = {}
|
|
29384
|
+
|
|
29385
|
+
type_by_question_type = {v: k for k, v in QUESTION_TYPE_TO_FIELD_TYPE.items()}
|
|
29386
|
+
|
|
29387
|
+
def collect_raw(question: dict[str, Any]) -> None:
|
|
29388
|
+
que_id = _coerce_positive_int(question.get("queId"))
|
|
29389
|
+
if que_id is not None and que_id not in index:
|
|
29390
|
+
que_type = _coerce_positive_int(question.get("queType"))
|
|
29391
|
+
# 用 _parse_field 解析(含 options/option_details,与 _parse_schema 对齐);
|
|
29392
|
+
# 直收原始 dict 的 queTitle/queType 不够——选项字段需 options 才能解析值→id
|
|
29393
|
+
parsed = _parse_field(question)
|
|
29394
|
+
index[que_id] = parsed
|
|
29395
|
+
for key in ("subQuestions", "innerQuestions"):
|
|
29396
|
+
for child in question.get(key) or []:
|
|
29397
|
+
if isinstance(child, list):
|
|
29398
|
+
for inner in child:
|
|
29399
|
+
if isinstance(inner, dict):
|
|
29400
|
+
collect_raw(inner)
|
|
29401
|
+
elif isinstance(child, dict):
|
|
29402
|
+
collect_raw(child)
|
|
29403
|
+
|
|
29404
|
+
if schema is not None:
|
|
29405
|
+
for base_question in schema.get("baseQues", []) or []:
|
|
29406
|
+
if isinstance(base_question, dict):
|
|
29407
|
+
collect_raw(base_question)
|
|
29408
|
+
for line in schema.get("formQues", []) or []:
|
|
29409
|
+
for question in (line if isinstance(line, list) else [line]):
|
|
29410
|
+
if isinstance(question, dict):
|
|
29411
|
+
collect_raw(question)
|
|
29412
|
+
return index
|
|
29413
|
+
|
|
29414
|
+
def collect(field: dict[str, Any]) -> None:
|
|
29415
|
+
que_id = _coerce_positive_int(field.get("que_id"))
|
|
29416
|
+
if que_id is not None and que_id not in index:
|
|
29417
|
+
index[que_id] = field
|
|
29418
|
+
for sub in field.get("subfields") or []:
|
|
29419
|
+
if isinstance(sub, dict):
|
|
29420
|
+
collect(sub)
|
|
29421
|
+
|
|
29422
|
+
for field in fields:
|
|
29423
|
+
if isinstance(field, dict):
|
|
29424
|
+
collect(field)
|
|
29425
|
+
return index
|
|
29426
|
+
|
|
29427
|
+
|
|
29428
|
+
def _collect_field_id_to_name(schema: dict[str, Any]) -> dict[int, str]:
|
|
29429
|
+
"""从 form schema 收集 queId→queTitle(含子字段/段落内字段;同名时各 id 独立映射)。"""
|
|
29430
|
+
id_to_name: dict[int, str] = {}
|
|
29431
|
+
|
|
29432
|
+
def collect(question: dict[str, Any]) -> None:
|
|
29433
|
+
que_id = _coerce_positive_int(question.get("queId"))
|
|
29434
|
+
if que_id is not None and que_id not in id_to_name:
|
|
29435
|
+
title = str(question.get("queTitle") or "").strip()
|
|
29436
|
+
if title:
|
|
29437
|
+
id_to_name[que_id] = title
|
|
29438
|
+
for key in ("subQuestions", "innerQuestions"):
|
|
29439
|
+
for child in question.get(key) or []:
|
|
29440
|
+
if isinstance(child, list):
|
|
29441
|
+
for inner in child:
|
|
29442
|
+
if isinstance(inner, dict):
|
|
29443
|
+
collect(inner)
|
|
29444
|
+
elif isinstance(child, dict):
|
|
29445
|
+
collect(child)
|
|
29446
|
+
|
|
29447
|
+
for base_question in schema.get("baseQues", []) or []:
|
|
29448
|
+
if isinstance(base_question, dict):
|
|
29449
|
+
collect(base_question)
|
|
29450
|
+
for line in schema.get("formQues", []) or []:
|
|
29451
|
+
for question in (line if isinstance(line, list) else [line]):
|
|
29452
|
+
if isinstance(question, dict):
|
|
29453
|
+
collect(question)
|
|
29454
|
+
return id_to_name
|
|
29455
|
+
|
|
29456
|
+
|
|
29457
|
+
def _resolve_field_ref_preserving(
|
|
29458
|
+
config: dict[str, Any],
|
|
29459
|
+
config_key: str,
|
|
29460
|
+
name: str | None,
|
|
29461
|
+
field_map: dict[str, int],
|
|
29462
|
+
id_to_name: dict[int, str],
|
|
29463
|
+
) -> int | None:
|
|
29464
|
+
"""同名字段歧义消解(字段引用类:card title/cover、board group 等)。
|
|
29465
|
+
|
|
29466
|
+
契约按字段名引用,同 form 内同名字段时按名解析会取到错误的一个。
|
|
29467
|
+
UPDATE 场景读面原 queId 可用:若原 queId 的字段名与声明名一致,保留原 queId(保真);
|
|
29468
|
+
只有声明名与原值名字不同(用户真改了字段)才按名重新解析。
|
|
29469
|
+
"""
|
|
29470
|
+
if not name:
|
|
29471
|
+
return None
|
|
29472
|
+
original = _coerce_positive_int(config.get(config_key))
|
|
29473
|
+
if original is not None and id_to_name.get(original) == name:
|
|
29474
|
+
return original
|
|
29475
|
+
return field_map.get(name)
|
|
29476
|
+
|
|
29477
|
+
|
|
28736
29478
|
def _view_upsert_payload_from_existing_view(
|
|
28737
29479
|
*,
|
|
28738
29480
|
config: dict[str, Any],
|
|
@@ -28742,25 +29484,28 @@ def _view_upsert_payload_from_existing_view(
|
|
|
28742
29484
|
) -> dict[str, Any]:
|
|
28743
29485
|
view_type = _normalize_view_type_name(config.get("viewgraphType") or summary.get("type"))
|
|
28744
29486
|
configured_columns = [
|
|
28745
|
-
|
|
28746
|
-
for
|
|
28747
|
-
if
|
|
29487
|
+
que_id
|
|
29488
|
+
for item in (config.get("viewgraphQueIds") or [])
|
|
29489
|
+
if (que_id := _coerce_positive_int(item)) is not None
|
|
29490
|
+
and (field_names_by_id.get(que_id) or "") not in _KNOWN_SYSTEM_VIEW_COLUMNS
|
|
28748
29491
|
]
|
|
28749
29492
|
columns = [
|
|
28750
|
-
|
|
28751
|
-
for
|
|
28752
|
-
if
|
|
29493
|
+
que_id
|
|
29494
|
+
for item in (summary.get("apply_column_ids") or summary.get("columns") or [])
|
|
29495
|
+
if (que_id := _coerce_positive_int(item)) is not None
|
|
29496
|
+
and (field_names_by_id.get(que_id) or "") not in _KNOWN_SYSTEM_VIEW_COLUMNS
|
|
28753
29497
|
]
|
|
28754
29498
|
if configured_columns and (not columns or len(configured_columns) >= len(columns)):
|
|
28755
29499
|
columns = configured_columns
|
|
28756
29500
|
display_config = summary.get("display_config") if isinstance(summary.get("display_config"), dict) else {}
|
|
28757
|
-
|
|
28758
|
-
|
|
29501
|
+
group_que_id = _coerce_positive_int(config.get("groupQueId") or summary.get("group_que_id") or display_config.get("group_que_id"))
|
|
29502
|
+
title_que_id = _coerce_positive_int(config.get("titleQue") or display_config.get("title_que_id"))
|
|
28759
29503
|
gantt_config = config.get("viewgraphGanttConfigVO") if isinstance(config.get("viewgraphGanttConfigVO"), dict) else {}
|
|
28760
|
-
|
|
28761
|
-
|
|
28762
|
-
|
|
29504
|
+
start_que_id = _coerce_positive_int(gantt_config.get("startTimeQueId"))
|
|
29505
|
+
end_que_id = _coerce_positive_int(gantt_config.get("endTimeQueId"))
|
|
29506
|
+
gantt_title_que_id = _coerce_positive_int(gantt_config.get("titleQueId"))
|
|
28763
29507
|
query_conditions = _extract_view_query_conditions_config(config)
|
|
29508
|
+
query_conditions["rows"] = query_conditions.get("row_field_ids") or []
|
|
28764
29509
|
query_conditions.pop("row_field_ids", None)
|
|
28765
29510
|
associated_resources = _extract_view_associated_resources_config(config)
|
|
28766
29511
|
payload: dict[str, Any] = {
|
|
@@ -28768,7 +29513,6 @@ def _view_upsert_payload_from_existing_view(
|
|
|
28768
29513
|
"view_key": view_key,
|
|
28769
29514
|
"type": view_type or "table",
|
|
28770
29515
|
"columns": columns,
|
|
28771
|
-
"group_by": group_by,
|
|
28772
29516
|
"filters": [],
|
|
28773
29517
|
"query_conditions": query_conditions,
|
|
28774
29518
|
"associated_resources": {
|
|
@@ -28777,15 +29521,17 @@ def _view_upsert_payload_from_existing_view(
|
|
|
28777
29521
|
"associated_item_ids": list(associated_resources.get("configured_associated_item_ids") or []),
|
|
28778
29522
|
},
|
|
28779
29523
|
}
|
|
28780
|
-
if
|
|
28781
|
-
payload["
|
|
29524
|
+
if group_que_id is not None:
|
|
29525
|
+
payload["group_que_id"] = group_que_id
|
|
29526
|
+
if title_que_id is not None:
|
|
29527
|
+
payload["title_que_id"] = title_que_id
|
|
28782
29528
|
if view_type == "gantt":
|
|
28783
|
-
if
|
|
28784
|
-
payload["
|
|
28785
|
-
if
|
|
28786
|
-
payload["
|
|
28787
|
-
if
|
|
28788
|
-
payload["
|
|
29529
|
+
if start_que_id is not None:
|
|
29530
|
+
payload["start_que_id"] = start_que_id
|
|
29531
|
+
if end_que_id is not None:
|
|
29532
|
+
payload["end_que_id"] = end_que_id
|
|
29533
|
+
if gantt_title_que_id is not None:
|
|
29534
|
+
payload["title_que_id"] = gantt_title_que_id
|
|
28789
29535
|
return payload
|
|
28790
29536
|
|
|
28791
29537
|
|
|
@@ -28796,7 +29542,7 @@ def _first_present(mapping: dict[str, Any], *keys: str) -> Any:
|
|
|
28796
29542
|
return None
|
|
28797
29543
|
|
|
28798
29544
|
|
|
28799
|
-
def
|
|
29545
|
+
def _build_view_creation_payload(
|
|
28800
29546
|
*,
|
|
28801
29547
|
app_key: str,
|
|
28802
29548
|
base_info: dict[str, Any],
|
|
@@ -28804,23 +29550,24 @@ def _build_view_create_payload(
|
|
|
28804
29550
|
patch: ViewUpsertPatch,
|
|
28805
29551
|
ordinal: int,
|
|
28806
29552
|
view_filters: list[list[dict[str, Any]]] | None,
|
|
28807
|
-
|
|
29553
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
28808
29554
|
auth_override: dict[str, Any] | None = None,
|
|
28809
29555
|
explicit_button_dtos: list[dict[str, Any]] | None = None,
|
|
28810
29556
|
query_condition_payload: dict[str, Any] | None = None,
|
|
28811
29557
|
associated_resources_payload: dict[str, Any] | None = None,
|
|
29558
|
+
hierarchy_filter_groups: list[list[dict[str, Any]]] | None = None,
|
|
28812
29559
|
) -> JSONObject:
|
|
28813
29560
|
entity = _entity_spec_from_app(base_info=base_info, schema=schema, views=None)
|
|
28814
29561
|
parsed_schema = _parse_schema(schema)
|
|
28815
|
-
|
|
28816
|
-
field_ids = [
|
|
29562
|
+
visible_que_ids = _resolve_view_visible_que_ids(patch)
|
|
29563
|
+
field_ids = [f"field_{que}" for que in visible_que_ids]
|
|
28817
29564
|
gantt_config = _build_public_gantt_payload(parsed_schema["fields"], extract_field_map(schema), patch)
|
|
28818
29565
|
view_spec = ViewSpec(
|
|
28819
29566
|
view_id=_slugify(patch.name, default=f"view_{uuid4().hex[:6]}"),
|
|
28820
29567
|
name=patch.name,
|
|
28821
29568
|
type=patch.type.value,
|
|
28822
29569
|
field_ids=field_ids,
|
|
28823
|
-
group_by_field_id=
|
|
29570
|
+
group_by_field_id=f"field_{patch.group_que_id}" if patch.group_que_id else None,
|
|
28824
29571
|
config=gantt_config,
|
|
28825
29572
|
)
|
|
28826
29573
|
from ..solution.spec_models import EntitySpec
|
|
@@ -28829,7 +29576,6 @@ def _build_view_create_payload(
|
|
|
28829
29576
|
compiled = compile_views(EntitySpec.model_validate({**entity, "views": [view_spec.model_dump(mode="json")]}))[0]
|
|
28830
29577
|
field_map = extract_field_map(schema)
|
|
28831
29578
|
payload = deepcopy(compiled["create_payload"])
|
|
28832
|
-
visible_que_ids = [field_map[field_name] for field_name in visible_field_names if field_name in field_map]
|
|
28833
29579
|
payload["appKey"] = app_key
|
|
28834
29580
|
payload["ordinal"] = ordinal
|
|
28835
29581
|
payload["viewgraphQueIds"] = visible_que_ids
|
|
@@ -28841,17 +29587,21 @@ def _build_view_create_payload(
|
|
|
28841
29587
|
payload["beingShowTitleQue"] = True
|
|
28842
29588
|
payload["titleQue"] = gantt_config.get("titleQueId") or (visible_que_ids[0] if visible_que_ids else None)
|
|
28843
29589
|
if patch.type.value == "board":
|
|
28844
|
-
payload["groupQueId"] =
|
|
29590
|
+
payload["groupQueId"] = patch.group_que_id
|
|
28845
29591
|
return _hydrate_view_backend_payload(
|
|
28846
29592
|
payload=payload,
|
|
28847
29593
|
view_type=patch.type.value,
|
|
28848
29594
|
visible_que_id_values=visible_que_ids,
|
|
28849
|
-
group_que_id=
|
|
29595
|
+
group_que_id=patch.group_que_id,
|
|
28850
29596
|
view_filters=view_filters,
|
|
28851
29597
|
gantt_payload=gantt_config,
|
|
28852
29598
|
button_config_dtos=explicit_button_dtos,
|
|
28853
29599
|
query_condition_payload=query_condition_payload,
|
|
28854
29600
|
associated_resources_payload=associated_resources_payload,
|
|
29601
|
+
patch=patch,
|
|
29602
|
+
field_map=field_map,
|
|
29603
|
+
fields_by_que_id=fields_by_que_id,
|
|
29604
|
+
hierarchy_filter_groups=hierarchy_filter_groups,
|
|
28855
29605
|
)
|
|
28856
29606
|
|
|
28857
29607
|
|
|
@@ -28989,6 +29739,8 @@ def _normalize_view_type_name(value: Any) -> str:
|
|
|
28989
29739
|
return "gantt"
|
|
28990
29740
|
if "board" in normalized:
|
|
28991
29741
|
return "board"
|
|
29742
|
+
if "hierarchy" in normalized:
|
|
29743
|
+
return "hierarchy"
|
|
28992
29744
|
if "card" in normalized:
|
|
28993
29745
|
return "card"
|
|
28994
29746
|
return "table"
|
|
@@ -29002,20 +29754,20 @@ def _build_view_update_payload(
|
|
|
29002
29754
|
schema: dict[str, Any],
|
|
29003
29755
|
patch: ViewUpsertPatch,
|
|
29004
29756
|
view_filters: list[list[dict[str, Any]]] | None,
|
|
29005
|
-
|
|
29757
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
29006
29758
|
auth_override: dict[str, Any] | None = None,
|
|
29007
29759
|
explicit_button_dtos: list[dict[str, Any]] | None = None,
|
|
29008
29760
|
query_condition_payload: dict[str, Any] | None = None,
|
|
29009
29761
|
associated_resources_payload: dict[str, Any] | None = None,
|
|
29762
|
+
hierarchy_filter_groups: list[list[dict[str, Any]]] | None = None,
|
|
29010
29763
|
) -> JSONObject:
|
|
29011
29764
|
config_response = views.view_get_config(profile=profile, viewgraph_key=source_viewgraph_key)
|
|
29012
29765
|
config = config_response.get("result") if isinstance(config_response.get("result"), dict) else {}
|
|
29013
29766
|
payload = deepcopy(config)
|
|
29014
29767
|
parsed_schema = _parse_schema(schema)
|
|
29015
29768
|
field_map = extract_field_map(schema)
|
|
29016
|
-
|
|
29017
|
-
|
|
29018
|
-
visible_que_id_values = [field_map[name] for name in visible_field_names if name in field_map]
|
|
29769
|
+
visible_que_ids = _resolve_view_visible_que_ids(patch)
|
|
29770
|
+
visible_que_id_values = list(visible_que_ids)
|
|
29019
29771
|
gantt_payload = _build_public_gantt_payload(parsed_schema["fields"], field_map, patch)
|
|
29020
29772
|
|
|
29021
29773
|
for key in (
|
|
@@ -29037,8 +29789,25 @@ def _build_view_update_payload(
|
|
|
29037
29789
|
payload["auth"] = deepcopy(auth_override if isinstance(auth_override, dict) else payload.get("auth") or default_member_auth())
|
|
29038
29790
|
if "viewName" in payload:
|
|
29039
29791
|
payload["viewName"] = patch.name
|
|
29040
|
-
|
|
29041
|
-
|
|
29792
|
+
# 保留系统列在业务列之间的位置;业务列顺序和增删仍由声明决定。
|
|
29793
|
+
existing_que_ids = dict.fromkeys(
|
|
29794
|
+
que_id
|
|
29795
|
+
for que_id in (_coerce_any_int(raw) for raw in (config.get("viewgraphQueIds") or []))
|
|
29796
|
+
if que_id is not None
|
|
29797
|
+
)
|
|
29798
|
+
business_que_ids = iter(que_id for que_id in visible_que_id_values if que_id > 5)
|
|
29799
|
+
merged_que_ids: list[int] = []
|
|
29800
|
+
for que_id in existing_que_ids:
|
|
29801
|
+
if que_id <= 5:
|
|
29802
|
+
merged_que_ids.append(que_id)
|
|
29803
|
+
elif que_id in visible_que_id_values:
|
|
29804
|
+
merged_que_ids.append(next(business_que_ids))
|
|
29805
|
+
merged_que_ids.extend(business_que_ids)
|
|
29806
|
+
merged_que_ids.extend(que_id for que_id in visible_que_id_values if que_id <= 5 and que_id not in merged_que_ids)
|
|
29807
|
+
payload["viewgraphQueIds"] = merged_que_ids
|
|
29808
|
+
payload["viewgraphQuestions"] = _build_viewgraph_questions(
|
|
29809
|
+
schema, merged_que_ids, existing_by_que_id=_flatten_viewgraph_questions(config.get("viewgraphQuestions"))
|
|
29810
|
+
)
|
|
29042
29811
|
payload.setdefault("sortType", "defaultSort")
|
|
29043
29812
|
payload.setdefault("viewgraphSorts", [{"queId": 0, "beingSortAscend": True, "queType": 8}])
|
|
29044
29813
|
payload.setdefault("beingPinNavigate", True)
|
|
@@ -29068,25 +29837,75 @@ def _build_view_update_payload(
|
|
|
29068
29837
|
payload["viewgraphType"] = "boardView"
|
|
29069
29838
|
payload["beingShowTitleQue"] = True
|
|
29070
29839
|
payload["titleQue"] = visible_que_id_values[0] if visible_que_id_values else None
|
|
29071
|
-
payload["groupQueId"] =
|
|
29840
|
+
payload["groupQueId"] = patch.group_que_id
|
|
29072
29841
|
elif normalized_type == "gantt":
|
|
29073
29842
|
payload["viewgraphType"] = "ganttView"
|
|
29074
29843
|
payload["beingShowTitleQue"] = True
|
|
29075
29844
|
payload["titleQue"] = gantt_payload.get("titleQueId") or (visible_que_id_values[0] if visible_que_id_values else None)
|
|
29076
29845
|
payload.pop("groupQueId", None)
|
|
29846
|
+
elif normalized_type == "hierarchy":
|
|
29847
|
+
payload["viewgraphType"] = "hierarchyView"
|
|
29848
|
+
payload["beingShowTitleQue"] = True
|
|
29849
|
+
payload.pop("groupQueId", None)
|
|
29077
29850
|
return _hydrate_view_backend_payload(
|
|
29078
29851
|
payload=payload,
|
|
29079
29852
|
view_type=normalized_type,
|
|
29080
29853
|
visible_que_id_values=visible_que_id_values,
|
|
29081
|
-
group_que_id=
|
|
29854
|
+
group_que_id=patch.group_que_id,
|
|
29082
29855
|
view_filters=view_filters,
|
|
29083
29856
|
gantt_payload=gantt_payload,
|
|
29084
29857
|
button_config_dtos=button_config_dtos,
|
|
29085
29858
|
query_condition_payload=query_condition_payload,
|
|
29086
29859
|
associated_resources_payload=associated_resources_payload,
|
|
29860
|
+
patch=patch,
|
|
29861
|
+
field_map=field_map,
|
|
29862
|
+
fields_by_que_id=fields_by_que_id,
|
|
29863
|
+
hierarchy_filter_groups=hierarchy_filter_groups,
|
|
29864
|
+
schema=schema,
|
|
29087
29865
|
)
|
|
29088
29866
|
|
|
29089
29867
|
|
|
29868
|
+
# 系统列 queId→公开名(后端语义;读面展示与 viewgraphQueIds 透传时的名称解析)
|
|
29869
|
+
_SYSTEM_VIEW_QUE_NAMES = {
|
|
29870
|
+
0: "编号",
|
|
29871
|
+
1: "提交人",
|
|
29872
|
+
2: "提交时间",
|
|
29873
|
+
3: "更新时间",
|
|
29874
|
+
4: "流程标题",
|
|
29875
|
+
5: "当前流程状态",
|
|
29876
|
+
-17: "数据ID",
|
|
29877
|
+
-19: "删除时间",
|
|
29878
|
+
-20: "删除人",
|
|
29879
|
+
-21: "处理节点",
|
|
29880
|
+
-22: "抄送节点",
|
|
29881
|
+
-23: "处理时间",
|
|
29882
|
+
-24: "抄送时间",
|
|
29883
|
+
-25: "接收时间",
|
|
29884
|
+
}
|
|
29885
|
+
|
|
29886
|
+
_SYSTEM_VIEW_QUE_IDS = frozenset(
|
|
29887
|
+
{
|
|
29888
|
+
*_SYSTEM_VIEW_QUE_NAMES,
|
|
29889
|
+
-1,
|
|
29890
|
+
-2,
|
|
29891
|
+
-3,
|
|
29892
|
+
-4,
|
|
29893
|
+
-5,
|
|
29894
|
+
-6,
|
|
29895
|
+
-7,
|
|
29896
|
+
-8,
|
|
29897
|
+
-18,
|
|
29898
|
+
-30,
|
|
29899
|
+
-31,
|
|
29900
|
+
}
|
|
29901
|
+
)
|
|
29902
|
+
|
|
29903
|
+
|
|
29904
|
+
def _is_known_system_view_que_id(value: Any) -> bool:
|
|
29905
|
+
que_id = _coerce_any_int(value)
|
|
29906
|
+
return que_id in _SYSTEM_VIEW_QUE_IDS if que_id is not None else False
|
|
29907
|
+
|
|
29908
|
+
|
|
29090
29909
|
_KNOWN_SYSTEM_VIEW_COLUMNS = {
|
|
29091
29910
|
"编号",
|
|
29092
29911
|
"当前流程状态",
|
|
@@ -29110,16 +29929,16 @@ def _build_minimal_view_payload(
|
|
|
29110
29929
|
patch: ViewUpsertPatch,
|
|
29111
29930
|
ordinal: int,
|
|
29112
29931
|
view_filters: list[list[dict[str, Any]]] | None,
|
|
29113
|
-
|
|
29932
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
29114
29933
|
auth_override: dict[str, Any] | None = None,
|
|
29115
29934
|
explicit_button_dtos: list[dict[str, Any]] | None = None,
|
|
29116
29935
|
query_condition_payload: dict[str, Any] | None = None,
|
|
29117
29936
|
associated_resources_payload: dict[str, Any] | None = None,
|
|
29937
|
+
hierarchy_filter_groups: list[list[dict[str, Any]]] | None = None,
|
|
29118
29938
|
) -> JSONObject:
|
|
29119
29939
|
field_map = extract_field_map(schema)
|
|
29120
29940
|
parsed_schema = _parse_schema(schema)
|
|
29121
|
-
|
|
29122
|
-
visible_que_id_values = [field_map[name] for name in visible_field_names if name in field_map]
|
|
29941
|
+
visible_que_id_values = _resolve_view_visible_que_ids(patch)
|
|
29123
29942
|
gantt_payload = _build_public_gantt_payload(parsed_schema["fields"], field_map, patch)
|
|
29124
29943
|
payload: JSONObject = {
|
|
29125
29944
|
"appKey": app_key,
|
|
@@ -29129,6 +29948,7 @@ def _build_minimal_view_payload(
|
|
|
29129
29948
|
"card": "cardView",
|
|
29130
29949
|
"board": "boardView",
|
|
29131
29950
|
"gantt": "ganttView",
|
|
29951
|
+
"hierarchy": "hierarchyView",
|
|
29132
29952
|
}[patch.type.value],
|
|
29133
29953
|
"ordinal": ordinal,
|
|
29134
29954
|
"viewgraphQueIds": visible_que_id_values,
|
|
@@ -29139,17 +29959,21 @@ def _build_minimal_view_payload(
|
|
|
29139
29959
|
payload["beingShowTitleQue"] = True
|
|
29140
29960
|
payload["titleQue"] = gantt_payload.get("titleQueId") or (visible_que_id_values[0] if visible_que_id_values else None)
|
|
29141
29961
|
if patch.type.value == "board":
|
|
29142
|
-
payload["groupQueId"] =
|
|
29962
|
+
payload["groupQueId"] = patch.group_que_id
|
|
29143
29963
|
return _hydrate_view_backend_payload(
|
|
29144
29964
|
payload=payload,
|
|
29145
29965
|
view_type=patch.type.value,
|
|
29146
29966
|
visible_que_id_values=visible_que_id_values,
|
|
29147
|
-
group_que_id=
|
|
29967
|
+
group_que_id=patch.group_que_id,
|
|
29148
29968
|
view_filters=view_filters,
|
|
29149
29969
|
gantt_payload=gantt_payload,
|
|
29150
29970
|
button_config_dtos=explicit_button_dtos,
|
|
29151
29971
|
query_condition_payload=query_condition_payload,
|
|
29152
29972
|
associated_resources_payload=associated_resources_payload,
|
|
29973
|
+
patch=patch,
|
|
29974
|
+
field_map=field_map,
|
|
29975
|
+
fields_by_que_id=fields_by_que_id,
|
|
29976
|
+
hierarchy_filter_groups=hierarchy_filter_groups,
|
|
29153
29977
|
)
|
|
29154
29978
|
|
|
29155
29979
|
|
|
@@ -29164,6 +29988,11 @@ def _hydrate_view_backend_payload(
|
|
|
29164
29988
|
button_config_dtos: list[dict[str, Any]] | None = None,
|
|
29165
29989
|
query_condition_payload: dict[str, Any] | None = None,
|
|
29166
29990
|
associated_resources_payload: dict[str, Any] | None = None,
|
|
29991
|
+
patch: ViewUpsertPatch | None = None,
|
|
29992
|
+
field_map: dict[str, int] | None = None,
|
|
29993
|
+
fields_by_que_id: dict[str, dict[str, Any]] | None = None,
|
|
29994
|
+
hierarchy_filter_groups: list[list[dict[str, Any]]] | None = None,
|
|
29995
|
+
schema: dict[str, Any] | None = None,
|
|
29167
29996
|
) -> JSONObject:
|
|
29168
29997
|
data = deepcopy(payload)
|
|
29169
29998
|
data.setdefault("beingPinNavigate", True)
|
|
@@ -29233,26 +30062,193 @@ def _hydrate_view_backend_payload(
|
|
|
29233
30062
|
data["titleQue"] = (gantt_payload or {}).get("titleQueId") or (visible_que_id_values[0] if visible_que_id_values else None)
|
|
29234
30063
|
data["groupQueId"] = None
|
|
29235
30064
|
data["viewgraphGanttConfigVO"] = deepcopy(gantt_payload) if gantt_payload else None
|
|
30065
|
+
elif view_type == "hierarchy":
|
|
30066
|
+
data["viewgraphType"] = "hierarchyView"
|
|
30067
|
+
data["beingShowTitleQue"] = True
|
|
30068
|
+
data["groupQueId"] = None
|
|
30069
|
+
data["viewgraphGanttConfigVO"] = None
|
|
29236
30070
|
if view_filters is not None:
|
|
29237
30071
|
data["viewgraphLimit"] = deepcopy(view_filters)
|
|
29238
30072
|
data["viewgraphLimitType"] = 1
|
|
29239
30073
|
data["viewgraphLimitFormula"] = ""
|
|
29240
30074
|
data["dataPermissionType"] = "CUSTOM"
|
|
29241
30075
|
data["dataScope"] = "CUSTOM" if view_filters else "ALL"
|
|
30076
|
+
if patch is not None:
|
|
30077
|
+
_apply_view_config_groups(
|
|
30078
|
+
data,
|
|
30079
|
+
patch=patch,
|
|
30080
|
+
view_type=view_type,
|
|
30081
|
+
visible_que_id_values=visible_que_id_values,
|
|
30082
|
+
field_map=field_map or {},
|
|
30083
|
+
fields_by_que_id=fields_by_que_id or {},
|
|
30084
|
+
hierarchy_filter_groups=hierarchy_filter_groups,
|
|
30085
|
+
config=payload if isinstance(payload, dict) else None,
|
|
30086
|
+
id_to_name=_collect_field_id_to_name(schema) if isinstance(schema, dict) else None,
|
|
30087
|
+
)
|
|
29242
30088
|
_apply_view_query_conditions_payload(data, query_condition_payload)
|
|
29243
30089
|
_apply_view_associated_resources_payload(data, associated_resources_payload)
|
|
29244
30090
|
return data
|
|
29245
30091
|
|
|
29246
30092
|
|
|
29247
|
-
|
|
29248
|
-
|
|
29249
|
-
|
|
29250
|
-
|
|
29251
|
-
|
|
29252
|
-
|
|
30093
|
+
_DATA_PERMISSION_SCOPE_MAP = {
|
|
30094
|
+
"all": "ALL",
|
|
30095
|
+
"oneself": "ONESELF",
|
|
30096
|
+
"dept": "DEPT",
|
|
30097
|
+
"subordinate": "SUBORDINATE",
|
|
30098
|
+
"custom": "CUSTOM",
|
|
30099
|
+
}
|
|
30100
|
+
|
|
30101
|
+
|
|
30102
|
+
def _apply_view_config_groups(
|
|
30103
|
+
data: JSONObject,
|
|
30104
|
+
*,
|
|
30105
|
+
patch: ViewUpsertPatch,
|
|
30106
|
+
view_type: str,
|
|
30107
|
+
visible_que_id_values: list[int],
|
|
30108
|
+
field_map: dict[str, int],
|
|
30109
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
30110
|
+
hierarchy_filter_groups: list[list[dict[str, Any]]] | None,
|
|
30111
|
+
config: dict[str, Any] | None = None,
|
|
30112
|
+
id_to_name: dict[int, str] | None = None,
|
|
30113
|
+
) -> None:
|
|
30114
|
+
"""编译 complete 档新字段组:data_permission/sort/card/hierarchy/display/record/share。"""
|
|
30115
|
+
if patch.data_permission is not None:
|
|
30116
|
+
data["dataPermissionType"] = "CONFIG" if patch.data_permission.type == "config" else "CUSTOM"
|
|
30117
|
+
data["dataScope"] = _DATA_PERMISSION_SCOPE_MAP.get(patch.data_permission.scope, "ALL")
|
|
30118
|
+
if patch.sort is not None:
|
|
30119
|
+
if patch.sort:
|
|
30120
|
+
sorts: list[dict[str, Any]] = []
|
|
30121
|
+
for rule in patch.sort:
|
|
30122
|
+
field = fields_by_que_id.get(rule.que_id) or {}
|
|
30123
|
+
que_type = _coerce_positive_int(field.get("que_type")) or FIELD_TYPE_TO_QUESTION_TYPE.get(
|
|
30124
|
+
str(field.get("type") or FieldType.text.value), 2
|
|
30125
|
+
)
|
|
30126
|
+
sorts.append(
|
|
30127
|
+
{
|
|
30128
|
+
"queId": rule.que_id,
|
|
30129
|
+
"beingSortAscend": bool(rule.ascending),
|
|
30130
|
+
"queType": que_type,
|
|
30131
|
+
}
|
|
30132
|
+
)
|
|
30133
|
+
data["sortType"] = "customSort"
|
|
30134
|
+
data["viewgraphSorts"] = sorts
|
|
30135
|
+
else:
|
|
30136
|
+
data["sortType"] = "defaultSort"
|
|
30137
|
+
data["viewgraphSorts"] = [{"queId": 0, "beingSortAscend": True, "queType": 8}]
|
|
30138
|
+
card = patch.card
|
|
30139
|
+
config = config or {}
|
|
30140
|
+
id_to_name = id_to_name or {}
|
|
30141
|
+
if card is not None:
|
|
30142
|
+
if card.title_que_id is not None:
|
|
30143
|
+
data["titleQue"] = card.title_que_id
|
|
30144
|
+
if card.cover_que_id is not None:
|
|
30145
|
+
data["frontCoverQueId"] = card.cover_que_id
|
|
30146
|
+
if card.clipping_mode is not None:
|
|
30147
|
+
data["clippingMode"] = card.clipping_mode
|
|
30148
|
+
if card.image_adaption is not None:
|
|
30149
|
+
data["beingImageAdaption"] = card.image_adaption
|
|
30150
|
+
if card.show_field_title is not None:
|
|
30151
|
+
data["beingShowQueTitle"] = card.show_field_title
|
|
30152
|
+
if card.show_title is not None:
|
|
30153
|
+
data["beingShowTitleQue"] = card.show_title
|
|
30154
|
+
if card.show_cover is not None:
|
|
30155
|
+
data["beingShowCover"] = card.show_cover
|
|
30156
|
+
if card.group_que_id is not None:
|
|
30157
|
+
data["groupQueId"] = card.group_que_id
|
|
30158
|
+
if card.group_color is not None:
|
|
30159
|
+
data["beingGroupColor"] = card.group_color
|
|
30160
|
+
if patch.hierarchy is not None and view_type == "hierarchy":
|
|
30161
|
+
data["viewgraphHierarchyConfigVO"] = {
|
|
30162
|
+
"titleQueId": patch.hierarchy.title_que_id,
|
|
30163
|
+
"childrenQueId": patch.hierarchy.children_que_id,
|
|
30164
|
+
"rootNodeFilter": deepcopy(hierarchy_filter_groups) if hierarchy_filter_groups is not None else [],
|
|
30165
|
+
}
|
|
30166
|
+
data["titleQue"] = patch.hierarchy.title_que_id
|
|
30167
|
+
display = patch.display
|
|
30168
|
+
if display is not None:
|
|
30169
|
+
if display.row_height is not None:
|
|
30170
|
+
data["defaultRowHigh"] = display.row_height
|
|
30171
|
+
if display.pin_navigate is not None:
|
|
30172
|
+
data["beingPinNavigate"] = display.pin_navigate
|
|
30173
|
+
if display.remark is not None:
|
|
30174
|
+
data["remark"] = display.remark
|
|
30175
|
+
record = patch.record
|
|
30176
|
+
if record is not None:
|
|
30177
|
+
if record.audit_visible is not None:
|
|
30178
|
+
data["beingAuditRecordVisible"] = record.audit_visible
|
|
30179
|
+
if record.data_log_visible is not None:
|
|
30180
|
+
data["beingDataLogVisible"] = record.data_log_visible
|
|
30181
|
+
if record.qrobot_record_visible is not None:
|
|
30182
|
+
data["beingQrobotRecordVisible"] = record.qrobot_record_visible
|
|
30183
|
+
if record.workflow_future_visible is not None:
|
|
30184
|
+
data["beingWorkflowNodeFutureListVisible"] = record.workflow_future_visible
|
|
30185
|
+
if record.comment is not None:
|
|
30186
|
+
data["beingCommentStatus"] = record.comment
|
|
30187
|
+
share = patch.share
|
|
30188
|
+
if share is not None:
|
|
30189
|
+
if share.need_pass is not None:
|
|
30190
|
+
data["beingNeedPass"] = share.need_pass
|
|
30191
|
+
data["needPass"] = share.need_pass
|
|
30192
|
+
if share.password is None:
|
|
30193
|
+
# 声明未带密码(提取语义:不回传敏感值)——显式清空 payload 里读面带来的旧密码;
|
|
30194
|
+
# 后端 isNotBlank(空)跳过校验且不更新密码列,原密码保留(Java AbstractViewgraphStrategicService:424)
|
|
30195
|
+
data["viewgraphPass"] = ""
|
|
30196
|
+
data.pop("pass", None)
|
|
30197
|
+
if share.password is not None:
|
|
30198
|
+
data["viewgraphPass"] = share.password
|
|
30199
|
+
if share.force_login is not None:
|
|
30200
|
+
data["beingForceLogin"] = share.force_login
|
|
30201
|
+
if share.print_enabled is not None:
|
|
30202
|
+
data["beingPrintStatus"] = share.print_enabled
|
|
30203
|
+
|
|
30204
|
+
|
|
30205
|
+
def _iter_view_config_group_field_names(patch: ViewUpsertPatch) -> list[tuple[str, list[int]]]:
|
|
30206
|
+
"""枚举 complete 档新字段组引用的 queId(用于 UNKNOWN_VIEW_FIELD 校验)。"""
|
|
30207
|
+
groups: list[tuple[str, list[int]]] = []
|
|
30208
|
+
if patch.sort:
|
|
30209
|
+
groups.append(("sort", [rule.que_id for rule in patch.sort]))
|
|
30210
|
+
if patch.card is not None:
|
|
30211
|
+
groups.append(
|
|
30212
|
+
(
|
|
30213
|
+
"card",
|
|
30214
|
+
[
|
|
30215
|
+
que
|
|
30216
|
+
for que in (patch.card.title_que_id, patch.card.cover_que_id, patch.card.group_que_id)
|
|
30217
|
+
if que
|
|
30218
|
+
],
|
|
30219
|
+
)
|
|
30220
|
+
)
|
|
30221
|
+
if patch.gantt is not None:
|
|
30222
|
+
gantt_ids = [
|
|
30223
|
+
que
|
|
30224
|
+
for que in (patch.gantt.title_que_id, patch.gantt.start_que_id, patch.gantt.end_que_id)
|
|
30225
|
+
if que
|
|
30226
|
+
]
|
|
30227
|
+
gantt_ids.extend(group.que_id for group in patch.gantt.groups or [])
|
|
30228
|
+
for dep in patch.gantt.dependencies or []:
|
|
30229
|
+
gantt_ids.extend([dep.dependency_que_id, dep.predecessor_que_id])
|
|
30230
|
+
if gantt_ids:
|
|
30231
|
+
groups.append(("gantt", gantt_ids))
|
|
30232
|
+
if patch.hierarchy is not None:
|
|
30233
|
+
groups.append(("hierarchy", [patch.hierarchy.title_que_id, patch.hierarchy.children_que_id]))
|
|
30234
|
+
return groups
|
|
30235
|
+
|
|
30236
|
+
|
|
30237
|
+
|
|
30238
|
+
def _resolve_view_visible_que_ids(patch: ViewUpsertPatch) -> list[int]:
|
|
30239
|
+
"""收集视图引用的业务字段和已知系统字段 queId。"""
|
|
30240
|
+
ordered: list[int] = []
|
|
30241
|
+
extra_ids: list[int | None] = []
|
|
30242
|
+
if patch.hierarchy is not None:
|
|
30243
|
+
extra_ids.extend([patch.hierarchy.title_que_id, patch.hierarchy.children_que_id])
|
|
30244
|
+
for value in [*patch.columns, patch.title_que_id, patch.start_que_id, patch.end_que_id, patch.group_que_id, *extra_ids]:
|
|
30245
|
+
que_id = _coerce_any_int(value)
|
|
30246
|
+
if que_id is not None and (que_id > 0 or _is_known_system_view_que_id(que_id)) and que_id not in ordered:
|
|
30247
|
+
ordered.append(que_id)
|
|
29253
30248
|
return ordered
|
|
29254
30249
|
|
|
29255
30250
|
|
|
30251
|
+
|
|
29256
30252
|
def _build_public_gantt_payload(
|
|
29257
30253
|
fields: list[dict[str, Any]],
|
|
29258
30254
|
field_map: dict[str, int],
|
|
@@ -29260,22 +30256,35 @@ def _build_public_gantt_payload(
|
|
|
29260
30256
|
) -> dict[str, Any]:
|
|
29261
30257
|
if patch.type.value != "gantt":
|
|
29262
30258
|
return {}
|
|
29263
|
-
|
|
29264
|
-
|
|
29265
|
-
|
|
30259
|
+
gantt_config = patch.gantt
|
|
30260
|
+
title_que_id = (
|
|
30261
|
+
getattr(gantt_config, "title_que_id", None)
|
|
30262
|
+
or patch.title_que_id
|
|
30263
|
+
or (patch.columns[0] if patch.columns else None)
|
|
30264
|
+
)
|
|
30265
|
+
start_que_id = getattr(gantt_config, "start_que_id", None) or patch.start_que_id
|
|
30266
|
+
end_que_id = getattr(gantt_config, "end_que_id", None) or patch.end_que_id
|
|
30267
|
+
dependency = gantt_config.dependencies[0] if gantt_config is not None and gantt_config.dependencies else None
|
|
30268
|
+
auto_calibration = bool(getattr(gantt_config, "auto_calibration", False) or False)
|
|
29266
30269
|
return {
|
|
29267
|
-
"titleQueId":
|
|
29268
|
-
"startTimeQueId":
|
|
29269
|
-
"endTimeQueId":
|
|
29270
|
-
"defaultTimeDimension": "week",
|
|
29271
|
-
"ganttGroupVOList": [
|
|
30270
|
+
"titleQueId": title_que_id,
|
|
30271
|
+
"startTimeQueId": start_que_id,
|
|
30272
|
+
"endTimeQueId": end_que_id,
|
|
30273
|
+
"defaultTimeDimension": getattr(gantt_config, "default_time_dimension", None) or "week",
|
|
30274
|
+
"ganttGroupVOList": [
|
|
30275
|
+
{
|
|
30276
|
+
"groupQueId": group.que_id,
|
|
30277
|
+
"ordinal": group.ordinal if group.ordinal is not None else index,
|
|
30278
|
+
}
|
|
30279
|
+
for index, group in enumerate((gantt_config.groups if gantt_config is not None else []), start=1)
|
|
30280
|
+
],
|
|
29272
30281
|
"ganttDependencyVO": {
|
|
29273
|
-
"dependencyQueId": None,
|
|
29274
|
-
"predecessorTaskQueId": None,
|
|
29275
|
-
"
|
|
29276
|
-
"
|
|
29277
|
-
"endEndOptionId": None,
|
|
29278
|
-
"endStartOptionId": None,
|
|
30282
|
+
"dependencyQueId": dependency.dependency_que_id if dependency else None,
|
|
30283
|
+
"predecessorTaskQueId": dependency.predecessor_que_id if dependency else None,
|
|
30284
|
+
"startStartOptionId": dependency.start_start_option_id if dependency else None,
|
|
30285
|
+
"startEndOptionId": dependency.start_end_option_id if dependency else None,
|
|
30286
|
+
"endEndOptionId": dependency.end_end_option_id if dependency else None,
|
|
30287
|
+
"endStartOptionId": dependency.end_start_option_id if dependency else None,
|
|
29279
30288
|
},
|
|
29280
30289
|
"ganttAutoCalibrationVO": {
|
|
29281
30290
|
"autoCalibrationRuleVO": {
|
|
@@ -29286,15 +30295,15 @@ def _build_public_gantt_payload(
|
|
|
29286
30295
|
"endStartFinish": True,
|
|
29287
30296
|
"endEndFinish": False,
|
|
29288
30297
|
},
|
|
29289
|
-
"beingAutoCalibration":
|
|
29290
|
-
"userAutoCalibration":
|
|
30298
|
+
"beingAutoCalibration": auto_calibration,
|
|
30299
|
+
"userAutoCalibration": auto_calibration,
|
|
29291
30300
|
},
|
|
29292
30301
|
}
|
|
29293
30302
|
|
|
29294
30303
|
|
|
29295
30304
|
def _build_view_filter_groups(
|
|
29296
30305
|
*,
|
|
29297
|
-
|
|
30306
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
29298
30307
|
filters: list[Any],
|
|
29299
30308
|
) -> tuple[list[list[dict[str, Any]]], list[dict[str, Any]]]:
|
|
29300
30309
|
translated_rules: list[dict[str, Any]] = []
|
|
@@ -29304,14 +30313,14 @@ def _build_view_filter_groups(
|
|
|
29304
30313
|
raw_rule = raw_rule.model_dump(mode="json")
|
|
29305
30314
|
if not isinstance(raw_rule, dict):
|
|
29306
30315
|
continue
|
|
29307
|
-
|
|
29308
|
-
field =
|
|
30316
|
+
rule_que_id = _coerce_positive_int(raw_rule.get("que_id"))
|
|
30317
|
+
field = fields_by_que_id.get(rule_que_id) if rule_que_id is not None else None
|
|
29309
30318
|
if field is None:
|
|
29310
30319
|
issues.append(
|
|
29311
30320
|
{
|
|
29312
30321
|
"error_code": "UNKNOWN_VIEW_FIELD",
|
|
29313
|
-
"missing_fields": [
|
|
29314
|
-
"reason_path": "filters[].
|
|
30322
|
+
"missing_fields": [str(raw_rule.get("que_id") or "")],
|
|
30323
|
+
"reason_path": "filters[].que_id",
|
|
29315
30324
|
}
|
|
29316
30325
|
)
|
|
29317
30326
|
continue
|
|
@@ -29393,7 +30402,8 @@ def _resolve_view_filter_values(
|
|
|
29393
30402
|
unresolved_values.append(_stringify_condition_value(value))
|
|
29394
30403
|
continue
|
|
29395
30404
|
resolved_details.append({"id": resolved.get("id"), "value": str(resolved.get("value") or resolved.get("id"))})
|
|
29396
|
-
if unresolved_values:
|
|
30405
|
+
if unresolved_values and not resolved_details:
|
|
30406
|
+
# 全部值都解析不出(选项已删):返回错误让上层判定(该规则已死语义)
|
|
29397
30407
|
return (
|
|
29398
30408
|
[],
|
|
29399
30409
|
[],
|
|
@@ -29408,6 +30418,7 @@ def _resolve_view_filter_values(
|
|
|
29408
30418
|
},
|
|
29409
30419
|
},
|
|
29410
30420
|
)
|
|
30421
|
+
# 部分值解析不出:剔除已删选项值继续写(兼容存量脏数据,不再整视图失败)
|
|
29411
30422
|
return (
|
|
29412
30423
|
[str(detail["id"]) for detail in resolved_details if detail.get("id") is not None],
|
|
29413
30424
|
[_coerce_view_filter_value_detail(detail) for detail in resolved_details],
|
|
@@ -29451,6 +30462,10 @@ def _build_view_filter_value_details(values: list[Any]) -> list[dict[str, Any]]:
|
|
|
29451
30462
|
details.append(_coerce_view_filter_value_detail({"id": item_id, "value": item_value if item_value is not None else str(item_id)}))
|
|
29452
30463
|
elif isinstance(value, int):
|
|
29453
30464
|
details.append(_coerce_view_filter_value_detail({"id": value, "value": str(value)}))
|
|
30465
|
+
elif isinstance(value, str):
|
|
30466
|
+
# text/number 字段:AnswerValue.id 是 Integer(选项 id),字符串值无 id;
|
|
30467
|
+
# 后端 formula getId()==null → 走 getValue() 分支构建 ""value"" 形式
|
|
30468
|
+
details.append(_coerce_view_filter_value_detail({"id": None, "value": value}))
|
|
29454
30469
|
return details
|
|
29455
30470
|
|
|
29456
30471
|
|
|
@@ -29617,11 +30632,11 @@ def _view_filter_groups_semantic_readback(expected: Any, actual: Any) -> tuple[b
|
|
|
29617
30632
|
def _public_view_filter_groups_from_match_rules(
|
|
29618
30633
|
groups: Any,
|
|
29619
30634
|
*,
|
|
29620
|
-
|
|
30635
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
29621
30636
|
) -> list[list[dict[str, Any]]]:
|
|
29622
30637
|
fields_by_que_id = {
|
|
29623
30638
|
_coerce_positive_int(field.get("que_id")): field
|
|
29624
|
-
for field in
|
|
30639
|
+
for field in fields_by_que_id.values()
|
|
29625
30640
|
if isinstance(field, dict) and _coerce_positive_int(field.get("que_id")) is not None
|
|
29626
30641
|
}
|
|
29627
30642
|
public_groups: list[list[dict[str, Any]]] = []
|
|
@@ -29632,7 +30647,7 @@ def _public_view_filter_groups_from_match_rules(
|
|
|
29632
30647
|
field = fields_by_que_id.get(que_id) or {}
|
|
29633
30648
|
values = _public_view_filter_rule_values(rule, field=field)
|
|
29634
30649
|
public_rule: dict[str, Any] = {
|
|
29635
|
-
"
|
|
30650
|
+
"que_id": que_id,
|
|
29636
30651
|
"operator": _public_view_filter_operator_from_judge_type(rule.get("judgeType"), values=values),
|
|
29637
30652
|
}
|
|
29638
30653
|
if public_rule["operator"] == "in":
|
|
@@ -29657,6 +30672,8 @@ def _public_view_filter_operator_from_judge_type(judge_type: Any, *, values: lis
|
|
|
29657
30672
|
if normalized in {JUDGE_INCLUDE, JUDGE_FUZZY_MATCH}:
|
|
29658
30673
|
return "contains"
|
|
29659
30674
|
if normalized in {JUDGE_EQUAL_ANY, JUDGE_INCLUDE_ANY}:
|
|
30675
|
+
# ANY 判型+空值 = "该字段未选任何值"的合法存量形态(judgeType 保真回写,
|
|
30676
|
+
# 转 is_empty 会丢判型——后端 9 与 0 是不同判定域,实测回读不同)
|
|
29660
30677
|
return "in"
|
|
29661
30678
|
if normalized == JUDGE_GREATER_OR_EQUAL:
|
|
29662
30679
|
return "gte"
|