@qingflow-tech/qingflow-cli 1.1.50-beta.2 → 1.1.50-beta.4
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 +180 -191
- 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 +227 -17
- package/src/qingflow_mcp/builder_facade/schemas/views/manifest.json +42 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/complete.json +1245 -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 +73 -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 +319 -0
- package/src/qingflow_mcp/builder_facade/service.py +1460 -410
- package/src/qingflow_mcp/builder_facade/views_extract.py +592 -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/compiler/view_compiler.py +2 -1
- package/src/qingflow_mcp/solution/executor.py +96 -8
- package/src/qingflow_mcp/solution/spec_models.py +1 -0
- 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,94 @@ 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
|
+
calendar_system_date = (
|
|
12464
|
+
{patch.calendar.date_que_id}
|
|
12465
|
+
if group_name == "calendar" and patch.calendar is not None and patch.calendar.date_que_id in {2, 3}
|
|
12466
|
+
else set()
|
|
12467
|
+
)
|
|
12468
|
+
missing_config_fields = [que for que in config_que_ids if que not in fields_by_que_id and que not in calendar_system_date]
|
|
12469
|
+
if missing_config_fields:
|
|
12470
|
+
return _failed(
|
|
12471
|
+
"UNKNOWN_VIEW_FIELD",
|
|
12472
|
+
f"view {group_name} configuration references unknown fields",
|
|
12473
|
+
normalized_args=normalized_args,
|
|
12474
|
+
details={
|
|
12475
|
+
"app_key": app_key,
|
|
12476
|
+
"view_name": patch.name,
|
|
12477
|
+
"missing_fields": missing_config_fields,
|
|
12478
|
+
"reason_path": f"{group_name}",
|
|
12479
|
+
},
|
|
12480
|
+
missing_fields=missing_config_fields,
|
|
12481
|
+
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
12482
|
+
)
|
|
12483
|
+
hierarchy_root_filter_groups: list[list[dict[str, Any]]] | None = None
|
|
12484
|
+
if patch.hierarchy is not None and patch.hierarchy.root_filter:
|
|
12485
|
+
hierarchy_root_filter_groups = []
|
|
12486
|
+
for root_filter_group in patch.hierarchy.root_filter:
|
|
12487
|
+
translated_group, hierarchy_filter_issues = _build_view_filter_groups(
|
|
12488
|
+
fields_by_que_id=fields_by_que_id,
|
|
12489
|
+
filters=root_filter_group,
|
|
12490
|
+
)
|
|
12491
|
+
if hierarchy_filter_issues:
|
|
12492
|
+
first_issue = hierarchy_filter_issues[0]
|
|
12493
|
+
return _failed(
|
|
12494
|
+
str(first_issue.get("error_code") or "UNKNOWN_VIEW_FIELD"),
|
|
12495
|
+
"view hierarchy root_filter references invalid fields or values",
|
|
12496
|
+
normalized_args=normalized_args,
|
|
12497
|
+
details={
|
|
12498
|
+
"app_key": app_key,
|
|
12499
|
+
"view_name": patch.name,
|
|
12500
|
+
**first_issue,
|
|
12501
|
+
},
|
|
12502
|
+
missing_fields=list(first_issue.get("missing_fields") or []),
|
|
12503
|
+
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
12504
|
+
)
|
|
12505
|
+
hierarchy_root_filter_groups.extend(translated_group)
|
|
12344
12506
|
translated_filters: list[list[dict[str, Any]]] | None
|
|
12345
12507
|
filter_issues: list[dict[str, Any]]
|
|
12346
12508
|
if patch.preserve_filters:
|
|
12347
12509
|
translated_filters = None
|
|
12348
12510
|
filter_issues = []
|
|
12511
|
+
elif patch.filters and isinstance(patch.filters[0], list):
|
|
12512
|
+
# 嵌套形态 [[rule,...],[rule,...]]:组间 OR、组内 AND(viewgraphLimit 多组),
|
|
12513
|
+
# 复用 button_limit 的多组编译;后端简易模式会从多组规则自动派生公式
|
|
12514
|
+
translated_filters, filter_issues = _build_view_button_limit_groups(
|
|
12515
|
+
fields_by_que_id=fields_by_que_id, groups=patch.filters
|
|
12516
|
+
)
|
|
12349
12517
|
else:
|
|
12350
|
-
translated_filters, filter_issues = _build_view_filter_groups(
|
|
12518
|
+
translated_filters, filter_issues = _build_view_filter_groups(fields_by_que_id=fields_by_que_id, filters=patch.filters)
|
|
12351
12519
|
if filter_issues:
|
|
12352
12520
|
first_issue = filter_issues[0]
|
|
12353
12521
|
return _failed(
|
|
@@ -12369,7 +12537,7 @@ class AiBuilderFacade:
|
|
|
12369
12537
|
query_condition_issues = []
|
|
12370
12538
|
else:
|
|
12371
12539
|
query_condition_payload, expected_query_conditions, query_condition_issues = _build_view_query_conditions_payload(
|
|
12372
|
-
|
|
12540
|
+
fields_by_que_id=fields_by_que_id,
|
|
12373
12541
|
query_conditions=patch.query_conditions,
|
|
12374
12542
|
)
|
|
12375
12543
|
if query_condition_issues:
|
|
@@ -12426,7 +12594,7 @@ class AiBuilderFacade:
|
|
|
12426
12594
|
expected_button_summary = None
|
|
12427
12595
|
elif patch.buttons is not None:
|
|
12428
12596
|
explicit_button_dtos, button_issues = _build_view_button_dtos(
|
|
12429
|
-
|
|
12597
|
+
fields_by_que_id=fields_by_que_id,
|
|
12430
12598
|
bindings=patch.buttons,
|
|
12431
12599
|
valid_custom_button_ids=valid_custom_button_ids,
|
|
12432
12600
|
)
|
|
@@ -12484,6 +12652,10 @@ class AiBuilderFacade:
|
|
|
12484
12652
|
existing_key = _extract_view_key(matched_existing_view)
|
|
12485
12653
|
created_key: str | None = None
|
|
12486
12654
|
system_view_list_type = _resolve_system_view_list_type(view_key=existing_key, view_name=patch.name) if existing_key else None
|
|
12655
|
+
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"}:
|
|
12656
|
+
# 环境受限开关:默认视图列序同步依赖 app apply/baseInfo 回读,在快照/脱敏环境上该读面可能不可用;
|
|
12657
|
+
# 视图写主体(view_update)不受影响,仅跳过同步与尾部校验。
|
|
12658
|
+
system_view_list_type = None
|
|
12487
12659
|
operation_phase = "view_update" if existing_key else "view_create"
|
|
12488
12660
|
query_condition_payload_for_apply = deepcopy(query_condition_payload)
|
|
12489
12661
|
expected_query_conditions_for_verify = deepcopy(expected_query_conditions)
|
|
@@ -12517,16 +12689,17 @@ class AiBuilderFacade:
|
|
|
12517
12689
|
views=self.views,
|
|
12518
12690
|
profile=profile,
|
|
12519
12691
|
source_viewgraph_key=existing_key,
|
|
12692
|
+
hierarchy_filter_groups=hierarchy_root_filter_groups,
|
|
12520
12693
|
schema=schema,
|
|
12521
12694
|
patch=patch,
|
|
12522
12695
|
view_filters=translated_filters,
|
|
12523
|
-
|
|
12696
|
+
fields_by_que_id=fields_by_que_id,
|
|
12524
12697
|
auth_override=view_auth_override,
|
|
12525
12698
|
explicit_button_dtos=explicit_button_dtos,
|
|
12526
12699
|
query_condition_payload=query_condition_payload_for_apply,
|
|
12527
12700
|
associated_resources_payload=associated_resources_payload_for_apply,
|
|
12528
12701
|
)
|
|
12529
|
-
self.
|
|
12702
|
+
self._apply_view_update(profile=profile, view_key=existing_key, payload=payload)
|
|
12530
12703
|
system_view_sync: dict[str, Any] | None = None
|
|
12531
12704
|
if system_view_list_type is not None and patch.type.value == "table":
|
|
12532
12705
|
operation_phase = "default_view_apply_config_sync"
|
|
@@ -12537,7 +12710,9 @@ class AiBuilderFacade:
|
|
|
12537
12710
|
payload=payload,
|
|
12538
12711
|
list_type=system_view_list_type,
|
|
12539
12712
|
schema=schema,
|
|
12540
|
-
visible_field_names=
|
|
12713
|
+
visible_field_names=[
|
|
12714
|
+
str((fields_by_que_id.get(que) or {}).get("name") or "") for que in apply_columns if que in fields_by_que_id
|
|
12715
|
+
],
|
|
12541
12716
|
)
|
|
12542
12717
|
if not bool(system_view_sync.get("verified")):
|
|
12543
12718
|
failure_entry = {
|
|
@@ -12562,24 +12737,23 @@ class AiBuilderFacade:
|
|
|
12562
12737
|
"apply_columns": apply_columns,
|
|
12563
12738
|
},
|
|
12564
12739
|
}
|
|
12565
|
-
|
|
12566
|
-
|
|
12740
|
+
failure_result = failure_entry
|
|
12741
|
+
view_result = failure_entry
|
|
12567
12742
|
continue
|
|
12568
12743
|
updated.append(patch.name)
|
|
12569
|
-
|
|
12570
|
-
|
|
12571
|
-
|
|
12572
|
-
|
|
12573
|
-
|
|
12574
|
-
|
|
12575
|
-
|
|
12576
|
-
|
|
12577
|
-
|
|
12578
|
-
|
|
12579
|
-
|
|
12580
|
-
|
|
12581
|
-
|
|
12582
|
-
)
|
|
12744
|
+
view_result = {
|
|
12745
|
+
"name": patch.name,
|
|
12746
|
+
"view_key": existing_key,
|
|
12747
|
+
"type": patch.type.value,
|
|
12748
|
+
"status": "updated",
|
|
12749
|
+
"expected_filters": deepcopy(translated_filters),
|
|
12750
|
+
"expected_buttons": deepcopy(expected_button_summary),
|
|
12751
|
+
"expected_query_conditions": deepcopy(expected_query_conditions_for_verify),
|
|
12752
|
+
"expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
|
|
12753
|
+
"expected_pinned": patch.pinned,
|
|
12754
|
+
"system_view_sync": system_view_sync,
|
|
12755
|
+
"apply_columns": deepcopy(apply_columns),
|
|
12756
|
+
}
|
|
12583
12757
|
record_action_button_intent(patch=patch, view_key=existing_key, index=ordinal - 1)
|
|
12584
12758
|
else:
|
|
12585
12759
|
template_key = _pick_view_template_key(existing_view_list, desired_type=patch.type.value)
|
|
@@ -12591,50 +12765,45 @@ class AiBuilderFacade:
|
|
|
12591
12765
|
views=self.views,
|
|
12592
12766
|
profile=profile,
|
|
12593
12767
|
source_viewgraph_key=created_key,
|
|
12768
|
+
hierarchy_filter_groups=hierarchy_root_filter_groups,
|
|
12594
12769
|
schema=schema,
|
|
12595
12770
|
patch=patch,
|
|
12596
12771
|
view_filters=translated_filters,
|
|
12597
|
-
|
|
12772
|
+
fields_by_que_id=fields_by_que_id,
|
|
12598
12773
|
auth_override=view_auth_override,
|
|
12599
12774
|
explicit_button_dtos=explicit_button_dtos,
|
|
12600
12775
|
query_condition_payload=query_condition_payload_for_apply,
|
|
12601
12776
|
associated_resources_payload=associated_resources_payload_for_apply,
|
|
12602
12777
|
)
|
|
12603
|
-
self.
|
|
12778
|
+
self._apply_view_update(profile=profile, view_key=created_key, payload=payload)
|
|
12604
12779
|
else:
|
|
12605
|
-
payload =
|
|
12780
|
+
payload = _build_view_creation_payload(
|
|
12606
12781
|
app_key=app_key,
|
|
12607
12782
|
base_info=base,
|
|
12608
12783
|
schema=schema,
|
|
12609
12784
|
patch=patch,
|
|
12610
12785
|
ordinal=ordinal,
|
|
12611
12786
|
view_filters=translated_filters,
|
|
12612
|
-
|
|
12787
|
+
fields_by_que_id=fields_by_que_id,
|
|
12613
12788
|
auth_override=view_auth_override,
|
|
12614
12789
|
explicit_button_dtos=explicit_button_dtos,
|
|
12615
12790
|
query_condition_payload=query_condition_payload_for_apply,
|
|
12616
12791
|
associated_resources_payload=associated_resources_payload_for_apply,
|
|
12792
|
+
hierarchy_filter_groups=hierarchy_root_filter_groups,
|
|
12617
12793
|
)
|
|
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
|
|
12794
|
+
created_key = self._apply_view_create(profile=profile, payload=payload)
|
|
12625
12795
|
created.append(patch.name)
|
|
12626
|
-
|
|
12627
|
-
|
|
12628
|
-
|
|
12629
|
-
|
|
12630
|
-
|
|
12631
|
-
|
|
12632
|
-
|
|
12633
|
-
|
|
12634
|
-
|
|
12635
|
-
|
|
12636
|
-
|
|
12637
|
-
)
|
|
12796
|
+
view_result = {
|
|
12797
|
+
"name": patch.name,
|
|
12798
|
+
"view_key": created_key,
|
|
12799
|
+
"type": patch.type.value,
|
|
12800
|
+
"status": "created",
|
|
12801
|
+
"expected_filters": deepcopy(translated_filters),
|
|
12802
|
+
"expected_buttons": deepcopy(expected_button_summary),
|
|
12803
|
+
"expected_query_conditions": deepcopy(expected_query_conditions_for_verify),
|
|
12804
|
+
"expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
|
|
12805
|
+
"expected_pinned": patch.pinned,
|
|
12806
|
+
}
|
|
12638
12807
|
record_action_button_intent(patch=patch, view_key=created_key, index=ordinal - 1)
|
|
12639
12808
|
except (QingflowApiError, RuntimeError) as error:
|
|
12640
12809
|
api_error = _coerce_api_error(error)
|
|
@@ -12661,17 +12830,20 @@ class AiBuilderFacade:
|
|
|
12661
12830
|
patch=patch,
|
|
12662
12831
|
ordinal=ordinal,
|
|
12663
12832
|
view_filters=translated_filters,
|
|
12664
|
-
|
|
12833
|
+
hierarchy_filter_groups=hierarchy_root_filter_groups,
|
|
12834
|
+
fields_by_que_id=fields_by_que_id,
|
|
12665
12835
|
auth_override=view_auth_override,
|
|
12666
12836
|
explicit_button_dtos=fallback_button_dtos,
|
|
12667
12837
|
query_condition_payload=query_condition_payload_for_apply,
|
|
12668
12838
|
associated_resources_payload=associated_resources_payload_for_apply,
|
|
12669
12839
|
)
|
|
12670
|
-
self.
|
|
12840
|
+
self._apply_view_update(profile=profile, view_key=target_key, payload=fallback_payload)
|
|
12671
12841
|
system_view_sync: dict[str, Any] | None = None
|
|
12672
12842
|
fallback_system_view_list_type = (
|
|
12673
12843
|
_resolve_system_view_list_type(view_key=target_key, view_name=patch.name)
|
|
12674
|
-
if patch.type.value == "table"
|
|
12844
|
+
if patch.type.value == "table"
|
|
12845
|
+
and target_key
|
|
12846
|
+
and os.environ.get("QINGFLOW_VIEWS_APPLY_SKIP_SYSTEM_VIEW_SYNC", "").strip().lower() not in {"1", "true", "yes", "on"}
|
|
12675
12847
|
else None
|
|
12676
12848
|
)
|
|
12677
12849
|
if fallback_system_view_list_type is not None:
|
|
@@ -12683,7 +12855,9 @@ class AiBuilderFacade:
|
|
|
12683
12855
|
payload=fallback_payload,
|
|
12684
12856
|
list_type=fallback_system_view_list_type,
|
|
12685
12857
|
schema=schema,
|
|
12686
|
-
visible_field_names=
|
|
12858
|
+
visible_field_names=[
|
|
12859
|
+
str((fields_by_que_id.get(que) or {}).get("name") or "") for que in apply_columns if que in fields_by_que_id
|
|
12860
|
+
],
|
|
12687
12861
|
)
|
|
12688
12862
|
if not bool(system_view_sync.get("verified")):
|
|
12689
12863
|
failure_entry = {
|
|
@@ -12708,44 +12882,42 @@ class AiBuilderFacade:
|
|
|
12708
12882
|
"apply_columns": apply_columns,
|
|
12709
12883
|
},
|
|
12710
12884
|
}
|
|
12711
|
-
|
|
12712
|
-
|
|
12885
|
+
failure_result = failure_entry
|
|
12886
|
+
view_result = failure_entry
|
|
12713
12887
|
continue
|
|
12714
12888
|
if existing_key:
|
|
12715
12889
|
updated.append(patch.name)
|
|
12716
|
-
|
|
12717
|
-
|
|
12718
|
-
|
|
12719
|
-
|
|
12720
|
-
|
|
12721
|
-
|
|
12722
|
-
|
|
12723
|
-
|
|
12724
|
-
|
|
12725
|
-
|
|
12726
|
-
|
|
12727
|
-
|
|
12728
|
-
|
|
12729
|
-
|
|
12730
|
-
)
|
|
12890
|
+
view_result = {
|
|
12891
|
+
"name": patch.name,
|
|
12892
|
+
"view_key": existing_key,
|
|
12893
|
+
"type": patch.type.value,
|
|
12894
|
+
"status": "updated",
|
|
12895
|
+
"fallback_applied": True,
|
|
12896
|
+
"expected_filters": deepcopy(translated_filters),
|
|
12897
|
+
"expected_buttons": deepcopy(expected_button_summary),
|
|
12898
|
+
"expected_query_conditions": deepcopy(expected_query_conditions_for_verify),
|
|
12899
|
+
"expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
|
|
12900
|
+
"expected_pinned": patch.pinned,
|
|
12901
|
+
"system_view_sync": system_view_sync,
|
|
12902
|
+
"apply_columns": deepcopy(apply_columns),
|
|
12903
|
+
}
|
|
12731
12904
|
record_action_button_intent(patch=patch, view_key=existing_key, index=ordinal - 1)
|
|
12732
12905
|
else:
|
|
12733
12906
|
created.append(patch.name)
|
|
12734
|
-
|
|
12735
|
-
|
|
12736
|
-
|
|
12737
|
-
|
|
12738
|
-
|
|
12739
|
-
|
|
12740
|
-
|
|
12741
|
-
|
|
12742
|
-
|
|
12743
|
-
|
|
12744
|
-
|
|
12745
|
-
|
|
12746
|
-
|
|
12747
|
-
|
|
12748
|
-
)
|
|
12907
|
+
view_result = {
|
|
12908
|
+
"name": patch.name,
|
|
12909
|
+
"view_key": created_key,
|
|
12910
|
+
"type": patch.type.value,
|
|
12911
|
+
"status": "created",
|
|
12912
|
+
"fallback_applied": True,
|
|
12913
|
+
"expected_filters": deepcopy(translated_filters),
|
|
12914
|
+
"expected_buttons": deepcopy(expected_button_summary),
|
|
12915
|
+
"expected_query_conditions": deepcopy(expected_query_conditions_for_verify),
|
|
12916
|
+
"expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
|
|
12917
|
+
"expected_pinned": patch.pinned,
|
|
12918
|
+
"system_view_sync": system_view_sync,
|
|
12919
|
+
"apply_columns": deepcopy(apply_columns),
|
|
12920
|
+
}
|
|
12749
12921
|
record_action_button_intent(patch=patch, view_key=created_key, index=ordinal - 1)
|
|
12750
12922
|
continue
|
|
12751
12923
|
fallback_payload = _build_minimal_view_payload(
|
|
@@ -12754,16 +12926,16 @@ class AiBuilderFacade:
|
|
|
12754
12926
|
patch=patch,
|
|
12755
12927
|
ordinal=ordinal,
|
|
12756
12928
|
view_filters=translated_filters,
|
|
12757
|
-
|
|
12929
|
+
hierarchy_filter_groups=hierarchy_root_filter_groups,
|
|
12930
|
+
fields_by_que_id=fields_by_que_id,
|
|
12758
12931
|
auth_override=view_auth_override,
|
|
12759
12932
|
explicit_button_dtos=explicit_button_dtos,
|
|
12760
12933
|
query_condition_payload=query_condition_payload_for_apply,
|
|
12761
12934
|
associated_resources_payload=associated_resources_payload_for_apply,
|
|
12762
12935
|
)
|
|
12763
|
-
self.
|
|
12936
|
+
self._apply_view_create(profile=profile, payload=fallback_payload)
|
|
12764
12937
|
created.append(patch.name)
|
|
12765
|
-
|
|
12766
|
-
{
|
|
12938
|
+
view_result = {
|
|
12767
12939
|
"name": patch.name,
|
|
12768
12940
|
"view_key": created_key,
|
|
12769
12941
|
"type": patch.type.value,
|
|
@@ -12773,7 +12945,6 @@ class AiBuilderFacade:
|
|
|
12773
12945
|
"expected_query_conditions": deepcopy(expected_query_conditions_for_verify),
|
|
12774
12946
|
"expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
|
|
12775
12947
|
}
|
|
12776
|
-
)
|
|
12777
12948
|
record_action_button_intent(patch=patch, view_key=created_key, index=ordinal - 1)
|
|
12778
12949
|
continue
|
|
12779
12950
|
except (QingflowApiError, RuntimeError) as fallback_error:
|
|
@@ -12799,11 +12970,16 @@ class AiBuilderFacade:
|
|
|
12799
12970
|
"view_name": patch.name,
|
|
12800
12971
|
"view_type": patch.type.value,
|
|
12801
12972
|
"columns": patch.columns,
|
|
12802
|
-
"
|
|
12803
|
-
"filters": [
|
|
12804
|
-
|
|
12805
|
-
|
|
12806
|
-
|
|
12973
|
+
"group_que_id": patch.group_que_id,
|
|
12974
|
+
"filters": [
|
|
12975
|
+
(item.model_dump(mode="json") if hasattr(item, "model_dump") else item)
|
|
12976
|
+
if not isinstance(item, list)
|
|
12977
|
+
else [sub.model_dump(mode="json") if hasattr(sub, "model_dump") else sub for sub in item]
|
|
12978
|
+
for item in patch.filters
|
|
12979
|
+
],
|
|
12980
|
+
"start_que_id": patch.start_que_id,
|
|
12981
|
+
"end_que_id": patch.end_que_id,
|
|
12982
|
+
"title_que_id": patch.title_que_id,
|
|
12807
12983
|
"operation": "sync_default_view" if operation_phase == "default_view_apply_config_sync" else ("update" if existing_key or created_key else "create"),
|
|
12808
12984
|
"list_type": system_view_list_type,
|
|
12809
12985
|
"transport_error": {
|
|
@@ -12813,24 +12989,69 @@ class AiBuilderFacade:
|
|
|
12813
12989
|
},
|
|
12814
12990
|
**_view_apply_failure_diagnostics(
|
|
12815
12991
|
patch=patch,
|
|
12816
|
-
|
|
12992
|
+
fields_by_que_id=fields_by_que_id,
|
|
12817
12993
|
backend_code=api_error.backend_code,
|
|
12818
12994
|
operation_phase=operation_phase,
|
|
12819
12995
|
),
|
|
12820
12996
|
},
|
|
12821
12997
|
}
|
|
12822
|
-
|
|
12823
|
-
|
|
12998
|
+
failure_result = failure_entry
|
|
12999
|
+
view_result = failure_entry
|
|
12824
13000
|
continue
|
|
12825
13001
|
view_mutation_ms += _duration_ms(view_mutation_started_at)
|
|
13002
|
+
for item in ((view_result,) if view_result is not None else ()):
|
|
13003
|
+
requested_pinned = item.get("expected_pinned")
|
|
13004
|
+
view_key = str(item.get("view_key") or "").strip()
|
|
13005
|
+
if (
|
|
13006
|
+
str(item.get("status") or "") not in {"created", "updated"}
|
|
13007
|
+
or not isinstance(requested_pinned, bool)
|
|
13008
|
+
or not view_key
|
|
13009
|
+
):
|
|
13010
|
+
continue
|
|
13011
|
+
try:
|
|
13012
|
+
pin_result = self._set_view_pinned(
|
|
13013
|
+
profile=profile,
|
|
13014
|
+
app_key=app_key,
|
|
13015
|
+
view_key=view_key,
|
|
13016
|
+
pinned=requested_pinned,
|
|
13017
|
+
)
|
|
13018
|
+
item["pinned_state"] = pin_result
|
|
13019
|
+
if not bool(pin_result.get("verified")):
|
|
13020
|
+
failure_result = {
|
|
13021
|
+
"name": item.get("name"),
|
|
13022
|
+
"view_key": view_key,
|
|
13023
|
+
"type": item.get("type"),
|
|
13024
|
+
"status": "failed",
|
|
13025
|
+
"error_code": "VIEW_PIN_STATE_READBACK_MISMATCH",
|
|
13026
|
+
"message": "view configuration was saved, but its pinned state did not verify",
|
|
13027
|
+
"operation": "set_pinned",
|
|
13028
|
+
"details": pin_result,
|
|
13029
|
+
}
|
|
13030
|
+
except (QingflowApiError, RuntimeError, ValueError) as error:
|
|
13031
|
+
api_error = _coerce_api_error(error)
|
|
13032
|
+
pin_failure = {
|
|
13033
|
+
"name": item.get("name"),
|
|
13034
|
+
"view_key": view_key,
|
|
13035
|
+
"type": item.get("type"),
|
|
13036
|
+
"status": "failed",
|
|
13037
|
+
"error_code": "VIEW_PIN_STATE_APPLY_FAILED",
|
|
13038
|
+
"message": "view configuration was saved, but updating its pinned state failed",
|
|
13039
|
+
"request_id": api_error.request_id,
|
|
13040
|
+
"backend_code": api_error.backend_code,
|
|
13041
|
+
"http_status": api_error.http_status,
|
|
13042
|
+
"operation": "set_pinned",
|
|
13043
|
+
"details": {"requested_pinned": requested_pinned},
|
|
13044
|
+
}
|
|
13045
|
+
item["pinned_state"] = {"requested_pinned": requested_pinned, "verified": False}
|
|
13046
|
+
failure_result = pin_failure
|
|
12826
13047
|
successful_action_view_keys = {
|
|
12827
13048
|
str(item.get("view_key") or "").strip()
|
|
12828
|
-
for item in
|
|
13049
|
+
for item in ((view_result,) if view_result is not None else ())
|
|
12829
13050
|
if str(item.get("status") or "") in {"created", "updated"} and str(item.get("view_key") or "").strip()
|
|
12830
13051
|
}
|
|
12831
13052
|
successful_action_view_names = {
|
|
12832
13053
|
str(item.get("name") or "").strip()
|
|
12833
|
-
for item in
|
|
13054
|
+
for item in ((view_result,) if view_result is not None else ())
|
|
12834
13055
|
if str(item.get("status") or "") in {"created", "updated"} and str(item.get("name") or "").strip()
|
|
12835
13056
|
}
|
|
12836
13057
|
skipped_action_button_intents = [
|
|
@@ -12881,12 +13102,12 @@ class AiBuilderFacade:
|
|
|
12881
13102
|
action_buttons_failed = bool(action_button_intents) and not (action_buttons_verified and view_action_button_bindings_verified)
|
|
12882
13103
|
action_buttons_retry_payload = action_buttons_result.get("retry_payload") if isinstance(action_buttons_result.get("retry_payload"), dict) else None
|
|
12883
13104
|
needs_view_list_readback = bool(created or updated)
|
|
12884
|
-
|
|
13105
|
+
verified_views: list[dict[str, Any]] | None = []
|
|
12885
13106
|
verified_views_unavailable = False
|
|
12886
13107
|
if needs_view_list_readback:
|
|
12887
13108
|
try:
|
|
12888
13109
|
view_readback_started_at = time.perf_counter()
|
|
12889
|
-
|
|
13110
|
+
verified_views, verified_views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
12890
13111
|
except (QingflowApiError, RuntimeError) as error:
|
|
12891
13112
|
view_readback_ms += _duration_ms(view_readback_started_at)
|
|
12892
13113
|
api_error = _coerce_api_error(error)
|
|
@@ -12900,23 +13121,23 @@ class AiBuilderFacade:
|
|
|
12900
13121
|
view_readback_ms += _duration_ms(view_readback_started_at)
|
|
12901
13122
|
verified_names = {
|
|
12902
13123
|
_extract_view_name(item)
|
|
12903
|
-
for item in (
|
|
13124
|
+
for item in (verified_views or [])
|
|
12904
13125
|
if isinstance(item, dict) and _extract_view_name(item)
|
|
12905
13126
|
}
|
|
12906
13127
|
verified_by_key = {
|
|
12907
13128
|
_extract_view_key(item): item
|
|
12908
|
-
for item in (
|
|
13129
|
+
for item in (verified_views or [])
|
|
12909
13130
|
if isinstance(item, dict) and _extract_view_key(item)
|
|
12910
13131
|
}
|
|
12911
13132
|
verified_view_keys_by_name: dict[str, list[str]] = {}
|
|
12912
|
-
for item in (
|
|
13133
|
+
for item in (verified_views or []):
|
|
12913
13134
|
if not isinstance(item, dict):
|
|
12914
13135
|
continue
|
|
12915
13136
|
name = _extract_view_name(item)
|
|
12916
13137
|
key = _extract_view_key(item)
|
|
12917
13138
|
if name and key:
|
|
12918
13139
|
verified_view_keys_by_name.setdefault(name, []).append(key)
|
|
12919
|
-
|
|
13140
|
+
verification_result: dict[str, Any] | None = None
|
|
12920
13141
|
filter_readback_pending = False
|
|
12921
13142
|
filter_mismatches: list[dict[str, Any]] = []
|
|
12922
13143
|
query_condition_readback_pending = False
|
|
@@ -12943,7 +13164,7 @@ class AiBuilderFacade:
|
|
|
12943
13164
|
return cached
|
|
12944
13165
|
|
|
12945
13166
|
view_verification_started_at = time.perf_counter()
|
|
12946
|
-
for item in
|
|
13167
|
+
for item in ((view_result,) if view_result is not None else ()):
|
|
12947
13168
|
status = str(item.get("status") or "")
|
|
12948
13169
|
name = str(item.get("name") or "")
|
|
12949
13170
|
item_view_key = str(item.get("view_key") or "").strip()
|
|
@@ -12986,7 +13207,7 @@ class AiBuilderFacade:
|
|
|
12986
13207
|
verification_entry["readback_ambiguous"] = True
|
|
12987
13208
|
verification_entry["matching_view_keys"] = matched_keys
|
|
12988
13209
|
filter_readback_pending = True
|
|
12989
|
-
|
|
13210
|
+
verification_result = verification_entry
|
|
12990
13211
|
continue
|
|
12991
13212
|
try:
|
|
12992
13213
|
config_result, config_error = read_view_config_for_verification(verification_key)
|
|
@@ -13004,11 +13225,11 @@ class AiBuilderFacade:
|
|
|
13004
13225
|
filters_verified = filters_semantically_equivalent and actual_data_scope == expected_data_scope
|
|
13005
13226
|
public_expected_filters = _public_view_filter_groups_from_match_rules(
|
|
13006
13227
|
expected_filter_summary,
|
|
13007
|
-
|
|
13228
|
+
fields_by_que_id=fields_by_que_id,
|
|
13008
13229
|
)
|
|
13009
13230
|
public_actual_filters = _public_view_filter_groups_from_match_rules(
|
|
13010
13231
|
semantic_actual_filters,
|
|
13011
|
-
|
|
13232
|
+
fields_by_que_id=fields_by_que_id,
|
|
13012
13233
|
)
|
|
13013
13234
|
verification_entry["filters_verified"] = filters_verified
|
|
13014
13235
|
verification_entry["view_key"] = verification_key
|
|
@@ -13058,7 +13279,7 @@ class AiBuilderFacade:
|
|
|
13058
13279
|
verification_entry["readback_ambiguous"] = True
|
|
13059
13280
|
verification_entry["matching_view_keys"] = matched_keys
|
|
13060
13281
|
query_condition_readback_pending = True
|
|
13061
|
-
|
|
13282
|
+
verification_result = verification_entry
|
|
13062
13283
|
continue
|
|
13063
13284
|
try:
|
|
13064
13285
|
config_result, config_error = read_view_config_for_verification(verification_key)
|
|
@@ -13107,7 +13328,7 @@ class AiBuilderFacade:
|
|
|
13107
13328
|
verification_entry["readback_ambiguous"] = True
|
|
13108
13329
|
verification_entry["matching_view_keys"] = matched_keys
|
|
13109
13330
|
associated_resource_readback_pending = True
|
|
13110
|
-
|
|
13331
|
+
verification_result = verification_entry
|
|
13111
13332
|
continue
|
|
13112
13333
|
try:
|
|
13113
13334
|
config_result, config_error = read_view_config_for_verification(verification_key)
|
|
@@ -13162,7 +13383,7 @@ class AiBuilderFacade:
|
|
|
13162
13383
|
verification_entry["readback_ambiguous"] = True
|
|
13163
13384
|
verification_entry["matching_view_keys"] = matched_keys
|
|
13164
13385
|
button_readback_pending = True
|
|
13165
|
-
|
|
13386
|
+
verification_result = verification_entry
|
|
13166
13387
|
continue
|
|
13167
13388
|
try:
|
|
13168
13389
|
config_result, config_error = read_view_config_for_verification(verification_key)
|
|
@@ -13210,51 +13431,45 @@ class AiBuilderFacade:
|
|
|
13210
13431
|
"category": api_error.category,
|
|
13211
13432
|
}
|
|
13212
13433
|
button_readback_pending = True
|
|
13213
|
-
|
|
13434
|
+
verification_result = verification_entry
|
|
13214
13435
|
elif status == "removed":
|
|
13215
|
-
|
|
13216
|
-
|
|
13217
|
-
|
|
13218
|
-
|
|
13219
|
-
|
|
13220
|
-
|
|
13221
|
-
|
|
13222
|
-
|
|
13223
|
-
|
|
13224
|
-
|
|
13225
|
-
|
|
13226
|
-
}
|
|
13227
|
-
)
|
|
13436
|
+
verification_result = {
|
|
13437
|
+
"name": name,
|
|
13438
|
+
"view_key": item.get("view_key"),
|
|
13439
|
+
"type": item.get("type"),
|
|
13440
|
+
"status": "removed",
|
|
13441
|
+
"present_in_readback": False,
|
|
13442
|
+
"removed_verified": True,
|
|
13443
|
+
"delete_executed": bool(item.get("delete_executed")),
|
|
13444
|
+
"readback_status": item.get("readback_status") or "deleted",
|
|
13445
|
+
"safe_to_retry_delete": False,
|
|
13446
|
+
}
|
|
13228
13447
|
elif status == "readback_pending" and item.get("operation") == "delete":
|
|
13229
13448
|
readback_status = str(item.get("readback_status") or "unavailable")
|
|
13230
|
-
|
|
13231
|
-
|
|
13232
|
-
|
|
13233
|
-
|
|
13234
|
-
|
|
13235
|
-
|
|
13236
|
-
|
|
13237
|
-
|
|
13238
|
-
|
|
13239
|
-
|
|
13240
|
-
|
|
13241
|
-
|
|
13242
|
-
}
|
|
13243
|
-
)
|
|
13449
|
+
verification_result = {
|
|
13450
|
+
"name": name,
|
|
13451
|
+
"view_key": item.get("view_key"),
|
|
13452
|
+
"type": item.get("type"),
|
|
13453
|
+
"status": "readback_pending",
|
|
13454
|
+
"present_in_readback": True if readback_status == "still_exists" else None,
|
|
13455
|
+
"removed_verified": False,
|
|
13456
|
+
"delete_executed": bool(item.get("delete_executed")),
|
|
13457
|
+
"readback_status": readback_status,
|
|
13458
|
+
"safe_to_retry_delete": False,
|
|
13459
|
+
"error_code": item.get("error_code"),
|
|
13460
|
+
}
|
|
13244
13461
|
else:
|
|
13245
|
-
|
|
13246
|
-
|
|
13247
|
-
|
|
13248
|
-
|
|
13249
|
-
|
|
13250
|
-
|
|
13251
|
-
|
|
13252
|
-
}
|
|
13253
|
-
)
|
|
13462
|
+
verification_result = {
|
|
13463
|
+
"name": item.get("name"),
|
|
13464
|
+
"type": item.get("type"),
|
|
13465
|
+
"status": "failed",
|
|
13466
|
+
"present_in_readback": None,
|
|
13467
|
+
"error_code": item.get("error_code"),
|
|
13468
|
+
}
|
|
13254
13469
|
view_verification_ms += _duration_ms(view_verification_started_at)
|
|
13255
13470
|
removed_delete_results = [
|
|
13256
13471
|
item
|
|
13257
|
-
for item in
|
|
13472
|
+
for item in ((view_result,) if view_result is not None else ())
|
|
13258
13473
|
if item.get("operation") == "delete" and bool(item.get("delete_executed"))
|
|
13259
13474
|
]
|
|
13260
13475
|
removed_verified = all(str(item.get("readback_status") or "") == "deleted" for item in removed_delete_results)
|
|
@@ -13269,19 +13484,19 @@ class AiBuilderFacade:
|
|
|
13269
13484
|
view_associated_resources_verified = verified and not associated_resource_readback_pending and not associated_resource_mismatches
|
|
13270
13485
|
view_buttons_verified = verified and not button_readback_pending and not button_mismatches and not custom_button_readback_pending
|
|
13271
13486
|
noop = not created and not updated and not removed and not action_button_write_executed
|
|
13272
|
-
if
|
|
13487
|
+
if failure_result is not None:
|
|
13273
13488
|
successful_changes = bool(created or updated or removed or action_button_write_succeeded)
|
|
13274
|
-
first_failure =
|
|
13489
|
+
first_failure = failure_result
|
|
13275
13490
|
response = {
|
|
13276
13491
|
"status": "partial_success" if successful_changes else "failed",
|
|
13277
13492
|
"error_code": "VIEW_APPLY_PARTIAL" if successful_changes else "VIEW_APPLY_FAILED",
|
|
13278
13493
|
"recoverable": True,
|
|
13279
|
-
"message": "
|
|
13494
|
+
"message": "view operation completed with a verification failure" if successful_changes else "view operation failed",
|
|
13280
13495
|
"normalized_args": normalized_args,
|
|
13281
13496
|
"missing_fields": [],
|
|
13282
13497
|
"allowed_values": {"view_types": [member.value for member in PublicViewType], "view.filter.operator": [member.value for member in ViewFilterOperator]},
|
|
13283
13498
|
"details": {
|
|
13284
|
-
"
|
|
13499
|
+
"view_result": deepcopy(view_result) if view_result is not None else None,
|
|
13285
13500
|
"filter_mismatches": filter_mismatches,
|
|
13286
13501
|
"query_condition_mismatches": query_condition_mismatches,
|
|
13287
13502
|
"associated_resource_mismatches": associated_resource_mismatches,
|
|
@@ -13339,13 +13554,14 @@ class AiBuilderFacade:
|
|
|
13339
13554
|
"action_buttons_verified": action_buttons_verified,
|
|
13340
13555
|
"view_button_bindings_verified": view_action_button_bindings_verified,
|
|
13341
13556
|
"views_read_unavailable": verified_views_unavailable,
|
|
13342
|
-
"
|
|
13557
|
+
"view": deepcopy(verification_result) if verification_result is not None else None,
|
|
13343
13558
|
"custom_button_readback_pending": custom_button_readback_pending,
|
|
13344
13559
|
"custom_button_readback_pending_entries": deepcopy(custom_button_readback_pending_entries),
|
|
13345
13560
|
},
|
|
13346
13561
|
"app_key": app_key,
|
|
13347
13562
|
"app_name": app_name,
|
|
13348
|
-
"
|
|
13563
|
+
"view_result": deepcopy(view_result) if view_result is not None else None,
|
|
13564
|
+
"views_diff": {"created": created, "updated": updated, "removed": removed, "failed": [failure_result]},
|
|
13349
13565
|
"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
13566
|
"write_executed": bool(created or updated or removed or action_button_write_executed),
|
|
13351
13567
|
"write_succeeded": bool(created or updated or removed or action_button_write_succeeded),
|
|
@@ -13414,6 +13630,7 @@ class AiBuilderFacade:
|
|
|
13414
13630
|
**({"query_condition_mismatches": query_condition_mismatches} if query_condition_mismatches else {}),
|
|
13415
13631
|
**({"associated_resource_mismatches": associated_resource_mismatches} if associated_resource_mismatches else {}),
|
|
13416
13632
|
**({"button_mismatches": button_mismatches} if button_mismatches else {}),
|
|
13633
|
+
"view_result": deepcopy(view_result) if view_result is not None else None,
|
|
13417
13634
|
**({"action_buttons_result": action_buttons_result} if action_button_intents else {}),
|
|
13418
13635
|
**(
|
|
13419
13636
|
{"custom_button_readback_pending": deepcopy(custom_button_readback_pending_entries)}
|
|
@@ -13441,10 +13658,11 @@ class AiBuilderFacade:
|
|
|
13441
13658
|
"delete_readback_pending": delete_readback_pending,
|
|
13442
13659
|
"custom_button_readback_pending": custom_button_readback_pending,
|
|
13443
13660
|
"custom_button_readback_pending_entries": deepcopy(custom_button_readback_pending_entries),
|
|
13444
|
-
"
|
|
13661
|
+
"view": deepcopy(verification_result) if verification_result is not None else None,
|
|
13445
13662
|
},
|
|
13446
13663
|
"app_key": app_key,
|
|
13447
13664
|
"app_name": app_name,
|
|
13665
|
+
"view_result": deepcopy(view_result) if view_result is not None else None,
|
|
13448
13666
|
"views_diff": {"created": created, "updated": updated, "removed": removed, "failed": []},
|
|
13449
13667
|
"verified": all_verified,
|
|
13450
13668
|
"write_executed": bool(created or updated or removed or action_button_write_executed),
|
|
@@ -13453,6 +13671,305 @@ class AiBuilderFacade:
|
|
|
13453
13671
|
}
|
|
13454
13672
|
return finish_with_publish(response)
|
|
13455
13673
|
|
|
13674
|
+
def app_views_schema(
|
|
13675
|
+
self,
|
|
13676
|
+
*,
|
|
13677
|
+
schema_version: str | None = None,
|
|
13678
|
+
schema_kind: str | None = None,
|
|
13679
|
+
) -> JSONObject:
|
|
13680
|
+
"""读取版本化视图声明 schema;detail kind 用于获取专项配置定义。"""
|
|
13681
|
+
normalized_args = {"schema_version": schema_version, "schema_kind": schema_kind}
|
|
13682
|
+
try:
|
|
13683
|
+
payload = views_schema_registry().schema(schema_version=schema_version, schema_kind=schema_kind)
|
|
13684
|
+
except ViewsContractError as error:
|
|
13685
|
+
return _failed(
|
|
13686
|
+
error.error_code,
|
|
13687
|
+
str(error.message),
|
|
13688
|
+
normalized_args=normalized_args,
|
|
13689
|
+
details={"reason_path": error.path or "", **(error.details or {})},
|
|
13690
|
+
suggested_next_call={"tool_name": "app_views_schema", "arguments": {"schema_kind": "summary"}},
|
|
13691
|
+
)
|
|
13692
|
+
return {
|
|
13693
|
+
"status": "success",
|
|
13694
|
+
"error_code": None,
|
|
13695
|
+
"recoverable": False,
|
|
13696
|
+
"message": "read views declaration schema",
|
|
13697
|
+
"normalized_args": normalized_args,
|
|
13698
|
+
"missing_fields": [],
|
|
13699
|
+
"allowed_values": {"schema_kind": payload["supportedSchemaKinds"]},
|
|
13700
|
+
"details": {},
|
|
13701
|
+
"request_id": None,
|
|
13702
|
+
"suggested_next_call": None,
|
|
13703
|
+
"warnings": [],
|
|
13704
|
+
"verification": {"schema_loaded": True},
|
|
13705
|
+
**payload,
|
|
13706
|
+
}
|
|
13707
|
+
|
|
13708
|
+
def app_views_get(
|
|
13709
|
+
self,
|
|
13710
|
+
*,
|
|
13711
|
+
profile: str,
|
|
13712
|
+
app_key: str,
|
|
13713
|
+
schema_version: str | None = None,
|
|
13714
|
+
schema_kind: str = "summary",
|
|
13715
|
+
) -> JSONObject:
|
|
13716
|
+
"""读取应用的 canonical 视图声明数组(summary 级投影或 complete 反向编译)。"""
|
|
13717
|
+
normalized_args = {"app_key": app_key, "schema_version": schema_version, "schema_kind": schema_kind}
|
|
13718
|
+
try:
|
|
13719
|
+
context = views_schema_registry().resolve_schema_version(schema_version)
|
|
13720
|
+
except ViewsContractError as error:
|
|
13721
|
+
return _failed(
|
|
13722
|
+
error.error_code,
|
|
13723
|
+
str(error.message),
|
|
13724
|
+
normalized_args=normalized_args,
|
|
13725
|
+
details={"reason_path": error.path or "", **(error.details or {})},
|
|
13726
|
+
suggested_next_call={"tool_name": "app_views_schema", "arguments": {"schema_kind": "summary"}},
|
|
13727
|
+
)
|
|
13728
|
+
try:
|
|
13729
|
+
views, _views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=False)
|
|
13730
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
13731
|
+
api_error = _coerce_api_error(error)
|
|
13732
|
+
return _failed_from_api_error(
|
|
13733
|
+
"VIEWS_READ_FAILED",
|
|
13734
|
+
api_error,
|
|
13735
|
+
normalized_args=normalized_args,
|
|
13736
|
+
details=_with_state_read_blocked_details({"app_key": app_key}, resource="views", error=api_error),
|
|
13737
|
+
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
13738
|
+
)
|
|
13739
|
+
schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
13740
|
+
parsed_schema = _parse_schema(schema)
|
|
13741
|
+
# 读面 summary 面向 agent 展示字段名:从原始 schema 递归收集(同名不丢);
|
|
13742
|
+
# 系统列(queId<=5)用公开名映射,未知系统列以 queId 展示兜底
|
|
13743
|
+
name_by_que_id: dict[int, str] = {
|
|
13744
|
+
**{system_id: name for system_id, name in _SYSTEM_VIEW_QUE_NAMES.items()},
|
|
13745
|
+
**_collect_field_id_to_name(schema),
|
|
13746
|
+
}
|
|
13747
|
+
normalized_kind = str(schema_kind or "summary").strip().lower()
|
|
13748
|
+
if normalized_kind == "complete":
|
|
13749
|
+
return self._app_views_get_complete(
|
|
13750
|
+
profile=profile,
|
|
13751
|
+
app_key=app_key,
|
|
13752
|
+
views=views,
|
|
13753
|
+
parsed_schema=parsed_schema,
|
|
13754
|
+
normalized_args=normalized_args,
|
|
13755
|
+
schema_version=schema_version,
|
|
13756
|
+
)
|
|
13757
|
+
projected: list[dict[str, Any]] = []
|
|
13758
|
+
scope_map = {"ALL": "all", "ONESELF": "oneself", "DEPT": "dept", "SUBORDINATE": "subordinate", "CUSTOM": "custom"}
|
|
13759
|
+
for view in views if isinstance(views, list) else []:
|
|
13760
|
+
if not isinstance(view, dict):
|
|
13761
|
+
continue
|
|
13762
|
+
entry: dict[str, Any] = {
|
|
13763
|
+
"view_key": _extract_view_key(view),
|
|
13764
|
+
"name": _extract_view_name(view),
|
|
13765
|
+
"type": _normalize_view_type_name(view.get("viewgraphType") or view.get("type")),
|
|
13766
|
+
}
|
|
13767
|
+
columns = [name_by_que_id.get(que_id, str(que_id)) for que_id in view.get("viewgraphQueIds") or [] if isinstance(que_id, int)]
|
|
13768
|
+
if columns:
|
|
13769
|
+
entry["columns"] = columns
|
|
13770
|
+
group_que_id = _coerce_positive_int(view.get("groupQueId"))
|
|
13771
|
+
if group_que_id is not None and group_que_id in name_by_que_id:
|
|
13772
|
+
entry["group_by"] = name_by_que_id[group_que_id]
|
|
13773
|
+
gantt_config = view.get("viewgraphGanttConfigVO")
|
|
13774
|
+
if isinstance(gantt_config, dict):
|
|
13775
|
+
for source, target in (
|
|
13776
|
+
("startTimeQueId", "start_field"),
|
|
13777
|
+
("endTimeQueId", "end_field"),
|
|
13778
|
+
("titleQueId", "title_field"),
|
|
13779
|
+
):
|
|
13780
|
+
gantt_que_id = _coerce_positive_int(gantt_config.get(source))
|
|
13781
|
+
if gantt_que_id is not None and gantt_que_id in name_by_que_id:
|
|
13782
|
+
entry[target] = name_by_que_id[gantt_que_id]
|
|
13783
|
+
title_que_id = _coerce_positive_int(view.get("titleQue"))
|
|
13784
|
+
if title_que_id is not None and title_que_id in name_by_que_id:
|
|
13785
|
+
entry.setdefault("title_field", name_by_que_id[title_que_id])
|
|
13786
|
+
data_permission_type = view.get("dataPermissionType")
|
|
13787
|
+
if data_permission_type:
|
|
13788
|
+
entry["data_permission"] = {
|
|
13789
|
+
"type": "config" if str(data_permission_type).upper() == "CONFIG" else "custom",
|
|
13790
|
+
"scope": scope_map.get(str(view.get("dataScope") or "").upper(), "all"),
|
|
13791
|
+
}
|
|
13792
|
+
projected.append(entry)
|
|
13793
|
+
return {
|
|
13794
|
+
"status": "success",
|
|
13795
|
+
"error_code": None,
|
|
13796
|
+
"recoverable": False,
|
|
13797
|
+
"message": "read canonical views declarations",
|
|
13798
|
+
"normalized_args": normalized_args,
|
|
13799
|
+
"missing_fields": [],
|
|
13800
|
+
"allowed_values": {"view_types": [member.value for member in PublicViewType]},
|
|
13801
|
+
"details": {},
|
|
13802
|
+
"request_id": None,
|
|
13803
|
+
"suggested_next_call": None,
|
|
13804
|
+
"warnings": [],
|
|
13805
|
+
"verification": {"views_read": len(projected)},
|
|
13806
|
+
"schemaVersion": context.schema_version,
|
|
13807
|
+
"apiVersion": context.api_version,
|
|
13808
|
+
"latestVersion": views_schema_registry().latest_version,
|
|
13809
|
+
"app_key": app_key,
|
|
13810
|
+
"views": projected,
|
|
13811
|
+
}
|
|
13812
|
+
|
|
13813
|
+
def _app_views_get_complete(
|
|
13814
|
+
self,
|
|
13815
|
+
*,
|
|
13816
|
+
profile: str,
|
|
13817
|
+
app_key: str,
|
|
13818
|
+
views: Any,
|
|
13819
|
+
parsed_schema: dict[str, Any],
|
|
13820
|
+
normalized_args: JSONObject,
|
|
13821
|
+
schema_version: str | None,
|
|
13822
|
+
) -> JSONObject:
|
|
13823
|
+
"""complete 档 canonical:逐视图 GET /view/{key}/viewConfig → 反向编译。"""
|
|
13824
|
+
from .views_extract import extract_complete_view_declaration, summarize_gaps
|
|
13825
|
+
|
|
13826
|
+
fields_by_que_id = _collect_fields_by_que_id(parsed_schema["fields"])
|
|
13827
|
+
projected: list[dict[str, Any]] = []
|
|
13828
|
+
read_errors: list[dict[str, Any]] = []
|
|
13829
|
+
all_gaps: list[dict[str, Any]] = []
|
|
13830
|
+
for view in views if isinstance(views, list) else []:
|
|
13831
|
+
if not isinstance(view, dict):
|
|
13832
|
+
continue
|
|
13833
|
+
view_key = _extract_view_key(view)
|
|
13834
|
+
view_name = _extract_view_name(view)
|
|
13835
|
+
if not view_key:
|
|
13836
|
+
continue
|
|
13837
|
+
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")):
|
|
13838
|
+
# 系统视图(列表/申请页等,viewgraphKey 为固定数字 id):不在公开声明语义内,直接跳过。
|
|
13839
|
+
continue
|
|
13840
|
+
try:
|
|
13841
|
+
config = self.views.view_get_config(profile=profile, viewgraph_key=view_key).get("result") or {}
|
|
13842
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
13843
|
+
api_error = _coerce_api_error(error)
|
|
13844
|
+
read_errors.append(
|
|
13845
|
+
{
|
|
13846
|
+
"view_key": view_key,
|
|
13847
|
+
"name": view_name,
|
|
13848
|
+
"error_code": "VIEW_CONFIG_READ_FAILED",
|
|
13849
|
+
"http_status": api_error.http_status,
|
|
13850
|
+
"message": str(api_error.message or error),
|
|
13851
|
+
}
|
|
13852
|
+
)
|
|
13853
|
+
continue
|
|
13854
|
+
if not isinstance(config, dict) or not config:
|
|
13855
|
+
read_errors.append({"view_key": view_key, "name": view_name, "error_code": "VIEW_CONFIG_EMPTY", "message": "viewConfig read returned no data"})
|
|
13856
|
+
continue
|
|
13857
|
+
declaration, gaps = extract_complete_view_declaration(config, fields_by_que_id=fields_by_que_id)
|
|
13858
|
+
if not declaration:
|
|
13859
|
+
read_errors.append(
|
|
13860
|
+
{
|
|
13861
|
+
"view_key": view_key,
|
|
13862
|
+
"name": view_name,
|
|
13863
|
+
"error_code": "VIEW_NOT_EXTRACTABLE",
|
|
13864
|
+
"message": "view configuration cannot be represented completely by the public contract; see gaps",
|
|
13865
|
+
"gaps": gaps,
|
|
13866
|
+
}
|
|
13867
|
+
)
|
|
13868
|
+
continue
|
|
13869
|
+
if gaps:
|
|
13870
|
+
all_gaps.append({"view_key": view_key, "gaps": gaps})
|
|
13871
|
+
projected.append(declaration)
|
|
13872
|
+
gap_summary: dict[str, int] = {}
|
|
13873
|
+
for item in all_gaps:
|
|
13874
|
+
for kind, count in summarize_gaps(item["gaps"]).items():
|
|
13875
|
+
gap_summary[kind] = gap_summary.get(kind, 0) + count
|
|
13876
|
+
context = views_schema_registry().resolve_schema_version(schema_version)
|
|
13877
|
+
return {
|
|
13878
|
+
"status": "success",
|
|
13879
|
+
"error_code": None,
|
|
13880
|
+
"recoverable": False,
|
|
13881
|
+
"message": "read canonical views declarations (complete)",
|
|
13882
|
+
"normalized_args": normalized_args,
|
|
13883
|
+
"missing_fields": [],
|
|
13884
|
+
"allowed_values": {"view_types": [member.value for member in PublicViewType]},
|
|
13885
|
+
"details": {
|
|
13886
|
+
"extraction_gap_summary": gap_summary,
|
|
13887
|
+
"extraction_gaps": all_gaps[:50],
|
|
13888
|
+
"config_read_errors": read_errors[:50],
|
|
13889
|
+
"config_read_error_count": len(read_errors),
|
|
13890
|
+
},
|
|
13891
|
+
"request_id": None,
|
|
13892
|
+
"suggested_next_call": None,
|
|
13893
|
+
"warnings": [],
|
|
13894
|
+
"verification": {"views_read": len(projected), "views_with_gaps": len(all_gaps), "read_errors": len(read_errors)},
|
|
13895
|
+
"schemaVersion": context.schema_version,
|
|
13896
|
+
"apiVersion": context.api_version,
|
|
13897
|
+
"latestVersion": views_schema_registry().latest_version,
|
|
13898
|
+
"app_key": app_key,
|
|
13899
|
+
"views": projected,
|
|
13900
|
+
}
|
|
13901
|
+
|
|
13902
|
+
def app_views_validate(
|
|
13903
|
+
self,
|
|
13904
|
+
*,
|
|
13905
|
+
declaration: JSONObject,
|
|
13906
|
+
schema_version: str | None = None,
|
|
13907
|
+
) -> JSONObject:
|
|
13908
|
+
"""纯离线校验一条视图声明;不访问后端。"""
|
|
13909
|
+
normalized_args = {"schema_version": schema_version}
|
|
13910
|
+
resolved_version = str(schema_version or (declaration.get("schema_version") if isinstance(declaration, dict) else None) or "").strip() or None
|
|
13911
|
+
try:
|
|
13912
|
+
context = views_schema_registry().resolve_schema_version(resolved_version)
|
|
13913
|
+
except ViewsContractError as error:
|
|
13914
|
+
return _failed(
|
|
13915
|
+
error.error_code,
|
|
13916
|
+
str(error.message),
|
|
13917
|
+
normalized_args=normalized_args,
|
|
13918
|
+
details={"reason_path": error.path or "", **(error.details or {})},
|
|
13919
|
+
suggested_next_call={"tool_name": "app_views_schema", "arguments": {"schema_kind": "complete"}},
|
|
13920
|
+
)
|
|
13921
|
+
issues: list[dict[str, Any]] = []
|
|
13922
|
+
batch_keys = ("views", "upsert_views", "upsertViews", "apps")
|
|
13923
|
+
if not isinstance(declaration, dict):
|
|
13924
|
+
issues.append({"path": "/", "code": "INVALID_SPEC", "message": "views declaration must be a JSON object"})
|
|
13925
|
+
elif any(key in declaration for key in batch_keys):
|
|
13926
|
+
issues.append(
|
|
13927
|
+
{
|
|
13928
|
+
"path": "/",
|
|
13929
|
+
"code": "VIEW_BATCH_CREATE_UNSUPPORTED",
|
|
13930
|
+
"message": "views schema validates one view declaration; batch envelopes are unsupported",
|
|
13931
|
+
}
|
|
13932
|
+
)
|
|
13933
|
+
else:
|
|
13934
|
+
declaration_item = {
|
|
13935
|
+
key: value
|
|
13936
|
+
for key, value in declaration.items()
|
|
13937
|
+
if key not in {"schema_version", "schemaVersion", "apiVersion"}
|
|
13938
|
+
}
|
|
13939
|
+
try:
|
|
13940
|
+
parsed = ViewUpsertPatch.model_validate(declaration_item)
|
|
13941
|
+
context.validate_declaration(compact_none_values(public_view_upsert_payload(parsed)))
|
|
13942
|
+
except ValidationError as exc:
|
|
13943
|
+
first_error = exc.errors()[0]
|
|
13944
|
+
issues.append(
|
|
13945
|
+
{
|
|
13946
|
+
"path": f"/{'/'.join(str(part) for part in first_error.get('loc', ()))}".rstrip("/"),
|
|
13947
|
+
"code": "INVALID_SPEC",
|
|
13948
|
+
"message": str(first_error.get("msg") or exc),
|
|
13949
|
+
}
|
|
13950
|
+
)
|
|
13951
|
+
except ViewsContractError as error:
|
|
13952
|
+
issues.append({"path": error.path or "/", "code": error.error_code, "message": str(error.message)})
|
|
13953
|
+
ok = not issues
|
|
13954
|
+
return {
|
|
13955
|
+
"status": "success" if ok else "failed",
|
|
13956
|
+
"error_code": None if ok else issues[0]["code"],
|
|
13957
|
+
"recoverable": True,
|
|
13958
|
+
"message": "views declaration is valid" if ok else "views declaration failed validation",
|
|
13959
|
+
"normalized_args": normalized_args,
|
|
13960
|
+
"missing_fields": [],
|
|
13961
|
+
"allowed_values": {},
|
|
13962
|
+
"details": {"issues": issues},
|
|
13963
|
+
"request_id": None,
|
|
13964
|
+
"suggested_next_call": None,
|
|
13965
|
+
"warnings": [],
|
|
13966
|
+
"verification": {"validated_entries": 1 if ok else 0},
|
|
13967
|
+
"ok": ok,
|
|
13968
|
+
"schemaVersion": context.schema_version,
|
|
13969
|
+
"apiVersion": context.api_version,
|
|
13970
|
+
"issues": issues,
|
|
13971
|
+
}
|
|
13972
|
+
|
|
13456
13973
|
def app_publish_verify(
|
|
13457
13974
|
self,
|
|
13458
13975
|
*,
|
|
@@ -15429,6 +15946,76 @@ class AiBuilderFacade:
|
|
|
15429
15946
|
result = legacy_normalized or normalized_views
|
|
15430
15947
|
return (result, False, None) if include_error else (result, False)
|
|
15431
15948
|
|
|
15949
|
+
def _set_view_pinned(
|
|
15950
|
+
self,
|
|
15951
|
+
*,
|
|
15952
|
+
profile: str,
|
|
15953
|
+
app_key: str,
|
|
15954
|
+
view_key: str,
|
|
15955
|
+
pinned: bool,
|
|
15956
|
+
) -> dict[str, Any]:
|
|
15957
|
+
"""Move one view between the application-level fixed and ordinary lists."""
|
|
15958
|
+
target_key = str(view_key or "").strip()
|
|
15959
|
+
if not target_key:
|
|
15960
|
+
raise ValueError("view_key is required to update the pinned state")
|
|
15961
|
+
response = self.views.view_list(profile=profile, app_key=app_key)
|
|
15962
|
+
raw_groups = response.get("result") if isinstance(response, dict) else None
|
|
15963
|
+
if not isinstance(raw_groups, list):
|
|
15964
|
+
raise ValueError("view list does not expose ordinal groups")
|
|
15965
|
+
|
|
15966
|
+
group_keys: dict[str, list[str]] = {
|
|
15967
|
+
"FIXED_VIEW_LIST": [],
|
|
15968
|
+
"VIEW_LIST": [],
|
|
15969
|
+
}
|
|
15970
|
+
for group in raw_groups:
|
|
15971
|
+
if not isinstance(group, dict):
|
|
15972
|
+
continue
|
|
15973
|
+
ordinal_type = str(group.get("ordinalType") or "").strip()
|
|
15974
|
+
if ordinal_type not in group_keys:
|
|
15975
|
+
continue
|
|
15976
|
+
view_list = group.get("viewList")
|
|
15977
|
+
if not isinstance(view_list, list):
|
|
15978
|
+
continue
|
|
15979
|
+
group_keys[ordinal_type] = [
|
|
15980
|
+
key
|
|
15981
|
+
for item in view_list
|
|
15982
|
+
if isinstance(item, dict)
|
|
15983
|
+
and (key := _extract_view_key(item))
|
|
15984
|
+
and key != target_key
|
|
15985
|
+
]
|
|
15986
|
+
|
|
15987
|
+
target_group = "FIXED_VIEW_LIST" if pinned else "VIEW_LIST"
|
|
15988
|
+
group_keys[target_group].append(target_key)
|
|
15989
|
+
self.views.view_reorder(
|
|
15990
|
+
profile=profile,
|
|
15991
|
+
app_key=app_key,
|
|
15992
|
+
payload=[
|
|
15993
|
+
{"ordinalType": "FIXED_VIEW_LIST", "viewKeyList": group_keys["FIXED_VIEW_LIST"]},
|
|
15994
|
+
{"ordinalType": "VIEW_LIST", "viewKeyList": group_keys["VIEW_LIST"]},
|
|
15995
|
+
],
|
|
15996
|
+
)
|
|
15997
|
+
|
|
15998
|
+
readback = self.views.view_list(profile=profile, app_key=app_key)
|
|
15999
|
+
readback_groups = readback.get("result") if isinstance(readback, dict) else None
|
|
16000
|
+
actual_group: str | None = None
|
|
16001
|
+
if isinstance(readback_groups, list):
|
|
16002
|
+
for group in readback_groups:
|
|
16003
|
+
if not isinstance(group, dict):
|
|
16004
|
+
continue
|
|
16005
|
+
ordinal_type = str(group.get("ordinalType") or "").strip()
|
|
16006
|
+
for item in group.get("viewList") or []:
|
|
16007
|
+
if isinstance(item, dict) and _extract_view_key(item) == target_key:
|
|
16008
|
+
actual_group = ordinal_type
|
|
16009
|
+
break
|
|
16010
|
+
if actual_group is not None:
|
|
16011
|
+
break
|
|
16012
|
+
return {
|
|
16013
|
+
"requested_pinned": pinned,
|
|
16014
|
+
"actual_pinned": actual_group == "FIXED_VIEW_LIST" if actual_group is not None else None,
|
|
16015
|
+
"verified": actual_group == target_group,
|
|
16016
|
+
"actual_ordinal_type": actual_group,
|
|
16017
|
+
}
|
|
16018
|
+
|
|
15432
16019
|
def _load_workflow_result(
|
|
15433
16020
|
self,
|
|
15434
16021
|
*,
|
|
@@ -19226,40 +19813,44 @@ def _chart_apply_batching_context(
|
|
|
19226
19813
|
def _view_apply_failure_diagnostics(
|
|
19227
19814
|
*,
|
|
19228
19815
|
patch: ViewUpsertPatch,
|
|
19229
|
-
|
|
19816
|
+
fields_by_que_id: dict[int, dict[str, Any]],
|
|
19230
19817
|
backend_code: Any,
|
|
19231
19818
|
operation_phase: str,
|
|
19232
19819
|
) -> dict[str, Any]:
|
|
19233
|
-
references: list[tuple[
|
|
19820
|
+
references: list[tuple[int, str]] = []
|
|
19234
19821
|
|
|
19235
|
-
def add_reference(
|
|
19236
|
-
|
|
19237
|
-
if
|
|
19238
|
-
references.append((
|
|
19822
|
+
def add_reference(value: Any, source: str) -> None:
|
|
19823
|
+
que_id = _coerce_positive_int(value)
|
|
19824
|
+
if que_id is not None:
|
|
19825
|
+
references.append((que_id, source))
|
|
19239
19826
|
|
|
19240
19827
|
for column in patch.columns:
|
|
19241
19828
|
add_reference(column, "columns")
|
|
19242
|
-
add_reference(patch.
|
|
19243
|
-
add_reference(patch.
|
|
19244
|
-
add_reference(patch.
|
|
19245
|
-
add_reference(patch.
|
|
19246
|
-
for
|
|
19247
|
-
|
|
19829
|
+
add_reference(patch.group_que_id, "group_que_id")
|
|
19830
|
+
add_reference(patch.start_que_id, "start_que_id")
|
|
19831
|
+
add_reference(patch.end_que_id, "end_que_id")
|
|
19832
|
+
add_reference(patch.title_que_id, "title_que_id")
|
|
19833
|
+
for item in patch.filters:
|
|
19834
|
+
if isinstance(item, list):
|
|
19835
|
+
for rule in item:
|
|
19836
|
+
add_reference(getattr(rule, "que_id", None), "filters")
|
|
19837
|
+
else:
|
|
19838
|
+
add_reference(getattr(item, "que_id", None), "filters")
|
|
19248
19839
|
|
|
19249
|
-
seen: set[str] = set()
|
|
19840
|
+
seen: set[tuple[int, str]] = set()
|
|
19250
19841
|
field_entries: list[dict[str, Any]] = []
|
|
19251
|
-
for
|
|
19252
|
-
key =
|
|
19842
|
+
for que_id, source in references:
|
|
19843
|
+
key = (que_id, source)
|
|
19253
19844
|
if key in seen:
|
|
19254
19845
|
continue
|
|
19255
19846
|
seen.add(key)
|
|
19256
|
-
field =
|
|
19847
|
+
field = fields_by_que_id.get(que_id) or {}
|
|
19257
19848
|
field_entries.append(
|
|
19258
19849
|
{
|
|
19259
|
-
"field_name":
|
|
19850
|
+
"field_name": str(field.get("name") or que_id),
|
|
19260
19851
|
"source": source,
|
|
19261
19852
|
"field_id": field.get("field_id"),
|
|
19262
|
-
"que_id": field.get("que_id"),
|
|
19853
|
+
"que_id": field.get("que_id") or que_id,
|
|
19263
19854
|
"field_type": field.get("type"),
|
|
19264
19855
|
"que_type": field.get("que_type"),
|
|
19265
19856
|
"exists_in_schema": bool(field),
|
|
@@ -19277,7 +19868,7 @@ def _view_apply_failure_diagnostics(
|
|
|
19277
19868
|
"recommended_minimal_retry": {
|
|
19278
19869
|
"name": patch.name,
|
|
19279
19870
|
"type": patch.type.value,
|
|
19280
|
-
"columns": [field_entries[0]["
|
|
19871
|
+
"columns": [field_entries[0]["que_id"]] if field_entries else [],
|
|
19281
19872
|
**({"filters": [rule.model_dump(mode="json", exclude_none=True) for rule in patch.filters]} if patch.filters else {}),
|
|
19282
19873
|
},
|
|
19283
19874
|
}
|
|
@@ -20356,6 +20947,8 @@ def _normalize_portal_view_type(view: dict[str, Any]) -> str:
|
|
|
20356
20947
|
"ganttView": "ganttView",
|
|
20357
20948
|
"hierarchy": "hierarchyView",
|
|
20358
20949
|
"hierarchyView": "hierarchyView",
|
|
20950
|
+
"calendar": "calendarView",
|
|
20951
|
+
"calendarView": "calendarView",
|
|
20359
20952
|
}
|
|
20360
20953
|
return mapping.get(raw, "tableView")
|
|
20361
20954
|
|
|
@@ -21186,8 +21779,10 @@ def _infer_field_type(question: dict[str, Any]) -> str:
|
|
|
21186
21779
|
|
|
21187
21780
|
|
|
21188
21781
|
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
21782
|
que_id = _coerce_positive_int(question.get("queId"))
|
|
21783
|
+
# 无标题字段用「字段{que_id}」兜底命名,与提取器(eval_replay/views get)对称,
|
|
21784
|
+
# 否则视图列引用无标题字段时 UNKNOWN_VIEW_FIELD
|
|
21785
|
+
name = str(question.get("queTitle") or "").strip() or (f"字段{que_id}" if que_id else "")
|
|
21191
21786
|
que_type = _coerce_positive_int(question.get("queType"))
|
|
21192
21787
|
field_type = _infer_field_type(question)
|
|
21193
21788
|
field_id = field_id_hint or f"field_{que_id or _slugify(name, default='x')}"
|
|
@@ -21357,6 +21952,17 @@ def _parse_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
|
|
21357
21952
|
for question in inner_row or []:
|
|
21358
21953
|
if not isinstance(question, dict):
|
|
21359
21954
|
continue
|
|
21955
|
+
if _coerce_positive_int(question.get("queType")) == 24:
|
|
21956
|
+
# 嵌套段落:不注册段落本身,递归解析其 innerQuestions
|
|
21957
|
+
for nested_row in question.get("innerQuestions", []) or []:
|
|
21958
|
+
for nested_question in (nested_row if isinstance(nested_row, list) else [nested_row]):
|
|
21959
|
+
if not isinstance(nested_question, dict):
|
|
21960
|
+
continue
|
|
21961
|
+
nested_field = _parse_field(nested_question)
|
|
21962
|
+
if str(nested_field["name"] or "").strip():
|
|
21963
|
+
fields_by_name.setdefault(nested_field["name"], nested_field)
|
|
21964
|
+
labels.append(nested_field["name"])
|
|
21965
|
+
continue
|
|
21360
21966
|
field = _parse_field(question)
|
|
21361
21967
|
fields_by_name.setdefault(field["name"], field)
|
|
21362
21968
|
labels.append(field["name"])
|
|
@@ -21382,8 +21988,16 @@ def _parse_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
|
|
21382
21988
|
for question in row:
|
|
21383
21989
|
if not isinstance(question, dict):
|
|
21384
21990
|
continue
|
|
21991
|
+
if _coerce_positive_int(question.get("queType")) == 24:
|
|
21992
|
+
# 同行混入段落容器:不注册段落本身(它的子字段在自己 row),跳过避免同名占位挡住真实字段
|
|
21993
|
+
continue
|
|
21385
21994
|
field = _parse_field(question)
|
|
21386
|
-
fields_by_name.
|
|
21995
|
+
existing = fields_by_name.get(field["name"])
|
|
21996
|
+
# 同名冲突:保留信息更丰富的那个(表格字段带 subfields,被同名普通字段覆盖会丢子字段引用面);
|
|
21997
|
+
# 但 queId 索引必须收录全部同名字段(_collect_fields_by_que_id 按 queId 消费,同名不同 id 各自有效)
|
|
21998
|
+
if existing is None or (not existing.get("subfields") and field.get("subfields")):
|
|
21999
|
+
fields_by_name[field["name"]] = field
|
|
22000
|
+
|
|
21387
22001
|
labels.append(field["name"])
|
|
21388
22002
|
if labels:
|
|
21389
22003
|
root_rows.append(labels)
|
|
@@ -26066,7 +26680,11 @@ def _merge_view_summary_with_config(
|
|
|
26066
26680
|
return summary
|
|
26067
26681
|
if isinstance(config.get("auth"), dict):
|
|
26068
26682
|
summary["visibility_summary"] = _visibility_summary(_public_visibility_from_member_auth(config.get("auth")))
|
|
26069
|
-
legacy_columns = [
|
|
26683
|
+
legacy_columns = [
|
|
26684
|
+
_SYSTEM_VIEW_QUE_NAMES.get(value if isinstance(value, int) else None, str(value))
|
|
26685
|
+
for value in (summary.get("columns") or [])
|
|
26686
|
+
if str(value or "").strip()
|
|
26687
|
+
]
|
|
26070
26688
|
question_entries = _extract_view_question_entries(config.get("viewgraphQuestions"))
|
|
26071
26689
|
canonical_question_entries = _extract_view_question_entries(question_list)
|
|
26072
26690
|
question_entries_by_id = {
|
|
@@ -26167,7 +26785,7 @@ def _merge_view_summary_with_config(
|
|
|
26167
26785
|
and str(entry.get("name") or "").strip()
|
|
26168
26786
|
and str(entry.get("name") or "").strip() not in _KNOWN_SYSTEM_VIEW_COLUMNS
|
|
26169
26787
|
]
|
|
26170
|
-
summary["columns"] =
|
|
26788
|
+
summary["columns"] = canonical_display_column_ids
|
|
26171
26789
|
summary["display_columns"] = canonical_display_columns
|
|
26172
26790
|
summary["display_column_ids"] = canonical_display_column_ids
|
|
26173
26791
|
summary["column_details"] = canonical_display_entries
|
|
@@ -26212,7 +26830,7 @@ def _merge_view_summary_with_config(
|
|
|
26212
26830
|
)
|
|
26213
26831
|
summary["filters"] = _public_view_filter_groups_from_match_rules(
|
|
26214
26832
|
config.get("viewgraphLimit"),
|
|
26215
|
-
|
|
26833
|
+
fields_by_que_id=field_lookup,
|
|
26216
26834
|
)
|
|
26217
26835
|
config_enriched = True
|
|
26218
26836
|
if any(key in config for key in ("asosChartVisible", "asosChartConfig", "asosChartIdList", "limitType")):
|
|
@@ -26455,12 +27073,16 @@ def _normalize_print_tpls(value: Any) -> list[dict[str, Any]]:
|
|
|
26455
27073
|
items: list[dict[str, Any]] = []
|
|
26456
27074
|
for item in value:
|
|
26457
27075
|
if isinstance(item, dict):
|
|
26458
|
-
tpl_id = item.get("
|
|
27076
|
+
tpl_id = item.get("printKey")
|
|
27077
|
+
if tpl_id is None:
|
|
27078
|
+
tpl_id = item.get("printTplId")
|
|
26459
27079
|
if tpl_id is None:
|
|
26460
27080
|
tpl_id = item.get("templateId")
|
|
26461
27081
|
if tpl_id is None:
|
|
26462
27082
|
tpl_id = item.get("id")
|
|
26463
|
-
tpl_name = item.get("
|
|
27083
|
+
tpl_name = item.get("printName")
|
|
27084
|
+
if tpl_name is None:
|
|
27085
|
+
tpl_name = item.get("printTplName")
|
|
26464
27086
|
if tpl_name is None:
|
|
26465
27087
|
tpl_name = item.get("templateName")
|
|
26466
27088
|
if tpl_name is None:
|
|
@@ -26904,7 +27526,7 @@ def _build_grouped_view_button_config(button_config_dtos: list[dict[str, Any]])
|
|
|
26904
27526
|
|
|
26905
27527
|
def _build_view_button_dtos(
|
|
26906
27528
|
*,
|
|
26907
|
-
|
|
27529
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
26908
27530
|
bindings: list[ViewButtonBindingPatch],
|
|
26909
27531
|
valid_custom_button_ids: set[int],
|
|
26910
27532
|
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
@@ -26913,7 +27535,7 @@ def _build_view_button_dtos(
|
|
|
26913
27535
|
for binding in bindings:
|
|
26914
27536
|
dto, binding_issues = _serialize_view_button_binding(
|
|
26915
27537
|
binding=binding,
|
|
26916
|
-
|
|
27538
|
+
fields_by_que_id=fields_by_que_id,
|
|
26917
27539
|
valid_custom_button_ids=valid_custom_button_ids,
|
|
26918
27540
|
)
|
|
26919
27541
|
if binding_issues:
|
|
@@ -26926,7 +27548,7 @@ def _build_view_button_dtos(
|
|
|
26926
27548
|
def _serialize_view_button_binding(
|
|
26927
27549
|
*,
|
|
26928
27550
|
binding: ViewButtonBindingPatch,
|
|
26929
|
-
|
|
27551
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
26930
27552
|
valid_custom_button_ids: set[int],
|
|
26931
27553
|
allow_unverified_custom_button_id: bool = False,
|
|
26932
27554
|
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
@@ -26944,7 +27566,7 @@ def _serialize_view_button_binding(
|
|
|
26944
27566
|
}
|
|
26945
27567
|
]
|
|
26946
27568
|
translated_limits, limit_issues = _build_view_button_limit_groups(
|
|
26947
|
-
|
|
27569
|
+
fields_by_que_id=fields_by_que_id,
|
|
26948
27570
|
groups=binding.button_limit,
|
|
26949
27571
|
)
|
|
26950
27572
|
if limit_issues:
|
|
@@ -26970,7 +27592,7 @@ def _serialize_view_button_binding(
|
|
|
26970
27592
|
|
|
26971
27593
|
def _build_view_button_limit_groups(
|
|
26972
27594
|
*,
|
|
26973
|
-
|
|
27595
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
26974
27596
|
groups: list[list[ViewFilterRulePatch]],
|
|
26975
27597
|
) -> tuple[list[list[dict[str, Any]]], list[dict[str, Any]]]:
|
|
26976
27598
|
translated_groups: list[list[dict[str, Any]]] = []
|
|
@@ -26984,14 +27606,14 @@ def _build_view_button_limit_groups(
|
|
|
26984
27606
|
raw_rule = raw_rule.model_dump(mode="json")
|
|
26985
27607
|
if not isinstance(raw_rule, dict):
|
|
26986
27608
|
continue
|
|
26987
|
-
|
|
26988
|
-
field =
|
|
27609
|
+
rule_que_id = _coerce_positive_int(raw_rule.get("que_id"))
|
|
27610
|
+
field = fields_by_que_id.get(rule_que_id) if rule_que_id is not None else None
|
|
26989
27611
|
if field is None:
|
|
26990
27612
|
issues.append(
|
|
26991
27613
|
{
|
|
26992
27614
|
"error_code": "UNKNOWN_VIEW_FIELD",
|
|
26993
|
-
"missing_fields": [
|
|
26994
|
-
"reason_path": "buttons[].button_limit[].
|
|
27615
|
+
"missing_fields": [str(raw_rule.get("que_id") or "")],
|
|
27616
|
+
"reason_path": "buttons[].button_limit[].que_id",
|
|
26995
27617
|
}
|
|
26996
27618
|
)
|
|
26997
27619
|
continue
|
|
@@ -27162,7 +27784,7 @@ def _custom_button_view_configs_from_view_summaries(views: list[dict[str, Any]])
|
|
|
27162
27784
|
placement = _public_view_button_placement(entry.get("config_type")) or entry.get("placement")
|
|
27163
27785
|
raw_button_limit = deepcopy(entry.get("button_limit") or [])
|
|
27164
27786
|
public_button_limit = (
|
|
27165
|
-
_public_view_filter_groups_from_match_rules(raw_button_limit,
|
|
27787
|
+
_public_view_filter_groups_from_match_rules(raw_button_limit, fields_by_que_id=field_lookup)
|
|
27166
27788
|
if raw_button_limit
|
|
27167
27789
|
else []
|
|
27168
27790
|
)
|
|
@@ -27360,6 +27982,11 @@ def _field_id_for_name(fields: list[dict[str, Any]], name: str) -> str:
|
|
|
27360
27982
|
for field in fields:
|
|
27361
27983
|
if field["name"] == name:
|
|
27362
27984
|
return field["field_id"]
|
|
27985
|
+
# 表格子字段是视图列/筛选的合法引用面:顶层未命中时递归 subfields
|
|
27986
|
+
for field in fields:
|
|
27987
|
+
for sub in field.get("subfields") or []:
|
|
27988
|
+
if isinstance(sub, dict) and sub.get("name") == name:
|
|
27989
|
+
return str(sub.get("field_id") or f"field_{sub.get('que_id') or ''}")
|
|
27363
27990
|
raise KeyError(name)
|
|
27364
27991
|
|
|
27365
27992
|
|
|
@@ -27459,7 +28086,7 @@ def _build_public_workflow_spec(*, nodes: list[dict[str, Any]], transitions: lis
|
|
|
27459
28086
|
|
|
27460
28087
|
def _build_flow_condition_matrix(
|
|
27461
28088
|
*,
|
|
27462
|
-
|
|
28089
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
27463
28090
|
node: dict[str, Any],
|
|
27464
28091
|
) -> tuple[list[list[dict[str, Any]]], list[dict[str, Any]]]:
|
|
27465
28092
|
if str(node.get("type") or "") != "condition":
|
|
@@ -27477,7 +28104,7 @@ def _build_flow_condition_matrix(
|
|
|
27477
28104
|
if not isinstance(raw_rule, dict):
|
|
27478
28105
|
continue
|
|
27479
28106
|
field_name = str(raw_rule.get("field_name") or "").strip()
|
|
27480
|
-
field =
|
|
28107
|
+
field = fields_by_que_id.get(field_name)
|
|
27481
28108
|
if field is None:
|
|
27482
28109
|
issues.append(
|
|
27483
28110
|
{
|
|
@@ -27524,7 +28151,9 @@ def _translate_flow_condition_rule(*, field: dict[str, Any], rule: dict[str, Any
|
|
|
27524
28151
|
base["judgeType"] = JUDGE_LESS_OR_EQUAL
|
|
27525
28152
|
base["judgeValues"] = [_stringify_condition_value(values[0])]
|
|
27526
28153
|
elif operator == "is_empty":
|
|
27527
|
-
|
|
28154
|
+
# 选项/成员/部门类字段的空值,后端历史形态是 EQUAL_ANY(9)+空值(该类字段用
|
|
28155
|
+
# ANY 语义选值,"没选任何值"也是 9 判型);文本/数字类才是 EQUAL(0)+空值
|
|
28156
|
+
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
28157
|
base["judgeValues"] = []
|
|
27529
28158
|
elif operator == "not_empty":
|
|
27530
28159
|
base["judgeType"] = JUDGE_UNEQUAL
|
|
@@ -27553,7 +28182,7 @@ def _empty_view_query_conditions_payload() -> dict[str, Any]:
|
|
|
27553
28182
|
|
|
27554
28183
|
def _build_view_query_conditions_payload(
|
|
27555
28184
|
*,
|
|
27556
|
-
|
|
28185
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
27557
28186
|
query_conditions: Any,
|
|
27558
28187
|
) -> tuple[dict[str, Any] | None, dict[str, Any] | None, list[dict[str, Any]]]:
|
|
27559
28188
|
if query_conditions is None:
|
|
@@ -27593,7 +28222,7 @@ def _build_view_query_conditions_payload(
|
|
|
27593
28222
|
compiled_row: list[int] = []
|
|
27594
28223
|
for column_index, selector in enumerate(row):
|
|
27595
28224
|
field, issue = _resolve_query_condition_field(
|
|
27596
|
-
|
|
28225
|
+
fields_by_que_id=fields_by_que_id,
|
|
27597
28226
|
selector=selector,
|
|
27598
28227
|
reason_path=f"query_conditions.rows[{row_index}][{column_index}]",
|
|
27599
28228
|
)
|
|
@@ -27611,14 +28240,16 @@ def _build_view_query_conditions_payload(
|
|
|
27611
28240
|
"queryConditionStatus": enabled,
|
|
27612
28241
|
"queryConditionExact": exact,
|
|
27613
28242
|
"hideBeforeQueryCondition": hide_before_query,
|
|
27614
|
-
|
|
28243
|
+
# 后端 ViewgraphInfo.queryCondition 是 List<ViewgraphQueryConditionConfig>({title?,queIds}),
|
|
28244
|
+
# 不能发裸 [[queId]](fastjson expect {, actual [ → 发现 #7)
|
|
28245
|
+
"queryCondition": [{"queIds": row} for row in rows] if enabled else [],
|
|
27615
28246
|
}
|
|
27616
28247
|
return payload, _normalize_view_query_conditions_for_compare(payload), []
|
|
27617
28248
|
|
|
27618
28249
|
|
|
27619
28250
|
def _resolve_query_condition_field(
|
|
27620
28251
|
*,
|
|
27621
|
-
|
|
28252
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
27622
28253
|
selector: Any,
|
|
27623
28254
|
reason_path: str,
|
|
27624
28255
|
) -> tuple[dict[str, Any], dict[str, Any] | None]:
|
|
@@ -27630,8 +28261,8 @@ def _resolve_query_condition_field(
|
|
|
27630
28261
|
"missing_fields": [],
|
|
27631
28262
|
"message": "query condition field selector cannot be empty",
|
|
27632
28263
|
}
|
|
27633
|
-
fields = [field for field in
|
|
27634
|
-
field =
|
|
28264
|
+
fields = [field for field in fields_by_que_id.values() if isinstance(field, dict)]
|
|
28265
|
+
field = fields_by_que_id.get(raw)
|
|
27635
28266
|
if field is None:
|
|
27636
28267
|
for candidate in fields:
|
|
27637
28268
|
if raw == str(candidate.get("field_id") or "").strip():
|
|
@@ -27642,7 +28273,7 @@ def _resolve_query_condition_field(
|
|
|
27642
28273
|
field = candidate
|
|
27643
28274
|
break
|
|
27644
28275
|
if field is None:
|
|
27645
|
-
subfield_parent = _find_subtable_subfield_parent(
|
|
28276
|
+
subfield_parent = _find_subtable_subfield_parent(fields_by_que_id=fields_by_que_id, selector=raw)
|
|
27646
28277
|
if subfield_parent is not None:
|
|
27647
28278
|
return {}, {
|
|
27648
28279
|
"error_code": "INVALID_QUERY_CONDITION_FIELD",
|
|
@@ -27690,10 +28321,10 @@ def _resolve_query_condition_field(
|
|
|
27690
28321
|
|
|
27691
28322
|
def _find_subtable_subfield_parent(
|
|
27692
28323
|
*,
|
|
27693
|
-
|
|
28324
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
27694
28325
|
selector: str,
|
|
27695
28326
|
) -> dict[str, Any] | None:
|
|
27696
|
-
for field in
|
|
28327
|
+
for field in fields_by_que_id.values():
|
|
27697
28328
|
if not isinstance(field, dict) or str(field.get("type") or "") != FieldType.subtable.value:
|
|
27698
28329
|
continue
|
|
27699
28330
|
for subfield in field.get("subfields") or []:
|
|
@@ -27754,6 +28385,9 @@ def _normalize_view_query_conditions_for_compare(config: Any) -> dict[str, Any]:
|
|
|
27754
28385
|
rows: list[list[int]] = []
|
|
27755
28386
|
if isinstance(raw_rows, list):
|
|
27756
28387
|
for raw_row in raw_rows:
|
|
28388
|
+
if isinstance(raw_row, dict):
|
|
28389
|
+
# ViewgraphQueryConditionConfig 形态({title?,queIds})
|
|
28390
|
+
raw_row = raw_row.get("queIds") or []
|
|
27757
28391
|
if not isinstance(raw_row, list):
|
|
27758
28392
|
continue
|
|
27759
28393
|
compiled_row = [
|
|
@@ -27816,11 +28450,18 @@ def _build_view_associated_resources_payload(
|
|
|
27816
28450
|
]
|
|
27817
28451
|
available_by_id = _associated_resource_index(available_resources)
|
|
27818
28452
|
if limit_type == "all":
|
|
28453
|
+
# limitType=ALL 时后端保留原 idList 不清(语义忽略但存储保留);
|
|
28454
|
+
# 透传声明的 id 列表,避免 apply 清空→回读 idList 不匹配→READBACK_MISMATCH
|
|
28455
|
+
all_ids = [
|
|
28456
|
+
item_id
|
|
28457
|
+
for item_id in (_coerce_positive_int(raw) for raw in raw_item_ids)
|
|
28458
|
+
if item_id is not None
|
|
28459
|
+
]
|
|
27819
28460
|
payload = {
|
|
27820
28461
|
"asosChartVisible": True,
|
|
27821
28462
|
"asosChartConfig": {
|
|
27822
28463
|
"limitType": ASSOCIATED_RESOURCE_LIMIT_TYPE_ALL,
|
|
27823
|
-
"asosChartIdList":
|
|
28464
|
+
"asosChartIdList": all_ids,
|
|
27824
28465
|
},
|
|
27825
28466
|
}
|
|
27826
28467
|
expected = _extract_view_associated_resources_config(payload, available_resources=available_resources)
|
|
@@ -27841,26 +28482,19 @@ def _build_view_associated_resources_payload(
|
|
|
27841
28482
|
]
|
|
27842
28483
|
item_ids.append(item_id)
|
|
27843
28484
|
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
|
-
]
|
|
28485
|
+
# 兼容存量脏引用:关联资源 id 指向闭包外/已下架的图表或视图时,剔除后继续写而非整视图失败;
|
|
28486
|
+
# 剔除明细记入 expected 供回读校验与响应展示
|
|
28487
|
+
resolvable_ids = [item_id for item_id in item_ids if item_id in available_by_id]
|
|
27856
28488
|
payload = {
|
|
27857
28489
|
"asosChartVisible": True,
|
|
27858
28490
|
"asosChartConfig": {
|
|
27859
28491
|
"limitType": ASSOCIATED_RESOURCE_LIMIT_TYPE_SELECT,
|
|
27860
|
-
"asosChartIdList":
|
|
28492
|
+
"asosChartIdList": resolvable_ids,
|
|
27861
28493
|
},
|
|
27862
28494
|
}
|
|
27863
28495
|
expected = _extract_view_associated_resources_config(payload, available_resources=available_resources)
|
|
28496
|
+
if isinstance(expected, dict) and missing_ids:
|
|
28497
|
+
expected["dropped_unresolvable_associated_item_ids"] = missing_ids
|
|
27864
28498
|
return payload, expected, []
|
|
27865
28499
|
|
|
27866
28500
|
|
|
@@ -28597,6 +29231,10 @@ _VIEW_PARTIAL_PATCH_KEY_ALIASES = {
|
|
|
28597
29231
|
"view_key": "view_key",
|
|
28598
29232
|
"viewKey": "view_key",
|
|
28599
29233
|
"viewgraphKey": "view_key",
|
|
29234
|
+
"pinned": "pinned",
|
|
29235
|
+
"fixed": "pinned",
|
|
29236
|
+
"is_pinned": "pinned",
|
|
29237
|
+
"isPinned": "pinned",
|
|
28600
29238
|
"type": "type",
|
|
28601
29239
|
"view_type": "type",
|
|
28602
29240
|
"viewType": "type",
|
|
@@ -28604,17 +29242,25 @@ _VIEW_PARTIAL_PATCH_KEY_ALIASES = {
|
|
|
28604
29242
|
"column_names": "columns",
|
|
28605
29243
|
"columnNames": "columns",
|
|
28606
29244
|
"fields": "columns",
|
|
28607
|
-
"
|
|
28608
|
-
"
|
|
29245
|
+
"group_que_id": "group_que_id",
|
|
29246
|
+
"groupQueId": "group_que_id",
|
|
29247
|
+
"group_by": "group_que_id",
|
|
29248
|
+
"groupBy": "group_que_id",
|
|
28609
29249
|
"filters": "filters",
|
|
28610
29250
|
"filter_rules": "filters",
|
|
28611
29251
|
"filterRules": "filters",
|
|
28612
|
-
"
|
|
28613
|
-
"
|
|
28614
|
-
"
|
|
28615
|
-
"
|
|
28616
|
-
"
|
|
28617
|
-
"
|
|
29252
|
+
"start_que_id": "start_que_id",
|
|
29253
|
+
"startQueId": "start_que_id",
|
|
29254
|
+
"start_field": "start_que_id",
|
|
29255
|
+
"startField": "start_que_id",
|
|
29256
|
+
"end_que_id": "end_que_id",
|
|
29257
|
+
"endQueId": "end_que_id",
|
|
29258
|
+
"end_field": "end_que_id",
|
|
29259
|
+
"endField": "end_que_id",
|
|
29260
|
+
"title_que_id": "title_que_id",
|
|
29261
|
+
"titleQueId": "title_que_id",
|
|
29262
|
+
"title_field": "title_que_id",
|
|
29263
|
+
"titleField": "title_que_id",
|
|
28618
29264
|
"buttons": "buttons",
|
|
28619
29265
|
"visibility": "visibility",
|
|
28620
29266
|
"query_conditions": "query_conditions",
|
|
@@ -28701,13 +29347,14 @@ def _normalize_view_partial_unset(raw_unset: Any, *, reason_path: str) -> tuple[
|
|
|
28701
29347
|
|
|
28702
29348
|
_VIEW_PARTIAL_SET_KEYS = {
|
|
28703
29349
|
"name",
|
|
29350
|
+
"pinned",
|
|
28704
29351
|
"type",
|
|
28705
29352
|
"columns",
|
|
28706
|
-
"
|
|
29353
|
+
"group_que_id",
|
|
28707
29354
|
"filters",
|
|
28708
|
-
"
|
|
28709
|
-
"
|
|
28710
|
-
"
|
|
29355
|
+
"start_que_id",
|
|
29356
|
+
"end_que_id",
|
|
29357
|
+
"title_que_id",
|
|
28711
29358
|
"buttons",
|
|
28712
29359
|
"visibility",
|
|
28713
29360
|
"query_conditions",
|
|
@@ -28733,6 +29380,108 @@ def _field_name_by_id(field_names_by_id: dict[int, str], field_id: Any) -> str |
|
|
|
28733
29380
|
return name or None
|
|
28734
29381
|
|
|
28735
29382
|
|
|
29383
|
+
def _collect_fields_by_que_id(fields: list[dict[str, Any]], *, schema: dict[str, Any] | None = None) -> dict[int, dict[str, Any]]:
|
|
29384
|
+
"""构建 queId→field 索引。
|
|
29385
|
+
|
|
29386
|
+
传 schema 时直接从原始 formQues/baseQues 递归收集(同名冲突不丢字段——名字索引
|
|
29387
|
+
只保留第一个,但同名不同 queId 的字段对 queId 引用各自有效);
|
|
29388
|
+
不传 schema 时从解析后的 fields 列表递归 subfields 构建。
|
|
29389
|
+
"""
|
|
29390
|
+
index: dict[int, dict[str, Any]] = {}
|
|
29391
|
+
|
|
29392
|
+
type_by_question_type = {v: k for k, v in QUESTION_TYPE_TO_FIELD_TYPE.items()}
|
|
29393
|
+
|
|
29394
|
+
def collect_raw(question: dict[str, Any]) -> None:
|
|
29395
|
+
que_id = _coerce_positive_int(question.get("queId"))
|
|
29396
|
+
if que_id is not None and que_id not in index:
|
|
29397
|
+
que_type = _coerce_positive_int(question.get("queType"))
|
|
29398
|
+
# 用 _parse_field 解析(含 options/option_details,与 _parse_schema 对齐);
|
|
29399
|
+
# 直收原始 dict 的 queTitle/queType 不够——选项字段需 options 才能解析值→id
|
|
29400
|
+
parsed = _parse_field(question)
|
|
29401
|
+
index[que_id] = parsed
|
|
29402
|
+
for key in ("subQuestions", "innerQuestions"):
|
|
29403
|
+
for child in question.get(key) or []:
|
|
29404
|
+
if isinstance(child, list):
|
|
29405
|
+
for inner in child:
|
|
29406
|
+
if isinstance(inner, dict):
|
|
29407
|
+
collect_raw(inner)
|
|
29408
|
+
elif isinstance(child, dict):
|
|
29409
|
+
collect_raw(child)
|
|
29410
|
+
|
|
29411
|
+
if schema is not None:
|
|
29412
|
+
for base_question in schema.get("baseQues", []) or []:
|
|
29413
|
+
if isinstance(base_question, dict):
|
|
29414
|
+
collect_raw(base_question)
|
|
29415
|
+
for line in schema.get("formQues", []) or []:
|
|
29416
|
+
for question in (line if isinstance(line, list) else [line]):
|
|
29417
|
+
if isinstance(question, dict):
|
|
29418
|
+
collect_raw(question)
|
|
29419
|
+
return index
|
|
29420
|
+
|
|
29421
|
+
def collect(field: dict[str, Any]) -> None:
|
|
29422
|
+
que_id = _coerce_positive_int(field.get("que_id"))
|
|
29423
|
+
if que_id is not None and que_id not in index:
|
|
29424
|
+
index[que_id] = field
|
|
29425
|
+
for sub in field.get("subfields") or []:
|
|
29426
|
+
if isinstance(sub, dict):
|
|
29427
|
+
collect(sub)
|
|
29428
|
+
|
|
29429
|
+
for field in fields:
|
|
29430
|
+
if isinstance(field, dict):
|
|
29431
|
+
collect(field)
|
|
29432
|
+
return index
|
|
29433
|
+
|
|
29434
|
+
|
|
29435
|
+
def _collect_field_id_to_name(schema: dict[str, Any]) -> dict[int, str]:
|
|
29436
|
+
"""从 form schema 收集 queId→queTitle(含子字段/段落内字段;同名时各 id 独立映射)。"""
|
|
29437
|
+
id_to_name: dict[int, str] = {}
|
|
29438
|
+
|
|
29439
|
+
def collect(question: dict[str, Any]) -> None:
|
|
29440
|
+
que_id = _coerce_positive_int(question.get("queId"))
|
|
29441
|
+
if que_id is not None and que_id not in id_to_name:
|
|
29442
|
+
title = str(question.get("queTitle") or "").strip()
|
|
29443
|
+
if title:
|
|
29444
|
+
id_to_name[que_id] = title
|
|
29445
|
+
for key in ("subQuestions", "innerQuestions"):
|
|
29446
|
+
for child in question.get(key) or []:
|
|
29447
|
+
if isinstance(child, list):
|
|
29448
|
+
for inner in child:
|
|
29449
|
+
if isinstance(inner, dict):
|
|
29450
|
+
collect(inner)
|
|
29451
|
+
elif isinstance(child, dict):
|
|
29452
|
+
collect(child)
|
|
29453
|
+
|
|
29454
|
+
for base_question in schema.get("baseQues", []) or []:
|
|
29455
|
+
if isinstance(base_question, dict):
|
|
29456
|
+
collect(base_question)
|
|
29457
|
+
for line in schema.get("formQues", []) or []:
|
|
29458
|
+
for question in (line if isinstance(line, list) else [line]):
|
|
29459
|
+
if isinstance(question, dict):
|
|
29460
|
+
collect(question)
|
|
29461
|
+
return id_to_name
|
|
29462
|
+
|
|
29463
|
+
|
|
29464
|
+
def _resolve_field_ref_preserving(
|
|
29465
|
+
config: dict[str, Any],
|
|
29466
|
+
config_key: str,
|
|
29467
|
+
name: str | None,
|
|
29468
|
+
field_map: dict[str, int],
|
|
29469
|
+
id_to_name: dict[int, str],
|
|
29470
|
+
) -> int | None:
|
|
29471
|
+
"""同名字段歧义消解(字段引用类:card title/cover、board group 等)。
|
|
29472
|
+
|
|
29473
|
+
契约按字段名引用,同 form 内同名字段时按名解析会取到错误的一个。
|
|
29474
|
+
UPDATE 场景读面原 queId 可用:若原 queId 的字段名与声明名一致,保留原 queId(保真);
|
|
29475
|
+
只有声明名与原值名字不同(用户真改了字段)才按名重新解析。
|
|
29476
|
+
"""
|
|
29477
|
+
if not name:
|
|
29478
|
+
return None
|
|
29479
|
+
original = _coerce_positive_int(config.get(config_key))
|
|
29480
|
+
if original is not None and id_to_name.get(original) == name:
|
|
29481
|
+
return original
|
|
29482
|
+
return field_map.get(name)
|
|
29483
|
+
|
|
29484
|
+
|
|
28736
29485
|
def _view_upsert_payload_from_existing_view(
|
|
28737
29486
|
*,
|
|
28738
29487
|
config: dict[str, Any],
|
|
@@ -28742,25 +29491,28 @@ def _view_upsert_payload_from_existing_view(
|
|
|
28742
29491
|
) -> dict[str, Any]:
|
|
28743
29492
|
view_type = _normalize_view_type_name(config.get("viewgraphType") or summary.get("type"))
|
|
28744
29493
|
configured_columns = [
|
|
28745
|
-
|
|
28746
|
-
for
|
|
28747
|
-
if
|
|
29494
|
+
que_id
|
|
29495
|
+
for item in (config.get("viewgraphQueIds") or [])
|
|
29496
|
+
if (que_id := _coerce_positive_int(item)) is not None
|
|
29497
|
+
and (field_names_by_id.get(que_id) or "") not in _KNOWN_SYSTEM_VIEW_COLUMNS
|
|
28748
29498
|
]
|
|
28749
29499
|
columns = [
|
|
28750
|
-
|
|
28751
|
-
for
|
|
28752
|
-
if
|
|
29500
|
+
que_id
|
|
29501
|
+
for item in (summary.get("apply_column_ids") or summary.get("columns") or [])
|
|
29502
|
+
if (que_id := _coerce_positive_int(item)) is not None
|
|
29503
|
+
and (field_names_by_id.get(que_id) or "") not in _KNOWN_SYSTEM_VIEW_COLUMNS
|
|
28753
29504
|
]
|
|
28754
29505
|
if configured_columns and (not columns or len(configured_columns) >= len(columns)):
|
|
28755
29506
|
columns = configured_columns
|
|
28756
29507
|
display_config = summary.get("display_config") if isinstance(summary.get("display_config"), dict) else {}
|
|
28757
|
-
|
|
28758
|
-
|
|
29508
|
+
group_que_id = _coerce_positive_int(config.get("groupQueId") or summary.get("group_que_id") or display_config.get("group_que_id"))
|
|
29509
|
+
title_que_id = _coerce_positive_int(config.get("titleQue") or display_config.get("title_que_id"))
|
|
28759
29510
|
gantt_config = config.get("viewgraphGanttConfigVO") if isinstance(config.get("viewgraphGanttConfigVO"), dict) else {}
|
|
28760
|
-
|
|
28761
|
-
|
|
28762
|
-
|
|
29511
|
+
start_que_id = _coerce_positive_int(gantt_config.get("startTimeQueId"))
|
|
29512
|
+
end_que_id = _coerce_positive_int(gantt_config.get("endTimeQueId"))
|
|
29513
|
+
gantt_title_que_id = _coerce_positive_int(gantt_config.get("titleQueId"))
|
|
28763
29514
|
query_conditions = _extract_view_query_conditions_config(config)
|
|
29515
|
+
query_conditions["rows"] = query_conditions.get("row_field_ids") or []
|
|
28764
29516
|
query_conditions.pop("row_field_ids", None)
|
|
28765
29517
|
associated_resources = _extract_view_associated_resources_config(config)
|
|
28766
29518
|
payload: dict[str, Any] = {
|
|
@@ -28768,7 +29520,6 @@ def _view_upsert_payload_from_existing_view(
|
|
|
28768
29520
|
"view_key": view_key,
|
|
28769
29521
|
"type": view_type or "table",
|
|
28770
29522
|
"columns": columns,
|
|
28771
|
-
"group_by": group_by,
|
|
28772
29523
|
"filters": [],
|
|
28773
29524
|
"query_conditions": query_conditions,
|
|
28774
29525
|
"associated_resources": {
|
|
@@ -28777,15 +29528,17 @@ def _view_upsert_payload_from_existing_view(
|
|
|
28777
29528
|
"associated_item_ids": list(associated_resources.get("configured_associated_item_ids") or []),
|
|
28778
29529
|
},
|
|
28779
29530
|
}
|
|
28780
|
-
if
|
|
28781
|
-
payload["
|
|
29531
|
+
if group_que_id is not None:
|
|
29532
|
+
payload["group_que_id"] = group_que_id
|
|
29533
|
+
if title_que_id is not None:
|
|
29534
|
+
payload["title_que_id"] = title_que_id
|
|
28782
29535
|
if view_type == "gantt":
|
|
28783
|
-
if
|
|
28784
|
-
payload["
|
|
28785
|
-
if
|
|
28786
|
-
payload["
|
|
28787
|
-
if
|
|
28788
|
-
payload["
|
|
29536
|
+
if start_que_id is not None:
|
|
29537
|
+
payload["start_que_id"] = start_que_id
|
|
29538
|
+
if end_que_id is not None:
|
|
29539
|
+
payload["end_que_id"] = end_que_id
|
|
29540
|
+
if gantt_title_que_id is not None:
|
|
29541
|
+
payload["title_que_id"] = gantt_title_que_id
|
|
28789
29542
|
return payload
|
|
28790
29543
|
|
|
28791
29544
|
|
|
@@ -28796,7 +29549,7 @@ def _first_present(mapping: dict[str, Any], *keys: str) -> Any:
|
|
|
28796
29549
|
return None
|
|
28797
29550
|
|
|
28798
29551
|
|
|
28799
|
-
def
|
|
29552
|
+
def _build_view_creation_payload(
|
|
28800
29553
|
*,
|
|
28801
29554
|
app_key: str,
|
|
28802
29555
|
base_info: dict[str, Any],
|
|
@@ -28804,23 +29557,24 @@ def _build_view_create_payload(
|
|
|
28804
29557
|
patch: ViewUpsertPatch,
|
|
28805
29558
|
ordinal: int,
|
|
28806
29559
|
view_filters: list[list[dict[str, Any]]] | None,
|
|
28807
|
-
|
|
29560
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
28808
29561
|
auth_override: dict[str, Any] | None = None,
|
|
28809
29562
|
explicit_button_dtos: list[dict[str, Any]] | None = None,
|
|
28810
29563
|
query_condition_payload: dict[str, Any] | None = None,
|
|
28811
29564
|
associated_resources_payload: dict[str, Any] | None = None,
|
|
29565
|
+
hierarchy_filter_groups: list[list[dict[str, Any]]] | None = None,
|
|
28812
29566
|
) -> JSONObject:
|
|
28813
29567
|
entity = _entity_spec_from_app(base_info=base_info, schema=schema, views=None)
|
|
28814
29568
|
parsed_schema = _parse_schema(schema)
|
|
28815
|
-
|
|
28816
|
-
field_ids = [
|
|
29569
|
+
visible_que_ids = _resolve_view_visible_que_ids(patch)
|
|
29570
|
+
field_ids = [f"field_{que}" for que in visible_que_ids]
|
|
28817
29571
|
gantt_config = _build_public_gantt_payload(parsed_schema["fields"], extract_field_map(schema), patch)
|
|
28818
29572
|
view_spec = ViewSpec(
|
|
28819
29573
|
view_id=_slugify(patch.name, default=f"view_{uuid4().hex[:6]}"),
|
|
28820
29574
|
name=patch.name,
|
|
28821
29575
|
type=patch.type.value,
|
|
28822
29576
|
field_ids=field_ids,
|
|
28823
|
-
group_by_field_id=
|
|
29577
|
+
group_by_field_id=f"field_{patch.group_que_id}" if patch.group_que_id else None,
|
|
28824
29578
|
config=gantt_config,
|
|
28825
29579
|
)
|
|
28826
29580
|
from ..solution.spec_models import EntitySpec
|
|
@@ -28829,7 +29583,6 @@ def _build_view_create_payload(
|
|
|
28829
29583
|
compiled = compile_views(EntitySpec.model_validate({**entity, "views": [view_spec.model_dump(mode="json")]}))[0]
|
|
28830
29584
|
field_map = extract_field_map(schema)
|
|
28831
29585
|
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
29586
|
payload["appKey"] = app_key
|
|
28834
29587
|
payload["ordinal"] = ordinal
|
|
28835
29588
|
payload["viewgraphQueIds"] = visible_que_ids
|
|
@@ -28841,17 +29594,21 @@ def _build_view_create_payload(
|
|
|
28841
29594
|
payload["beingShowTitleQue"] = True
|
|
28842
29595
|
payload["titleQue"] = gantt_config.get("titleQueId") or (visible_que_ids[0] if visible_que_ids else None)
|
|
28843
29596
|
if patch.type.value == "board":
|
|
28844
|
-
payload["groupQueId"] =
|
|
29597
|
+
payload["groupQueId"] = patch.group_que_id
|
|
28845
29598
|
return _hydrate_view_backend_payload(
|
|
28846
29599
|
payload=payload,
|
|
28847
29600
|
view_type=patch.type.value,
|
|
28848
29601
|
visible_que_id_values=visible_que_ids,
|
|
28849
|
-
group_que_id=
|
|
29602
|
+
group_que_id=patch.group_que_id,
|
|
28850
29603
|
view_filters=view_filters,
|
|
28851
29604
|
gantt_payload=gantt_config,
|
|
28852
29605
|
button_config_dtos=explicit_button_dtos,
|
|
28853
29606
|
query_condition_payload=query_condition_payload,
|
|
28854
29607
|
associated_resources_payload=associated_resources_payload,
|
|
29608
|
+
patch=patch,
|
|
29609
|
+
field_map=field_map,
|
|
29610
|
+
fields_by_que_id=fields_by_que_id,
|
|
29611
|
+
hierarchy_filter_groups=hierarchy_filter_groups,
|
|
28855
29612
|
)
|
|
28856
29613
|
|
|
28857
29614
|
|
|
@@ -28989,6 +29746,10 @@ def _normalize_view_type_name(value: Any) -> str:
|
|
|
28989
29746
|
return "gantt"
|
|
28990
29747
|
if "board" in normalized:
|
|
28991
29748
|
return "board"
|
|
29749
|
+
if "hierarchy" in normalized:
|
|
29750
|
+
return "hierarchy"
|
|
29751
|
+
if "calendar" in normalized:
|
|
29752
|
+
return "calendar"
|
|
28992
29753
|
if "card" in normalized:
|
|
28993
29754
|
return "card"
|
|
28994
29755
|
return "table"
|
|
@@ -29002,20 +29763,20 @@ def _build_view_update_payload(
|
|
|
29002
29763
|
schema: dict[str, Any],
|
|
29003
29764
|
patch: ViewUpsertPatch,
|
|
29004
29765
|
view_filters: list[list[dict[str, Any]]] | None,
|
|
29005
|
-
|
|
29766
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
29006
29767
|
auth_override: dict[str, Any] | None = None,
|
|
29007
29768
|
explicit_button_dtos: list[dict[str, Any]] | None = None,
|
|
29008
29769
|
query_condition_payload: dict[str, Any] | None = None,
|
|
29009
29770
|
associated_resources_payload: dict[str, Any] | None = None,
|
|
29771
|
+
hierarchy_filter_groups: list[list[dict[str, Any]]] | None = None,
|
|
29010
29772
|
) -> JSONObject:
|
|
29011
29773
|
config_response = views.view_get_config(profile=profile, viewgraph_key=source_viewgraph_key)
|
|
29012
29774
|
config = config_response.get("result") if isinstance(config_response.get("result"), dict) else {}
|
|
29013
29775
|
payload = deepcopy(config)
|
|
29014
29776
|
parsed_schema = _parse_schema(schema)
|
|
29015
29777
|
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]
|
|
29778
|
+
visible_que_ids = _resolve_view_visible_que_ids(patch)
|
|
29779
|
+
visible_que_id_values = list(visible_que_ids)
|
|
29019
29780
|
gantt_payload = _build_public_gantt_payload(parsed_schema["fields"], field_map, patch)
|
|
29020
29781
|
|
|
29021
29782
|
for key in (
|
|
@@ -29037,8 +29798,25 @@ def _build_view_update_payload(
|
|
|
29037
29798
|
payload["auth"] = deepcopy(auth_override if isinstance(auth_override, dict) else payload.get("auth") or default_member_auth())
|
|
29038
29799
|
if "viewName" in payload:
|
|
29039
29800
|
payload["viewName"] = patch.name
|
|
29040
|
-
|
|
29041
|
-
|
|
29801
|
+
# 保留系统列在业务列之间的位置;业务列顺序和增删仍由声明决定。
|
|
29802
|
+
existing_que_ids = dict.fromkeys(
|
|
29803
|
+
que_id
|
|
29804
|
+
for que_id in (_coerce_any_int(raw) for raw in (config.get("viewgraphQueIds") or []))
|
|
29805
|
+
if que_id is not None
|
|
29806
|
+
)
|
|
29807
|
+
business_que_ids = iter(que_id for que_id in visible_que_id_values if que_id > 5)
|
|
29808
|
+
merged_que_ids: list[int] = []
|
|
29809
|
+
for que_id in existing_que_ids:
|
|
29810
|
+
if que_id <= 5:
|
|
29811
|
+
merged_que_ids.append(que_id)
|
|
29812
|
+
elif que_id in visible_que_id_values:
|
|
29813
|
+
merged_que_ids.append(next(business_que_ids))
|
|
29814
|
+
merged_que_ids.extend(business_que_ids)
|
|
29815
|
+
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)
|
|
29816
|
+
payload["viewgraphQueIds"] = merged_que_ids
|
|
29817
|
+
payload["viewgraphQuestions"] = _build_viewgraph_questions(
|
|
29818
|
+
schema, merged_que_ids, existing_by_que_id=_flatten_viewgraph_questions(config.get("viewgraphQuestions"))
|
|
29819
|
+
)
|
|
29042
29820
|
payload.setdefault("sortType", "defaultSort")
|
|
29043
29821
|
payload.setdefault("viewgraphSorts", [{"queId": 0, "beingSortAscend": True, "queType": 8}])
|
|
29044
29822
|
payload.setdefault("beingPinNavigate", True)
|
|
@@ -29068,25 +29846,79 @@ def _build_view_update_payload(
|
|
|
29068
29846
|
payload["viewgraphType"] = "boardView"
|
|
29069
29847
|
payload["beingShowTitleQue"] = True
|
|
29070
29848
|
payload["titleQue"] = visible_que_id_values[0] if visible_que_id_values else None
|
|
29071
|
-
payload["groupQueId"] =
|
|
29849
|
+
payload["groupQueId"] = patch.group_que_id
|
|
29072
29850
|
elif normalized_type == "gantt":
|
|
29073
29851
|
payload["viewgraphType"] = "ganttView"
|
|
29074
29852
|
payload["beingShowTitleQue"] = True
|
|
29075
29853
|
payload["titleQue"] = gantt_payload.get("titleQueId") or (visible_que_id_values[0] if visible_que_id_values else None)
|
|
29076
29854
|
payload.pop("groupQueId", None)
|
|
29855
|
+
elif normalized_type == "hierarchy":
|
|
29856
|
+
payload["viewgraphType"] = "hierarchyView"
|
|
29857
|
+
payload["beingShowTitleQue"] = True
|
|
29858
|
+
payload.pop("groupQueId", None)
|
|
29859
|
+
elif normalized_type == "calendar":
|
|
29860
|
+
payload["viewgraphType"] = "calendarView"
|
|
29861
|
+
payload["beingShowTitleQue"] = True
|
|
29862
|
+
payload.pop("groupQueId", None)
|
|
29077
29863
|
return _hydrate_view_backend_payload(
|
|
29078
29864
|
payload=payload,
|
|
29079
29865
|
view_type=normalized_type,
|
|
29080
29866
|
visible_que_id_values=visible_que_id_values,
|
|
29081
|
-
group_que_id=
|
|
29867
|
+
group_que_id=patch.group_que_id,
|
|
29082
29868
|
view_filters=view_filters,
|
|
29083
29869
|
gantt_payload=gantt_payload,
|
|
29084
29870
|
button_config_dtos=button_config_dtos,
|
|
29085
29871
|
query_condition_payload=query_condition_payload,
|
|
29086
29872
|
associated_resources_payload=associated_resources_payload,
|
|
29873
|
+
patch=patch,
|
|
29874
|
+
field_map=field_map,
|
|
29875
|
+
fields_by_que_id=fields_by_que_id,
|
|
29876
|
+
hierarchy_filter_groups=hierarchy_filter_groups,
|
|
29877
|
+
schema=schema,
|
|
29087
29878
|
)
|
|
29088
29879
|
|
|
29089
29880
|
|
|
29881
|
+
# 系统列 queId→公开名(后端语义;读面展示与 viewgraphQueIds 透传时的名称解析)
|
|
29882
|
+
_SYSTEM_VIEW_QUE_NAMES = {
|
|
29883
|
+
0: "编号",
|
|
29884
|
+
1: "提交人",
|
|
29885
|
+
2: "提交时间",
|
|
29886
|
+
3: "更新时间",
|
|
29887
|
+
4: "流程标题",
|
|
29888
|
+
5: "当前流程状态",
|
|
29889
|
+
-17: "数据ID",
|
|
29890
|
+
-19: "删除时间",
|
|
29891
|
+
-20: "删除人",
|
|
29892
|
+
-21: "处理节点",
|
|
29893
|
+
-22: "抄送节点",
|
|
29894
|
+
-23: "处理时间",
|
|
29895
|
+
-24: "抄送时间",
|
|
29896
|
+
-25: "接收时间",
|
|
29897
|
+
}
|
|
29898
|
+
|
|
29899
|
+
_SYSTEM_VIEW_QUE_IDS = frozenset(
|
|
29900
|
+
{
|
|
29901
|
+
*_SYSTEM_VIEW_QUE_NAMES,
|
|
29902
|
+
-1,
|
|
29903
|
+
-2,
|
|
29904
|
+
-3,
|
|
29905
|
+
-4,
|
|
29906
|
+
-5,
|
|
29907
|
+
-6,
|
|
29908
|
+
-7,
|
|
29909
|
+
-8,
|
|
29910
|
+
-18,
|
|
29911
|
+
-30,
|
|
29912
|
+
-31,
|
|
29913
|
+
}
|
|
29914
|
+
)
|
|
29915
|
+
|
|
29916
|
+
|
|
29917
|
+
def _is_known_system_view_que_id(value: Any) -> bool:
|
|
29918
|
+
que_id = _coerce_any_int(value)
|
|
29919
|
+
return que_id in _SYSTEM_VIEW_QUE_IDS if que_id is not None else False
|
|
29920
|
+
|
|
29921
|
+
|
|
29090
29922
|
_KNOWN_SYSTEM_VIEW_COLUMNS = {
|
|
29091
29923
|
"编号",
|
|
29092
29924
|
"当前流程状态",
|
|
@@ -29110,16 +29942,16 @@ def _build_minimal_view_payload(
|
|
|
29110
29942
|
patch: ViewUpsertPatch,
|
|
29111
29943
|
ordinal: int,
|
|
29112
29944
|
view_filters: list[list[dict[str, Any]]] | None,
|
|
29113
|
-
|
|
29945
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
29114
29946
|
auth_override: dict[str, Any] | None = None,
|
|
29115
29947
|
explicit_button_dtos: list[dict[str, Any]] | None = None,
|
|
29116
29948
|
query_condition_payload: dict[str, Any] | None = None,
|
|
29117
29949
|
associated_resources_payload: dict[str, Any] | None = None,
|
|
29950
|
+
hierarchy_filter_groups: list[list[dict[str, Any]]] | None = None,
|
|
29118
29951
|
) -> JSONObject:
|
|
29119
29952
|
field_map = extract_field_map(schema)
|
|
29120
29953
|
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]
|
|
29954
|
+
visible_que_id_values = _resolve_view_visible_que_ids(patch)
|
|
29123
29955
|
gantt_payload = _build_public_gantt_payload(parsed_schema["fields"], field_map, patch)
|
|
29124
29956
|
payload: JSONObject = {
|
|
29125
29957
|
"appKey": app_key,
|
|
@@ -29129,6 +29961,8 @@ def _build_minimal_view_payload(
|
|
|
29129
29961
|
"card": "cardView",
|
|
29130
29962
|
"board": "boardView",
|
|
29131
29963
|
"gantt": "ganttView",
|
|
29964
|
+
"hierarchy": "hierarchyView",
|
|
29965
|
+
"calendar": "calendarView",
|
|
29132
29966
|
}[patch.type.value],
|
|
29133
29967
|
"ordinal": ordinal,
|
|
29134
29968
|
"viewgraphQueIds": visible_que_id_values,
|
|
@@ -29139,17 +29973,21 @@ def _build_minimal_view_payload(
|
|
|
29139
29973
|
payload["beingShowTitleQue"] = True
|
|
29140
29974
|
payload["titleQue"] = gantt_payload.get("titleQueId") or (visible_que_id_values[0] if visible_que_id_values else None)
|
|
29141
29975
|
if patch.type.value == "board":
|
|
29142
|
-
payload["groupQueId"] =
|
|
29976
|
+
payload["groupQueId"] = patch.group_que_id
|
|
29143
29977
|
return _hydrate_view_backend_payload(
|
|
29144
29978
|
payload=payload,
|
|
29145
29979
|
view_type=patch.type.value,
|
|
29146
29980
|
visible_que_id_values=visible_que_id_values,
|
|
29147
|
-
group_que_id=
|
|
29981
|
+
group_que_id=patch.group_que_id,
|
|
29148
29982
|
view_filters=view_filters,
|
|
29149
29983
|
gantt_payload=gantt_payload,
|
|
29150
29984
|
button_config_dtos=explicit_button_dtos,
|
|
29151
29985
|
query_condition_payload=query_condition_payload,
|
|
29152
29986
|
associated_resources_payload=associated_resources_payload,
|
|
29987
|
+
patch=patch,
|
|
29988
|
+
field_map=field_map,
|
|
29989
|
+
fields_by_que_id=fields_by_que_id,
|
|
29990
|
+
hierarchy_filter_groups=hierarchy_filter_groups,
|
|
29153
29991
|
)
|
|
29154
29992
|
|
|
29155
29993
|
|
|
@@ -29164,6 +30002,11 @@ def _hydrate_view_backend_payload(
|
|
|
29164
30002
|
button_config_dtos: list[dict[str, Any]] | None = None,
|
|
29165
30003
|
query_condition_payload: dict[str, Any] | None = None,
|
|
29166
30004
|
associated_resources_payload: dict[str, Any] | None = None,
|
|
30005
|
+
patch: ViewUpsertPatch | None = None,
|
|
30006
|
+
field_map: dict[str, int] | None = None,
|
|
30007
|
+
fields_by_que_id: dict[str, dict[str, Any]] | None = None,
|
|
30008
|
+
hierarchy_filter_groups: list[list[dict[str, Any]]] | None = None,
|
|
30009
|
+
schema: dict[str, Any] | None = None,
|
|
29167
30010
|
) -> JSONObject:
|
|
29168
30011
|
data = deepcopy(payload)
|
|
29169
30012
|
data.setdefault("beingPinNavigate", True)
|
|
@@ -29233,26 +30076,212 @@ def _hydrate_view_backend_payload(
|
|
|
29233
30076
|
data["titleQue"] = (gantt_payload or {}).get("titleQueId") or (visible_que_id_values[0] if visible_que_id_values else None)
|
|
29234
30077
|
data["groupQueId"] = None
|
|
29235
30078
|
data["viewgraphGanttConfigVO"] = deepcopy(gantt_payload) if gantt_payload else None
|
|
30079
|
+
elif view_type == "hierarchy":
|
|
30080
|
+
data["viewgraphType"] = "hierarchyView"
|
|
30081
|
+
data["beingShowTitleQue"] = True
|
|
30082
|
+
data["groupQueId"] = None
|
|
30083
|
+
data["viewgraphGanttConfigVO"] = None
|
|
30084
|
+
elif view_type == "calendar":
|
|
30085
|
+
data["viewgraphType"] = "calendarView"
|
|
30086
|
+
data["beingShowTitleQue"] = True
|
|
30087
|
+
data["groupQueId"] = None
|
|
30088
|
+
data["viewgraphGanttConfigVO"] = None
|
|
29236
30089
|
if view_filters is not None:
|
|
29237
30090
|
data["viewgraphLimit"] = deepcopy(view_filters)
|
|
29238
30091
|
data["viewgraphLimitType"] = 1
|
|
29239
30092
|
data["viewgraphLimitFormula"] = ""
|
|
29240
30093
|
data["dataPermissionType"] = "CUSTOM"
|
|
29241
30094
|
data["dataScope"] = "CUSTOM" if view_filters else "ALL"
|
|
30095
|
+
if patch is not None:
|
|
30096
|
+
_apply_view_config_groups(
|
|
30097
|
+
data,
|
|
30098
|
+
patch=patch,
|
|
30099
|
+
view_type=view_type,
|
|
30100
|
+
visible_que_id_values=visible_que_id_values,
|
|
30101
|
+
field_map=field_map or {},
|
|
30102
|
+
fields_by_que_id=fields_by_que_id or {},
|
|
30103
|
+
hierarchy_filter_groups=hierarchy_filter_groups,
|
|
30104
|
+
config=payload if isinstance(payload, dict) else None,
|
|
30105
|
+
id_to_name=_collect_field_id_to_name(schema) if isinstance(schema, dict) else None,
|
|
30106
|
+
)
|
|
29242
30107
|
_apply_view_query_conditions_payload(data, query_condition_payload)
|
|
29243
30108
|
_apply_view_associated_resources_payload(data, associated_resources_payload)
|
|
29244
30109
|
return data
|
|
29245
30110
|
|
|
29246
30111
|
|
|
29247
|
-
|
|
29248
|
-
|
|
29249
|
-
|
|
29250
|
-
|
|
29251
|
-
|
|
29252
|
-
|
|
30112
|
+
_DATA_PERMISSION_SCOPE_MAP = {
|
|
30113
|
+
"all": "ALL",
|
|
30114
|
+
"oneself": "ONESELF",
|
|
30115
|
+
"dept": "DEPT",
|
|
30116
|
+
"subordinate": "SUBORDINATE",
|
|
30117
|
+
"custom": "CUSTOM",
|
|
30118
|
+
}
|
|
30119
|
+
|
|
30120
|
+
|
|
30121
|
+
def _apply_view_config_groups(
|
|
30122
|
+
data: JSONObject,
|
|
30123
|
+
*,
|
|
30124
|
+
patch: ViewUpsertPatch,
|
|
30125
|
+
view_type: str,
|
|
30126
|
+
visible_que_id_values: list[int],
|
|
30127
|
+
field_map: dict[str, int],
|
|
30128
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
30129
|
+
hierarchy_filter_groups: list[list[dict[str, Any]]] | None,
|
|
30130
|
+
config: dict[str, Any] | None = None,
|
|
30131
|
+
id_to_name: dict[int, str] | None = None,
|
|
30132
|
+
) -> None:
|
|
30133
|
+
"""编译 complete 档新字段组:data_permission/sort/card/hierarchy/display/record/share。"""
|
|
30134
|
+
if patch.data_permission is not None:
|
|
30135
|
+
data["dataPermissionType"] = "CONFIG" if patch.data_permission.type == "config" else "CUSTOM"
|
|
30136
|
+
data["dataScope"] = _DATA_PERMISSION_SCOPE_MAP.get(patch.data_permission.scope, "ALL")
|
|
30137
|
+
if patch.sort is not None:
|
|
30138
|
+
if patch.sort:
|
|
30139
|
+
sorts: list[dict[str, Any]] = []
|
|
30140
|
+
for rule in patch.sort:
|
|
30141
|
+
field = fields_by_que_id.get(rule.que_id) or {}
|
|
30142
|
+
que_type = _coerce_positive_int(field.get("que_type")) or FIELD_TYPE_TO_QUESTION_TYPE.get(
|
|
30143
|
+
str(field.get("type") or FieldType.text.value), 2
|
|
30144
|
+
)
|
|
30145
|
+
sorts.append(
|
|
30146
|
+
{
|
|
30147
|
+
"queId": rule.que_id,
|
|
30148
|
+
"beingSortAscend": bool(rule.ascending),
|
|
30149
|
+
"queType": que_type,
|
|
30150
|
+
}
|
|
30151
|
+
)
|
|
30152
|
+
data["sortType"] = "customSort"
|
|
30153
|
+
data["viewgraphSorts"] = sorts
|
|
30154
|
+
else:
|
|
30155
|
+
data["sortType"] = "defaultSort"
|
|
30156
|
+
data["viewgraphSorts"] = [{"queId": 0, "beingSortAscend": True, "queType": 8}]
|
|
30157
|
+
card = patch.card
|
|
30158
|
+
config = config or {}
|
|
30159
|
+
id_to_name = id_to_name or {}
|
|
30160
|
+
if card is not None:
|
|
30161
|
+
if card.title_que_id is not None:
|
|
30162
|
+
data["titleQue"] = card.title_que_id
|
|
30163
|
+
if card.cover_que_id is not None:
|
|
30164
|
+
data["frontCoverQueId"] = card.cover_que_id
|
|
30165
|
+
if card.clipping_mode is not None:
|
|
30166
|
+
data["clippingMode"] = card.clipping_mode
|
|
30167
|
+
if card.image_adaption is not None:
|
|
30168
|
+
data["beingImageAdaption"] = card.image_adaption
|
|
30169
|
+
if card.show_field_title is not None:
|
|
30170
|
+
data["beingShowQueTitle"] = card.show_field_title
|
|
30171
|
+
if card.show_title is not None:
|
|
30172
|
+
data["beingShowTitleQue"] = card.show_title
|
|
30173
|
+
if card.show_cover is not None:
|
|
30174
|
+
data["beingShowCover"] = card.show_cover
|
|
30175
|
+
if card.group_que_id is not None:
|
|
30176
|
+
data["groupQueId"] = card.group_que_id
|
|
30177
|
+
if card.group_color is not None:
|
|
30178
|
+
data["beingGroupColor"] = card.group_color
|
|
30179
|
+
if patch.hierarchy is not None and view_type == "hierarchy":
|
|
30180
|
+
data["viewgraphHierarchyConfigVO"] = {
|
|
30181
|
+
"titleQueId": patch.hierarchy.title_que_id,
|
|
30182
|
+
"childrenQueId": patch.hierarchy.children_que_id,
|
|
30183
|
+
"rootNodeFilter": deepcopy(hierarchy_filter_groups) if hierarchy_filter_groups is not None else [],
|
|
30184
|
+
}
|
|
30185
|
+
data["titleQue"] = patch.hierarchy.title_que_id
|
|
30186
|
+
if patch.calendar is not None and view_type == "calendar":
|
|
30187
|
+
data["viewgraphCalendarConfigVO"] = {
|
|
30188
|
+
"titleQueId": patch.calendar.title_que_id,
|
|
30189
|
+
"dateQueId": patch.calendar.date_que_id,
|
|
30190
|
+
"colorQueId": patch.calendar.color_que_id,
|
|
30191
|
+
}
|
|
30192
|
+
data["titleQue"] = patch.calendar.title_que_id
|
|
30193
|
+
display = patch.display
|
|
30194
|
+
if display is not None:
|
|
30195
|
+
if display.row_height is not None:
|
|
30196
|
+
data["defaultRowHigh"] = display.row_height
|
|
30197
|
+
if display.pin_navigate is not None:
|
|
30198
|
+
data["beingPinNavigate"] = display.pin_navigate
|
|
30199
|
+
if display.remark is not None:
|
|
30200
|
+
data["remark"] = display.remark
|
|
30201
|
+
record = patch.record
|
|
30202
|
+
if record is not None:
|
|
30203
|
+
if record.audit_visible is not None:
|
|
30204
|
+
data["beingAuditRecordVisible"] = record.audit_visible
|
|
30205
|
+
if record.data_log_visible is not None:
|
|
30206
|
+
data["beingDataLogVisible"] = record.data_log_visible
|
|
30207
|
+
if record.qrobot_record_visible is not None:
|
|
30208
|
+
data["beingQrobotRecordVisible"] = record.qrobot_record_visible
|
|
30209
|
+
if record.workflow_future_visible is not None:
|
|
30210
|
+
data["beingWorkflowNodeFutureListVisible"] = record.workflow_future_visible
|
|
30211
|
+
if record.comment is not None:
|
|
30212
|
+
data["beingCommentStatus"] = record.comment
|
|
30213
|
+
share = patch.share
|
|
30214
|
+
if share is not None:
|
|
30215
|
+
if share.need_pass is not None:
|
|
30216
|
+
data["beingNeedPass"] = share.need_pass
|
|
30217
|
+
data["needPass"] = share.need_pass
|
|
30218
|
+
if share.password is None:
|
|
30219
|
+
# 声明未带密码(提取语义:不回传敏感值)——显式清空 payload 里读面带来的旧密码;
|
|
30220
|
+
# 后端 isNotBlank(空)跳过校验且不更新密码列,原密码保留(Java AbstractViewgraphStrategicService:424)
|
|
30221
|
+
data["viewgraphPass"] = ""
|
|
30222
|
+
data.pop("pass", None)
|
|
30223
|
+
if share.password is not None:
|
|
30224
|
+
data["viewgraphPass"] = share.password
|
|
30225
|
+
if share.force_login is not None:
|
|
30226
|
+
data["beingForceLogin"] = share.force_login
|
|
30227
|
+
if share.print_enabled is not None:
|
|
30228
|
+
data["beingPrintStatus"] = share.print_enabled
|
|
30229
|
+
|
|
30230
|
+
|
|
30231
|
+
def _iter_view_config_group_field_names(patch: ViewUpsertPatch) -> list[tuple[str, list[int]]]:
|
|
30232
|
+
"""枚举 complete 档新字段组引用的 queId(用于 UNKNOWN_VIEW_FIELD 校验)。"""
|
|
30233
|
+
groups: list[tuple[str, list[int]]] = []
|
|
30234
|
+
if patch.sort:
|
|
30235
|
+
groups.append(("sort", [rule.que_id for rule in patch.sort]))
|
|
30236
|
+
if patch.card is not None:
|
|
30237
|
+
groups.append(
|
|
30238
|
+
(
|
|
30239
|
+
"card",
|
|
30240
|
+
[
|
|
30241
|
+
que
|
|
30242
|
+
for que in (patch.card.title_que_id, patch.card.cover_que_id, patch.card.group_que_id)
|
|
30243
|
+
if que
|
|
30244
|
+
],
|
|
30245
|
+
)
|
|
30246
|
+
)
|
|
30247
|
+
if patch.gantt is not None:
|
|
30248
|
+
gantt_ids = [
|
|
30249
|
+
que
|
|
30250
|
+
for que in (patch.gantt.title_que_id, patch.gantt.start_que_id, patch.gantt.end_que_id)
|
|
30251
|
+
if que
|
|
30252
|
+
]
|
|
30253
|
+
gantt_ids.extend(group.que_id for group in patch.gantt.groups or [])
|
|
30254
|
+
for dep in patch.gantt.dependencies or []:
|
|
30255
|
+
gantt_ids.extend([dep.dependency_que_id, dep.predecessor_que_id])
|
|
30256
|
+
if gantt_ids:
|
|
30257
|
+
groups.append(("gantt", gantt_ids))
|
|
30258
|
+
if patch.hierarchy is not None:
|
|
30259
|
+
groups.append(("hierarchy", [patch.hierarchy.title_que_id, patch.hierarchy.children_que_id]))
|
|
30260
|
+
if patch.calendar is not None:
|
|
30261
|
+
calendar_ids = [patch.calendar.title_que_id, patch.calendar.date_que_id]
|
|
30262
|
+
if patch.calendar.color_que_id is not None:
|
|
30263
|
+
calendar_ids.append(patch.calendar.color_que_id)
|
|
30264
|
+
groups.append(("calendar", calendar_ids))
|
|
30265
|
+
return groups
|
|
30266
|
+
|
|
30267
|
+
|
|
30268
|
+
|
|
30269
|
+
def _resolve_view_visible_que_ids(patch: ViewUpsertPatch) -> list[int]:
|
|
30270
|
+
"""收集视图引用的业务字段和已知系统字段 queId。"""
|
|
30271
|
+
ordered: list[int] = []
|
|
30272
|
+
extra_ids: list[int | None] = []
|
|
30273
|
+
if patch.hierarchy is not None:
|
|
30274
|
+
extra_ids.extend([patch.hierarchy.title_que_id, patch.hierarchy.children_que_id])
|
|
30275
|
+
if patch.calendar is not None:
|
|
30276
|
+
extra_ids.extend([patch.calendar.title_que_id, patch.calendar.date_que_id, patch.calendar.color_que_id])
|
|
30277
|
+
for value in [*patch.columns, patch.title_que_id, patch.start_que_id, patch.end_que_id, patch.group_que_id, *extra_ids]:
|
|
30278
|
+
que_id = _coerce_any_int(value)
|
|
30279
|
+
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:
|
|
30280
|
+
ordered.append(que_id)
|
|
29253
30281
|
return ordered
|
|
29254
30282
|
|
|
29255
30283
|
|
|
30284
|
+
|
|
29256
30285
|
def _build_public_gantt_payload(
|
|
29257
30286
|
fields: list[dict[str, Any]],
|
|
29258
30287
|
field_map: dict[str, int],
|
|
@@ -29260,22 +30289,35 @@ def _build_public_gantt_payload(
|
|
|
29260
30289
|
) -> dict[str, Any]:
|
|
29261
30290
|
if patch.type.value != "gantt":
|
|
29262
30291
|
return {}
|
|
29263
|
-
|
|
29264
|
-
|
|
29265
|
-
|
|
30292
|
+
gantt_config = patch.gantt
|
|
30293
|
+
title_que_id = (
|
|
30294
|
+
getattr(gantt_config, "title_que_id", None)
|
|
30295
|
+
or patch.title_que_id
|
|
30296
|
+
or (patch.columns[0] if patch.columns else None)
|
|
30297
|
+
)
|
|
30298
|
+
start_que_id = getattr(gantt_config, "start_que_id", None) or patch.start_que_id
|
|
30299
|
+
end_que_id = getattr(gantt_config, "end_que_id", None) or patch.end_que_id
|
|
30300
|
+
dependency = gantt_config.dependencies[0] if gantt_config is not None and gantt_config.dependencies else None
|
|
30301
|
+
auto_calibration = bool(getattr(gantt_config, "auto_calibration", False) or False)
|
|
29266
30302
|
return {
|
|
29267
|
-
"titleQueId":
|
|
29268
|
-
"startTimeQueId":
|
|
29269
|
-
"endTimeQueId":
|
|
29270
|
-
"defaultTimeDimension": "week",
|
|
29271
|
-
"ganttGroupVOList": [
|
|
30303
|
+
"titleQueId": title_que_id,
|
|
30304
|
+
"startTimeQueId": start_que_id,
|
|
30305
|
+
"endTimeQueId": end_que_id,
|
|
30306
|
+
"defaultTimeDimension": getattr(gantt_config, "default_time_dimension", None) or "week",
|
|
30307
|
+
"ganttGroupVOList": [
|
|
30308
|
+
{
|
|
30309
|
+
"groupQueId": group.que_id,
|
|
30310
|
+
"ordinal": group.ordinal if group.ordinal is not None else index,
|
|
30311
|
+
}
|
|
30312
|
+
for index, group in enumerate((gantt_config.groups if gantt_config is not None else []), start=1)
|
|
30313
|
+
],
|
|
29272
30314
|
"ganttDependencyVO": {
|
|
29273
|
-
"dependencyQueId": None,
|
|
29274
|
-
"predecessorTaskQueId": None,
|
|
29275
|
-
"
|
|
29276
|
-
"
|
|
29277
|
-
"endEndOptionId": None,
|
|
29278
|
-
"endStartOptionId": None,
|
|
30315
|
+
"dependencyQueId": dependency.dependency_que_id if dependency else None,
|
|
30316
|
+
"predecessorTaskQueId": dependency.predecessor_que_id if dependency else None,
|
|
30317
|
+
"startStartOptionId": dependency.start_start_option_id if dependency else None,
|
|
30318
|
+
"startEndOptionId": dependency.start_end_option_id if dependency else None,
|
|
30319
|
+
"endEndOptionId": dependency.end_end_option_id if dependency else None,
|
|
30320
|
+
"endStartOptionId": dependency.end_start_option_id if dependency else None,
|
|
29279
30321
|
},
|
|
29280
30322
|
"ganttAutoCalibrationVO": {
|
|
29281
30323
|
"autoCalibrationRuleVO": {
|
|
@@ -29286,15 +30328,15 @@ def _build_public_gantt_payload(
|
|
|
29286
30328
|
"endStartFinish": True,
|
|
29287
30329
|
"endEndFinish": False,
|
|
29288
30330
|
},
|
|
29289
|
-
"beingAutoCalibration":
|
|
29290
|
-
"userAutoCalibration":
|
|
30331
|
+
"beingAutoCalibration": auto_calibration,
|
|
30332
|
+
"userAutoCalibration": auto_calibration,
|
|
29291
30333
|
},
|
|
29292
30334
|
}
|
|
29293
30335
|
|
|
29294
30336
|
|
|
29295
30337
|
def _build_view_filter_groups(
|
|
29296
30338
|
*,
|
|
29297
|
-
|
|
30339
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
29298
30340
|
filters: list[Any],
|
|
29299
30341
|
) -> tuple[list[list[dict[str, Any]]], list[dict[str, Any]]]:
|
|
29300
30342
|
translated_rules: list[dict[str, Any]] = []
|
|
@@ -29304,14 +30346,14 @@ def _build_view_filter_groups(
|
|
|
29304
30346
|
raw_rule = raw_rule.model_dump(mode="json")
|
|
29305
30347
|
if not isinstance(raw_rule, dict):
|
|
29306
30348
|
continue
|
|
29307
|
-
|
|
29308
|
-
field =
|
|
30349
|
+
rule_que_id = _coerce_positive_int(raw_rule.get("que_id"))
|
|
30350
|
+
field = fields_by_que_id.get(rule_que_id) if rule_que_id is not None else None
|
|
29309
30351
|
if field is None:
|
|
29310
30352
|
issues.append(
|
|
29311
30353
|
{
|
|
29312
30354
|
"error_code": "UNKNOWN_VIEW_FIELD",
|
|
29313
|
-
"missing_fields": [
|
|
29314
|
-
"reason_path": "filters[].
|
|
30355
|
+
"missing_fields": [str(raw_rule.get("que_id") or "")],
|
|
30356
|
+
"reason_path": "filters[].que_id",
|
|
29315
30357
|
}
|
|
29316
30358
|
)
|
|
29317
30359
|
continue
|
|
@@ -29393,7 +30435,8 @@ def _resolve_view_filter_values(
|
|
|
29393
30435
|
unresolved_values.append(_stringify_condition_value(value))
|
|
29394
30436
|
continue
|
|
29395
30437
|
resolved_details.append({"id": resolved.get("id"), "value": str(resolved.get("value") or resolved.get("id"))})
|
|
29396
|
-
if unresolved_values:
|
|
30438
|
+
if unresolved_values and not resolved_details:
|
|
30439
|
+
# 全部值都解析不出(选项已删):返回错误让上层判定(该规则已死语义)
|
|
29397
30440
|
return (
|
|
29398
30441
|
[],
|
|
29399
30442
|
[],
|
|
@@ -29408,6 +30451,7 @@ def _resolve_view_filter_values(
|
|
|
29408
30451
|
},
|
|
29409
30452
|
},
|
|
29410
30453
|
)
|
|
30454
|
+
# 部分值解析不出:剔除已删选项值继续写(兼容存量脏数据,不再整视图失败)
|
|
29411
30455
|
return (
|
|
29412
30456
|
[str(detail["id"]) for detail in resolved_details if detail.get("id") is not None],
|
|
29413
30457
|
[_coerce_view_filter_value_detail(detail) for detail in resolved_details],
|
|
@@ -29451,6 +30495,10 @@ def _build_view_filter_value_details(values: list[Any]) -> list[dict[str, Any]]:
|
|
|
29451
30495
|
details.append(_coerce_view_filter_value_detail({"id": item_id, "value": item_value if item_value is not None else str(item_id)}))
|
|
29452
30496
|
elif isinstance(value, int):
|
|
29453
30497
|
details.append(_coerce_view_filter_value_detail({"id": value, "value": str(value)}))
|
|
30498
|
+
elif isinstance(value, str):
|
|
30499
|
+
# text/number 字段:AnswerValue.id 是 Integer(选项 id),字符串值无 id;
|
|
30500
|
+
# 后端 formula getId()==null → 走 getValue() 分支构建 ""value"" 形式
|
|
30501
|
+
details.append(_coerce_view_filter_value_detail({"id": None, "value": value}))
|
|
29454
30502
|
return details
|
|
29455
30503
|
|
|
29456
30504
|
|
|
@@ -29617,11 +30665,11 @@ def _view_filter_groups_semantic_readback(expected: Any, actual: Any) -> tuple[b
|
|
|
29617
30665
|
def _public_view_filter_groups_from_match_rules(
|
|
29618
30666
|
groups: Any,
|
|
29619
30667
|
*,
|
|
29620
|
-
|
|
30668
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
29621
30669
|
) -> list[list[dict[str, Any]]]:
|
|
29622
30670
|
fields_by_que_id = {
|
|
29623
30671
|
_coerce_positive_int(field.get("que_id")): field
|
|
29624
|
-
for field in
|
|
30672
|
+
for field in fields_by_que_id.values()
|
|
29625
30673
|
if isinstance(field, dict) and _coerce_positive_int(field.get("que_id")) is not None
|
|
29626
30674
|
}
|
|
29627
30675
|
public_groups: list[list[dict[str, Any]]] = []
|
|
@@ -29632,7 +30680,7 @@ def _public_view_filter_groups_from_match_rules(
|
|
|
29632
30680
|
field = fields_by_que_id.get(que_id) or {}
|
|
29633
30681
|
values = _public_view_filter_rule_values(rule, field=field)
|
|
29634
30682
|
public_rule: dict[str, Any] = {
|
|
29635
|
-
"
|
|
30683
|
+
"que_id": que_id,
|
|
29636
30684
|
"operator": _public_view_filter_operator_from_judge_type(rule.get("judgeType"), values=values),
|
|
29637
30685
|
}
|
|
29638
30686
|
if public_rule["operator"] == "in":
|
|
@@ -29657,6 +30705,8 @@ def _public_view_filter_operator_from_judge_type(judge_type: Any, *, values: lis
|
|
|
29657
30705
|
if normalized in {JUDGE_INCLUDE, JUDGE_FUZZY_MATCH}:
|
|
29658
30706
|
return "contains"
|
|
29659
30707
|
if normalized in {JUDGE_EQUAL_ANY, JUDGE_INCLUDE_ANY}:
|
|
30708
|
+
# ANY 判型+空值 = "该字段未选任何值"的合法存量形态(judgeType 保真回写,
|
|
30709
|
+
# 转 is_empty 会丢判型——后端 9 与 0 是不同判定域,实测回读不同)
|
|
29660
30710
|
return "in"
|
|
29661
30711
|
if normalized == JUDGE_GREATER_OR_EQUAL:
|
|
29662
30712
|
return "gte"
|