@qingflow-tech/qingflow-cli 1.1.50 → 1.1.53
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 +8 -2
- package/skills/qingflow-cli/SKILL.md +115 -3
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md +3 -9
- package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +3 -2
- package/skills/qingflow-cli/reference/builder/30-schema-fields.md +9 -5
- package/skills/qingflow-cli/reference/builder/50-views.md +192 -191
- package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +2 -0
- package/skills/qingflow-cli/reference/builder/README.md +1 -1
- package/skills/qingflow-cli/reference/builder/reference/app-delivery-sop.md +6 -6
- package/skills/qingflow-cli/reference/data/README.md +1 -1
- 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/app_form_compat.py +3 -0
- package/src/qingflow_mcp/app_form_validator.py +69 -17
- package/src/qingflow_mcp/builder_facade/app_form.py +497 -100
- package/src/qingflow_mcp/builder_facade/models.py +227 -17
- package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +1 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +64 -30
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +6 -10
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +6 -10
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +11 -18
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +6 -10
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +6 -10
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +6 -10
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +46 -15
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/rich_text.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +61 -27
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/formula.json +43 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +7 -12
- 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 +1666 -509
- 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 +76 -55
- package/src/qingflow_mcp/config.py +1 -0
- 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 +34 -10
- 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 +218 -398
- package/src/qingflow_mcp/tools/base.py +42 -10
- 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,
|
|
@@ -159,9 +163,14 @@ FIELD_TYPE_TO_QUESTION_TYPE: dict[str, int] = {
|
|
|
159
163
|
FieldType.relation.value: 25,
|
|
160
164
|
}
|
|
161
165
|
|
|
166
|
+
# code_block / q_linker 输出目标字段类型。后端写链无类型白名单(FormSvcImpl 原样
|
|
167
|
+
# 落库),运行时 DataSvcImpl.getAnswerValueByString:attachment 按拆分 URL 写入、
|
|
168
|
+
# subtable 整表组装(均有专门分支),rich_text/data_relation 走 Question.canImport
|
|
169
|
+
# 兜底按字符串写入(不报错)——四者为存量合法配置,与 validator 侧白名单同口径。
|
|
162
170
|
INTEGRATION_OUTPUT_TARGET_FIELD_TYPES: tuple[str, ...] = (
|
|
163
171
|
FieldType.text.value,
|
|
164
172
|
FieldType.long_text.value,
|
|
173
|
+
"rich_text",
|
|
165
174
|
FieldType.number.value,
|
|
166
175
|
FieldType.amount.value,
|
|
167
176
|
FieldType.date.value,
|
|
@@ -169,6 +178,9 @@ INTEGRATION_OUTPUT_TARGET_FIELD_TYPES: tuple[str, ...] = (
|
|
|
169
178
|
FieldType.single_select.value,
|
|
170
179
|
FieldType.multi_select.value,
|
|
171
180
|
FieldType.boolean.value,
|
|
181
|
+
FieldType.attachment.value,
|
|
182
|
+
"data_relation",
|
|
183
|
+
FieldType.subtable.value,
|
|
172
184
|
)
|
|
173
185
|
|
|
174
186
|
MATCH_TYPE_ACCURACY = 1
|
|
@@ -290,6 +302,7 @@ class AiBuilderFacade:
|
|
|
290
302
|
self.buttons = buttons
|
|
291
303
|
self.packages = packages
|
|
292
304
|
self.views = views
|
|
305
|
+
self._view_writer = ViewWriter(views.sessions, views.backend)
|
|
293
306
|
self.workflows = workflows
|
|
294
307
|
self.portals = portals
|
|
295
308
|
self.charts = charts
|
|
@@ -3072,14 +3085,27 @@ class AiBuilderFacade:
|
|
|
3072
3085
|
member_ids=visibility.external_selectors.member_ids,
|
|
3073
3086
|
member_emails=visibility.external_selectors.member_emails,
|
|
3074
3087
|
)
|
|
3075
|
-
|
|
3076
|
-
|
|
3088
|
+
# 冗余引用降级:提取器对同一成员同时产出 uid/email/name 三通道(uid 必有且直通解析,
|
|
3089
|
+
# 保证成员不丢);email/name 走后端通讯录搜索,受数据/权限边界影响解析可能失败。
|
|
3090
|
+
# 这类失败降级为非阻断——只有 uid/dept_id 直通也覆盖不了的引用才真正阻断。
|
|
3091
|
+
_SOFT_RESOLUTION_KINDS = {"member_email", "member_name", "department_name", "role", "external_member_email"}
|
|
3092
|
+
_NOT_FOUND_RESOLUTION_CODES = {"MEMBER_NOT_FOUND", "AMBIGUOUS_MEMBER", "ROLE_NOT_FOUND", "AMBIGUOUS_ROLE", "DEPARTMENT_NOT_FOUND", "AMBIGUOUS_DEPARTMENT"}
|
|
3093
|
+
blocking_issues = [
|
|
3094
|
+
issue
|
|
3095
|
+
for issue in [
|
|
3077
3096
|
*member_resolution["issues"],
|
|
3078
3097
|
*role_resolution["issues"],
|
|
3079
3098
|
*department_resolution["issues"],
|
|
3080
3099
|
*external_member_resolution["issues"],
|
|
3081
3100
|
]
|
|
3082
|
-
|
|
3101
|
+
# 冗余 email/name 引用「找不到」降级为非阻断(uid/id 直通已保主体不丢);
|
|
3102
|
+
# 权限拒绝/查询失败类(LOOKUP_FAILED/PERMISSION_DENIED)仍阻断
|
|
3103
|
+
if not (
|
|
3104
|
+
str(issue.get("kind") or "") in _SOFT_RESOLUTION_KINDS
|
|
3105
|
+
and str(issue.get("error_code") or "") in _NOT_FOUND_RESOLUTION_CODES
|
|
3106
|
+
)
|
|
3107
|
+
]
|
|
3108
|
+
self._raise_visibility_resolution_issues(blocking_issues)
|
|
3083
3109
|
|
|
3084
3110
|
external_depart_entries: list[dict[str, Any]] = []
|
|
3085
3111
|
for dept_id in visibility.external_selectors.dept_ids:
|
|
@@ -3220,7 +3246,7 @@ class AiBuilderFacade:
|
|
|
3220
3246
|
}
|
|
3221
3247
|
)
|
|
3222
3248
|
condition_matrix, condition_issues = _build_flow_condition_matrix(
|
|
3223
|
-
|
|
3249
|
+
fields_by_que_id=field_name_to_field,
|
|
3224
3250
|
node=normalized_node,
|
|
3225
3251
|
)
|
|
3226
3252
|
issues.extend({**issue, "node_id": node.get("id")} for issue in condition_issues)
|
|
@@ -3907,7 +3933,7 @@ class AiBuilderFacade:
|
|
|
3907
3933
|
"reason_path": f"patch_buttons[{index}].button_id",
|
|
3908
3934
|
"button_id": selector_id,
|
|
3909
3935
|
"message": "button_id does not exist in the current app draft",
|
|
3910
|
-
"next_action": "call
|
|
3936
|
+
"next_action": "call app_get_buttons with button_text to reuse an existing custom button; create only if absent via app_custom_buttons_apply",
|
|
3911
3937
|
}
|
|
3912
3938
|
issues.append(issue)
|
|
3913
3939
|
results.append({"index": index, "status": "failed", **issue})
|
|
@@ -4211,7 +4237,7 @@ class AiBuilderFacade:
|
|
|
4211
4237
|
"reason_path": f"upsert_buttons[{index}].button_id",
|
|
4212
4238
|
"button_id": button_id,
|
|
4213
4239
|
"message": "button_id does not exist in the current app draft",
|
|
4214
|
-
"next_action": "call
|
|
4240
|
+
"next_action": "call app_get_buttons with button_text to reuse an existing custom button; create only if absent via app_custom_buttons_apply",
|
|
4215
4241
|
}
|
|
4216
4242
|
)
|
|
4217
4243
|
continue
|
|
@@ -4282,7 +4308,7 @@ class AiBuilderFacade:
|
|
|
4282
4308
|
"reason_path": f"remove_buttons[{index}].button_id",
|
|
4283
4309
|
"button_id": selector_id,
|
|
4284
4310
|
"message": "button_id does not exist in the current app draft",
|
|
4285
|
-
"next_action": "call
|
|
4311
|
+
"next_action": "call app_get_buttons with button_text to reuse an existing custom button; create only if absent via app_custom_buttons_apply",
|
|
4286
4312
|
}
|
|
4287
4313
|
)
|
|
4288
4314
|
continue
|
|
@@ -7155,10 +7181,14 @@ class AiBuilderFacade:
|
|
|
7155
7181
|
fields = cast(list[dict[str, Any]], state["parsed"]["fields"])
|
|
7156
7182
|
return _chart_field_names_by_id_from_public_fields(app_key=app_key, fields=fields), None
|
|
7157
7183
|
|
|
7158
|
-
def app_get_buttons(self, *, profile: str, app_key: str) -> JSONObject:
|
|
7184
|
+
def app_get_buttons(self, *, profile: str, app_key: str, button_text: str | None = None) -> JSONObject:
|
|
7159
7185
|
self.apps._require_app_key(app_key)
|
|
7160
7186
|
warnings: list[dict[str, Any]] = []
|
|
7161
7187
|
verification: dict[str, Any] = {"buttons_loaded": True, "view_configs_loaded": True}
|
|
7188
|
+
normalized_text = str(button_text or "").strip() or None
|
|
7189
|
+
normalized_args: dict[str, Any] = {"app_key": app_key}
|
|
7190
|
+
if normalized_text is not None:
|
|
7191
|
+
normalized_args["button_text"] = normalized_text
|
|
7162
7192
|
try:
|
|
7163
7193
|
raw = self.buttons.custom_button_list(profile=profile, app_key=app_key, being_draft=True)
|
|
7164
7194
|
except (QingflowApiError, RuntimeError) as error:
|
|
@@ -7166,11 +7196,18 @@ class AiBuilderFacade:
|
|
|
7166
7196
|
return _failed_from_api_error(
|
|
7167
7197
|
"BUTTONS_READ_FAILED",
|
|
7168
7198
|
api_error,
|
|
7169
|
-
normalized_args=
|
|
7199
|
+
normalized_args=normalized_args,
|
|
7170
7200
|
details={"app_key": app_key},
|
|
7171
7201
|
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
7172
7202
|
)
|
|
7173
7203
|
buttons = raw.get("items") if isinstance(raw.get("items"), list) else []
|
|
7204
|
+
total_count = len(buttons)
|
|
7205
|
+
if normalized_text is not None:
|
|
7206
|
+
buttons = [
|
|
7207
|
+
item
|
|
7208
|
+
for item in buttons
|
|
7209
|
+
if isinstance(item, dict) and str(item.get("buttonText") or item.get("button_text") or "").strip() == normalized_text
|
|
7210
|
+
]
|
|
7174
7211
|
view_configs: list[dict[str, Any]] = []
|
|
7175
7212
|
view_config_read_errors: list[dict[str, Any]] = []
|
|
7176
7213
|
try:
|
|
@@ -7190,7 +7227,7 @@ class AiBuilderFacade:
|
|
|
7190
7227
|
if view_config_read_errors:
|
|
7191
7228
|
verification["view_configs_loaded"] = False
|
|
7192
7229
|
warnings.append(_warning("BUTTON_VIEW_CONFIGS_PARTIAL", "some view button binding configs could not be read back"))
|
|
7193
|
-
|
|
7230
|
+
response: JSONObject = {
|
|
7194
7231
|
"status": "success",
|
|
7195
7232
|
"error_code": None,
|
|
7196
7233
|
"recoverable": False,
|
|
@@ -7205,6 +7242,11 @@ class AiBuilderFacade:
|
|
|
7205
7242
|
"verification": verification,
|
|
7206
7243
|
"details": {"view_config_read_errors": view_config_read_errors} if view_config_read_errors else {},
|
|
7207
7244
|
}
|
|
7245
|
+
if normalized_text is not None:
|
|
7246
|
+
response["button_text_filter"] = normalized_text
|
|
7247
|
+
response["total_count"] = total_count
|
|
7248
|
+
verification["button_text_exact_match"] = True
|
|
7249
|
+
return response
|
|
7208
7250
|
|
|
7209
7251
|
def app_get_associated_resources(self, *, profile: str, app_key: str) -> JSONObject:
|
|
7210
7252
|
self.apps._require_app_key(app_key)
|
|
@@ -8430,11 +8472,7 @@ class AiBuilderFacade:
|
|
|
8430
8472
|
try:
|
|
8431
8473
|
schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
8432
8474
|
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
|
-
}
|
|
8475
|
+
fields_by_que_id = _collect_fields_by_que_id([], schema=schema)
|
|
8438
8476
|
existing_views, _views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=False)
|
|
8439
8477
|
except (QingflowApiError, RuntimeError) as error:
|
|
8440
8478
|
api_error = _coerce_api_error(error)
|
|
@@ -8525,7 +8563,7 @@ class AiBuilderFacade:
|
|
|
8525
8563
|
view_binding = _custom_button_view_binding_to_view_button_patch(binding=binding, button_id=button_id)
|
|
8526
8564
|
dto, binding_issues = _serialize_view_button_binding(
|
|
8527
8565
|
binding=view_binding,
|
|
8528
|
-
|
|
8566
|
+
fields_by_que_id=fields_by_que_id,
|
|
8529
8567
|
valid_custom_button_ids=valid_custom_button_ids,
|
|
8530
8568
|
allow_unverified_custom_button_id=allow_unverified_numeric_button_ids,
|
|
8531
8569
|
)
|
|
@@ -9455,49 +9493,37 @@ class AiBuilderFacade:
|
|
|
9455
9493
|
return fields_result
|
|
9456
9494
|
current_fields = fields_result.get("fields", [])
|
|
9457
9495
|
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
|
-
}
|
|
9496
|
+
fields_by_que_id = _collect_fields_by_que_id(current_fields)
|
|
9463
9497
|
upsert_views = [public_view_upsert_payload(view) for view in request.upsert_views]
|
|
9464
9498
|
patch_views = [public_view_partial_payload(patch) for patch in request.patch_views]
|
|
9465
9499
|
if request.preset is not None:
|
|
9466
9500
|
upsert_views = _build_views_preset(request.preset, list(field_names))
|
|
9467
9501
|
blocking_issues: list[dict[str, Any]] = []
|
|
9468
9502
|
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]
|
|
9503
|
+
# queId 形态:columns 是 int 数组,系统列 queId<=0 天然排除
|
|
9504
|
+
raw_columns = [que for que in (patch.get("columns") or []) if isinstance(que, int) and que > 0]
|
|
9505
|
+
columns = raw_columns
|
|
9506
|
+
missing_columns = [que for que in columns if que not in fields_by_que_id]
|
|
9481
9507
|
if missing_columns:
|
|
9482
9508
|
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
|
-
|
|
9509
|
+
group_que_id = patch.get("group_que_id")
|
|
9510
|
+
if group_que_id and group_que_id not in fields_by_que_id:
|
|
9511
|
+
blocking_issues.append({"error_code": "UNKNOWN_VIEW_FIELD", "view_name": patch.get("name"), "missing_fields": [group_que_id]})
|
|
9512
|
+
start_que_id = patch.get("start_que_id")
|
|
9513
|
+
end_que_id = patch.get("end_que_id")
|
|
9514
|
+
title_que_id = patch.get("title_que_id")
|
|
9489
9515
|
if patch.get("type") == "gantt":
|
|
9490
9516
|
missing_required = []
|
|
9491
|
-
if not
|
|
9492
|
-
missing_required.append("
|
|
9493
|
-
if not
|
|
9494
|
-
missing_required.append("
|
|
9517
|
+
if not start_que_id:
|
|
9518
|
+
missing_required.append("start_que_id")
|
|
9519
|
+
if not end_que_id:
|
|
9520
|
+
missing_required.append("end_que_id")
|
|
9495
9521
|
if missing_required:
|
|
9496
9522
|
blocking_issues.append({"error_code": "INVALID_GANTT_CONFIG", "view_name": patch.get("name"), "missing_fields": missing_required})
|
|
9497
|
-
missing_gantt_fields = [
|
|
9523
|
+
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
9524
|
if missing_gantt_fields:
|
|
9499
9525
|
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(
|
|
9526
|
+
translated_filters, filter_issues = _build_view_filter_groups(fields_by_que_id=fields_by_que_id, filters=patch.get("filters") or [])
|
|
9501
9527
|
if filter_issues:
|
|
9502
9528
|
blocking_issues.extend(
|
|
9503
9529
|
{
|
|
@@ -9505,11 +9531,12 @@ class AiBuilderFacade:
|
|
|
9505
9531
|
"view_name": patch.get("name"),
|
|
9506
9532
|
}
|
|
9507
9533
|
for issue in filter_issues
|
|
9534
|
+
if issue.get("error_code")
|
|
9508
9535
|
)
|
|
9509
9536
|
if translated_filters:
|
|
9510
9537
|
patch["filters"] = [dict(rule) for rule in (patch.get("filters") or [])]
|
|
9511
9538
|
_, _, query_condition_issues = _build_view_query_conditions_payload(
|
|
9512
|
-
|
|
9539
|
+
fields_by_que_id=fields_by_que_id,
|
|
9513
9540
|
query_conditions=patch.get("query_conditions"),
|
|
9514
9541
|
)
|
|
9515
9542
|
if query_condition_issues:
|
|
@@ -11796,6 +11823,26 @@ class AiBuilderFacade:
|
|
|
11796
11823
|
result["retry_payload"] = self._view_action_buttons_retry_payload(profile=profile, app_key=app_key, intents=retry_intents) if retry_intents else None
|
|
11797
11824
|
return result
|
|
11798
11825
|
|
|
11826
|
+
def _apply_view_create(self, *, profile: str, payload: JSONObject) -> str | None:
|
|
11827
|
+
"""创建一个视图并从后端响应中提取 view key。"""
|
|
11828
|
+
create_result = self._view_writer.create_view(profile=profile, payload=payload)
|
|
11829
|
+
raw_created = create_result.get("result")
|
|
11830
|
+
if isinstance(raw_created, dict):
|
|
11831
|
+
raw_view_key = raw_created.get("viewgraphKey") or raw_created.get("viewKey")
|
|
11832
|
+
return str(raw_view_key).strip() if raw_view_key is not None and str(raw_view_key).strip() else None
|
|
11833
|
+
if isinstance(raw_created, str):
|
|
11834
|
+
return raw_created.strip() or None
|
|
11835
|
+
return None
|
|
11836
|
+
|
|
11837
|
+
def _apply_view_update(self, *, profile: str, view_key: str, payload: JSONObject) -> None:
|
|
11838
|
+
"""更新一个已有视图。"""
|
|
11839
|
+
self.views.view_update(profile=profile, viewgraph_key=view_key, payload=payload)
|
|
11840
|
+
|
|
11841
|
+
def _apply_view_remove(self, *, profile: str, view_key: str) -> JSONObject:
|
|
11842
|
+
"""删除一个视图并执行单 view key 读回验证。"""
|
|
11843
|
+
self.views.view_delete(profile=profile, viewgraph_key=view_key)
|
|
11844
|
+
return self._verify_view_deleted_by_key(profile=profile, view_key=view_key)
|
|
11845
|
+
|
|
11799
11846
|
def app_views_apply(
|
|
11800
11847
|
self,
|
|
11801
11848
|
*,
|
|
@@ -11805,15 +11852,91 @@ class AiBuilderFacade:
|
|
|
11805
11852
|
patch_views: list[ViewPartialPatch] | None = None,
|
|
11806
11853
|
remove_views: list[str],
|
|
11807
11854
|
publish: bool = True,
|
|
11855
|
+
schema_version: str | None = None,
|
|
11808
11856
|
) -> JSONObject:
|
|
11809
11857
|
patch_views = patch_views or []
|
|
11858
|
+
operation_values = (
|
|
11859
|
+
("upsert", "upsert_views", upsert_views, "VIEW_BATCH_CREATE_UNSUPPORTED"),
|
|
11860
|
+
("patch", "patch_views", patch_views, "VIEW_BATCH_OPERATION_UNSUPPORTED"),
|
|
11861
|
+
("remove", "remove_views", remove_views, "VIEW_BATCH_OPERATION_UNSUPPORTED"),
|
|
11862
|
+
)
|
|
11863
|
+
selected_operation_types = [operation for operation, _field_name, values, _error_code in operation_values if values]
|
|
11864
|
+
selected_operations = [field_name for _operation, field_name, values, _error_code in operation_values if values]
|
|
11810
11865
|
normalized_args = {
|
|
11811
11866
|
"app_key": app_key,
|
|
11812
|
-
"
|
|
11867
|
+
"upsert_view": public_view_upsert_payload(upsert_views[0]) if upsert_views else None,
|
|
11813
11868
|
"patch_views": [public_view_partial_payload(patch) for patch in patch_views],
|
|
11814
11869
|
"remove_views": list(remove_views),
|
|
11815
11870
|
"publish": publish,
|
|
11871
|
+
"schema_version": schema_version,
|
|
11816
11872
|
}
|
|
11873
|
+
if len(selected_operations) > 1:
|
|
11874
|
+
return _failed(
|
|
11875
|
+
"VIEW_SINGLE_OPERATION_UNSUPPORTED",
|
|
11876
|
+
"app_views_apply supports one view operation per call; choose upsert_view, patch_views, or remove_views",
|
|
11877
|
+
normalized_args=normalized_args,
|
|
11878
|
+
details={"app_key": app_key, "operations": selected_operations},
|
|
11879
|
+
suggested_next_call=None,
|
|
11880
|
+
)
|
|
11881
|
+
operation = selected_operation_types[0] if selected_operation_types else None
|
|
11882
|
+
for operation_name, _field_name, values, error_code in operation_values:
|
|
11883
|
+
if len(values) <= 1:
|
|
11884
|
+
continue
|
|
11885
|
+
first_value = values[0]
|
|
11886
|
+
if operation_name == "upsert":
|
|
11887
|
+
suggested_value: Any = public_view_upsert_payload(first_value)
|
|
11888
|
+
elif operation_name == "patch":
|
|
11889
|
+
suggested_value = [public_view_partial_payload(first_value)]
|
|
11890
|
+
else:
|
|
11891
|
+
suggested_value = [str(first_value)]
|
|
11892
|
+
argument_name = "upsert_view" if operation_name == "upsert" else f"{operation_name}_views"
|
|
11893
|
+
suggested_arguments: JSONObject = {
|
|
11894
|
+
"profile": profile,
|
|
11895
|
+
"app_key": app_key,
|
|
11896
|
+
argument_name: suggested_value,
|
|
11897
|
+
"publish": publish,
|
|
11898
|
+
}
|
|
11899
|
+
if schema_version is not None:
|
|
11900
|
+
suggested_arguments["schema_version"] = schema_version
|
|
11901
|
+
return _failed(
|
|
11902
|
+
error_code,
|
|
11903
|
+
(
|
|
11904
|
+
"app_views_apply supports creating or fully updating one view per call"
|
|
11905
|
+
if operation_name == "upsert"
|
|
11906
|
+
else f"app_views_apply supports at most one {operation_name} per call"
|
|
11907
|
+
),
|
|
11908
|
+
normalized_args=normalized_args,
|
|
11909
|
+
details={
|
|
11910
|
+
"app_key": app_key,
|
|
11911
|
+
f"max_{operation_name}_views": 1,
|
|
11912
|
+
f"received_{operation_name}_views": len(values),
|
|
11913
|
+
},
|
|
11914
|
+
suggested_next_call={
|
|
11915
|
+
"tool_name": "app_views_apply",
|
|
11916
|
+
"arguments": suggested_arguments,
|
|
11917
|
+
},
|
|
11918
|
+
)
|
|
11919
|
+
schema_context = None
|
|
11920
|
+
if schema_version is not None:
|
|
11921
|
+
try:
|
|
11922
|
+
schema_context = views_schema_registry().resolve_schema_version(schema_version)
|
|
11923
|
+
for upsert in upsert_views:
|
|
11924
|
+
schema_context.validate_upsert(
|
|
11925
|
+
compact_none_values(public_view_upsert_payload(upsert))
|
|
11926
|
+
)
|
|
11927
|
+
except ViewsContractError as error:
|
|
11928
|
+
return _failed(
|
|
11929
|
+
error.error_code,
|
|
11930
|
+
str(error.message),
|
|
11931
|
+
normalized_args=normalized_args,
|
|
11932
|
+
details={
|
|
11933
|
+
"app_key": app_key,
|
|
11934
|
+
"schema_version": schema_version,
|
|
11935
|
+
"reason_path": error.path or "",
|
|
11936
|
+
**(error.details or {}),
|
|
11937
|
+
},
|
|
11938
|
+
suggested_next_call={"tool_name": "app_views_schema", "arguments": {"schema_version": schema_version, "schema_kind": "complete"}},
|
|
11939
|
+
)
|
|
11817
11940
|
apply_started_at = time.perf_counter()
|
|
11818
11941
|
permission_ms = 0
|
|
11819
11942
|
view_state_read_ms = 0
|
|
@@ -11828,6 +11951,8 @@ class AiBuilderFacade:
|
|
|
11828
11951
|
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
11829
11952
|
|
|
11830
11953
|
def finalize(response: JSONObject) -> JSONObject:
|
|
11954
|
+
if schema_context is not None:
|
|
11955
|
+
response["schemaVersion"] = schema_context.schema_version
|
|
11831
11956
|
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
11832
11957
|
|
|
11833
11958
|
def attach_duration(response: JSONObject) -> JSONObject:
|
|
@@ -12044,7 +12169,25 @@ class AiBuilderFacade:
|
|
|
12044
12169
|
return finish(create_permission_outcome.block)
|
|
12045
12170
|
permission_outcomes.append(create_permission_outcome)
|
|
12046
12171
|
parsed_schema = _parse_schema(schema)
|
|
12047
|
-
|
|
12172
|
+
|
|
12173
|
+
def _register_subfield_names(field: dict[str, Any], index: dict[str, dict[str, Any]]) -> None:
|
|
12174
|
+
# 表格字段(queType 18)的子字段是视图列/筛选/排序的合法引用面(viewgraph_question 有独立行);
|
|
12175
|
+
# _parse_schema 只把顶层字段入列,这里递归并入引用索引(不改 fields 布局面)
|
|
12176
|
+
for sub in field.get("subfields") or []:
|
|
12177
|
+
if isinstance(sub, dict) and str(sub.get("name") or "").strip():
|
|
12178
|
+
index.setdefault(str(sub["name"]), sub)
|
|
12179
|
+
_register_subfield_names(sub, index)
|
|
12180
|
+
|
|
12181
|
+
field_name_to_field: dict[str, dict[str, Any]] = {}
|
|
12182
|
+
for field in parsed_schema["fields"]:
|
|
12183
|
+
if isinstance(field, dict) and str(field.get("name") or "").strip():
|
|
12184
|
+
field_name_to_field[str(field["name"])] = field
|
|
12185
|
+
_register_subfield_names(field, field_name_to_field)
|
|
12186
|
+
field_names = set(field_name_to_field)
|
|
12187
|
+
# views 契约全 queId 化后,列/筛选/排序/配置引用的是 queId(int);
|
|
12188
|
+
# fields_by_que_id 必须以 que_id 为键,与 _build_view_filter_groups 等消费方对齐。
|
|
12189
|
+
# 递归收集(顶层+表格子字段;段落内字段经 _parse_schema 已展平到 fields)。
|
|
12190
|
+
fields_by_que_id = _collect_fields_by_que_id([], schema=schema)
|
|
12048
12191
|
if patch_views:
|
|
12049
12192
|
expanded_views, patch_issues, patch_results = self._expand_view_partial_patches(
|
|
12050
12193
|
profile=profile,
|
|
@@ -12064,14 +12207,18 @@ class AiBuilderFacade:
|
|
|
12064
12207
|
suggested_next_call={"tool_name": "view_get", "arguments": {"profile": profile, "view_key": patch_issues[0].get("view_key") or "VIEW_KEY"}},
|
|
12065
12208
|
)
|
|
12066
12209
|
)
|
|
12067
|
-
|
|
12068
|
-
|
|
12210
|
+
if len(expanded_views) > 1:
|
|
12211
|
+
return finalize(_failed(
|
|
12212
|
+
"VIEW_BATCH_OPERATION_UNSUPPORTED",
|
|
12213
|
+
"app_views_apply supports hydrating one patch view per call",
|
|
12214
|
+
normalized_args=normalized_args,
|
|
12215
|
+
details={"app_key": app_key, "received_patch_views": len(expanded_views)},
|
|
12216
|
+
suggested_next_call=None,
|
|
12217
|
+
))
|
|
12218
|
+
upsert_views = expanded_views
|
|
12219
|
+
operation = "upsert" if expanded_views else "patch"
|
|
12220
|
+
normalized_args["upsert_view"] = public_view_upsert_payload(expanded_views[0]) if expanded_views else None
|
|
12069
12221
|
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
12222
|
requires_custom_button_validation = any(
|
|
12076
12223
|
any(binding.button_type == PublicViewButtonType.custom for binding in (patch.buttons or []))
|
|
12077
12224
|
for patch in upsert_views
|
|
@@ -12167,8 +12314,8 @@ class AiBuilderFacade:
|
|
|
12167
12314
|
associated_resource_inventory_ms += _duration_ms(associated_resource_inventory_started_at)
|
|
12168
12315
|
removed: list[str] = []
|
|
12169
12316
|
removed_keys: set[str] = set()
|
|
12170
|
-
|
|
12171
|
-
|
|
12317
|
+
view_result: dict[str, Any] | None = None
|
|
12318
|
+
failure_result: dict[str, Any] | None = None
|
|
12172
12319
|
action_button_intents: list[dict[str, Any]] = list(patch_action_button_intents)
|
|
12173
12320
|
|
|
12174
12321
|
def record_action_button_intent(*, patch: ViewUpsertPatch, view_key: str | None, index: int) -> None:
|
|
@@ -12186,10 +12333,17 @@ class AiBuilderFacade:
|
|
|
12186
12333
|
)
|
|
12187
12334
|
|
|
12188
12335
|
view_mutation_started_at = time.perf_counter()
|
|
12189
|
-
|
|
12190
|
-
selector_text = str(
|
|
12336
|
+
if operation == "remove" and remove_views:
|
|
12337
|
+
selector_text = str(remove_views[0] or "").strip()
|
|
12191
12338
|
if not selector_text:
|
|
12192
|
-
|
|
12339
|
+
view_mutation_ms += _duration_ms(view_mutation_started_at)
|
|
12340
|
+
return finish(_failed(
|
|
12341
|
+
"VIEW_NOT_FOUND",
|
|
12342
|
+
"remove_views item did not match an existing view name or view_key",
|
|
12343
|
+
normalized_args=normalized_args,
|
|
12344
|
+
details={"app_key": app_key},
|
|
12345
|
+
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
12346
|
+
))
|
|
12193
12347
|
key_match = existing_by_key.get(selector_text)
|
|
12194
12348
|
matches = [key_match] if isinstance(key_match, dict) else existing_by_name.get(selector_text, [])
|
|
12195
12349
|
if len(matches) > 1:
|
|
@@ -12216,44 +12370,40 @@ class AiBuilderFacade:
|
|
|
12216
12370
|
"error_code": "VIEW_NOT_FOUND",
|
|
12217
12371
|
"message": "remove_views item did not match an existing view name or view_key",
|
|
12218
12372
|
}
|
|
12219
|
-
|
|
12220
|
-
|
|
12221
|
-
|
|
12222
|
-
if len(matches) == 1:
|
|
12373
|
+
failure_result = failed_view
|
|
12374
|
+
view_result = deepcopy(failed_view)
|
|
12375
|
+
else:
|
|
12223
12376
|
key = _extract_view_key(matches[0])
|
|
12224
12377
|
removed_name = _extract_view_name(matches[0]) or selector_text
|
|
12225
12378
|
try:
|
|
12226
|
-
self.
|
|
12227
|
-
delete_readback = self._verify_view_deleted_by_key(profile=profile, view_key=key)
|
|
12379
|
+
delete_readback = self._apply_view_remove(profile=profile, view_key=key)
|
|
12228
12380
|
removed.append(removed_name)
|
|
12229
12381
|
if key:
|
|
12230
12382
|
removed_keys.add(key)
|
|
12231
12383
|
existing_by_key.pop(key, None)
|
|
12232
12384
|
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
|
-
)
|
|
12385
|
+
view_result = {
|
|
12386
|
+
"name": removed_name,
|
|
12387
|
+
"view_key": key,
|
|
12388
|
+
"type": None,
|
|
12389
|
+
"status": delete_readback.get("status") or "readback_pending",
|
|
12390
|
+
"operation": "delete",
|
|
12391
|
+
"delete_executed": True,
|
|
12392
|
+
"readback_status": delete_readback.get("readback_status"),
|
|
12393
|
+
"safe_to_retry_delete": False,
|
|
12394
|
+
**(
|
|
12395
|
+
{
|
|
12396
|
+
"error_code": delete_readback.get("error_code"),
|
|
12397
|
+
"message": delete_readback.get("message"),
|
|
12398
|
+
"request_id": delete_readback.get("request_id"),
|
|
12399
|
+
"backend_code": delete_readback.get("backend_code"),
|
|
12400
|
+
"http_status": delete_readback.get("http_status"),
|
|
12401
|
+
"transport_error": delete_readback.get("transport_error"),
|
|
12402
|
+
}
|
|
12403
|
+
if delete_readback.get("readback_status") != "deleted"
|
|
12404
|
+
else {}
|
|
12405
|
+
),
|
|
12406
|
+
}
|
|
12257
12407
|
except (QingflowApiError, RuntimeError) as error:
|
|
12258
12408
|
api_error = _coerce_api_error(error)
|
|
12259
12409
|
failed_view = {
|
|
@@ -12270,8 +12420,8 @@ class AiBuilderFacade:
|
|
|
12270
12420
|
"backend_code": api_error.backend_code,
|
|
12271
12421
|
"http_status": None if api_error.http_status == 404 else api_error.http_status,
|
|
12272
12422
|
}
|
|
12273
|
-
|
|
12274
|
-
|
|
12423
|
+
failure_result = failed_view
|
|
12424
|
+
view_result = deepcopy(failed_view)
|
|
12275
12425
|
created: list[str] = []
|
|
12276
12426
|
updated: list[str] = []
|
|
12277
12427
|
existing_view_list = [
|
|
@@ -12281,95 +12431,129 @@ class AiBuilderFacade:
|
|
|
12281
12431
|
and _extract_view_name(view) not in removed
|
|
12282
12432
|
and _extract_view_key(view) not in removed_keys
|
|
12283
12433
|
]
|
|
12284
|
-
|
|
12285
|
-
|
|
12286
|
-
|
|
12287
|
-
name
|
|
12288
|
-
for name in [str(value or "").strip() for value in patch.columns]
|
|
12289
|
-
if name in _KNOWN_SYSTEM_VIEW_COLUMNS
|
|
12290
|
-
]
|
|
12434
|
+
stale_warnings: list[dict[str, Any]] = []
|
|
12435
|
+
for ordinal, patch in ((1, upsert_views[0]),) if operation == "upsert" and upsert_views else ():
|
|
12436
|
+
apply_columns = _resolve_view_visible_que_ids(patch)
|
|
12291
12437
|
if patch.type in {PublicViewType.table, PublicViewType.card} and patch.columns and not apply_columns:
|
|
12292
12438
|
return _failed(
|
|
12293
12439
|
"VALIDATION_ERROR",
|
|
12294
12440
|
"view columns must include at least one real app field; system columns cannot be applied directly",
|
|
12295
12441
|
normalized_args=normalized_args,
|
|
12296
|
-
details={
|
|
12297
|
-
"app_key": app_key,
|
|
12298
|
-
"view_name": patch.name,
|
|
12299
|
-
"ignored_system_columns": ignored_system_columns,
|
|
12300
|
-
},
|
|
12442
|
+
details={"app_key": app_key, "view_name": patch.name},
|
|
12301
12443
|
)
|
|
12302
|
-
|
|
12444
|
+
# 死引用(字段已从表单删除,后端保存视图时原样透传)降级为警告透传,不再整视图拒绝
|
|
12445
|
+
missing_columns = [
|
|
12446
|
+
que for que in apply_columns
|
|
12447
|
+
if not _is_known_system_view_que_id(que) and que not in fields_by_que_id
|
|
12448
|
+
]
|
|
12303
12449
|
if missing_columns:
|
|
12304
|
-
|
|
12305
|
-
"
|
|
12306
|
-
"view columns reference unknown fields",
|
|
12307
|
-
normalized_args=normalized_args,
|
|
12308
|
-
details={
|
|
12309
|
-
"app_key": app_key,
|
|
12310
|
-
"view_name": patch.name,
|
|
12311
|
-
"missing_fields": missing_columns,
|
|
12312
|
-
"ignored_system_columns": ignored_system_columns,
|
|
12313
|
-
},
|
|
12314
|
-
missing_fields=missing_columns,
|
|
12315
|
-
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
12450
|
+
stale_warnings.append(
|
|
12451
|
+
_stale_view_reference_warning(scope="columns", view_name=patch.name, field_ids=missing_columns)
|
|
12316
12452
|
)
|
|
12317
|
-
if patch.
|
|
12318
|
-
|
|
12319
|
-
"
|
|
12320
|
-
f"group_by references unknown field '{patch.group_by}'",
|
|
12321
|
-
normalized_args=normalized_args,
|
|
12322
|
-
details={
|
|
12323
|
-
"app_key": app_key,
|
|
12324
|
-
"view_name": patch.name,
|
|
12325
|
-
"missing_fields": [patch.group_by],
|
|
12326
|
-
},
|
|
12327
|
-
missing_fields=[patch.group_by],
|
|
12328
|
-
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
12453
|
+
if patch.group_que_id and patch.group_que_id not in fields_by_que_id:
|
|
12454
|
+
stale_warnings.append(
|
|
12455
|
+
_stale_view_reference_warning(scope="group_que_id", view_name=patch.name, field_ids=[patch.group_que_id])
|
|
12329
12456
|
)
|
|
12330
|
-
for
|
|
12331
|
-
|
|
12332
|
-
|
|
12333
|
-
|
|
12334
|
-
|
|
12335
|
-
|
|
12336
|
-
|
|
12337
|
-
|
|
12338
|
-
|
|
12339
|
-
"missing_fields": [gantt_field_name],
|
|
12340
|
-
},
|
|
12341
|
-
missing_fields=[gantt_field_name],
|
|
12342
|
-
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
12457
|
+
for ref_scope, ref_que_id in (
|
|
12458
|
+
("gantt.start_que_id", patch.start_que_id),
|
|
12459
|
+
("gantt.end_que_id", patch.end_que_id),
|
|
12460
|
+
("title_que_id", patch.title_que_id),
|
|
12461
|
+
):
|
|
12462
|
+
# 系统字段(提交人/提交时间等)可作标题/时间轴;业务字段死引用仅警告透传
|
|
12463
|
+
if ref_que_id and not _is_known_system_view_que_id(ref_que_id) and ref_que_id not in fields_by_que_id:
|
|
12464
|
+
stale_warnings.append(
|
|
12465
|
+
_stale_view_reference_warning(scope=ref_scope, view_name=patch.name, field_ids=[ref_que_id])
|
|
12343
12466
|
)
|
|
12467
|
+
for group_name, config_que_ids in _iter_view_config_group_field_names(patch):
|
|
12468
|
+
missing_config_fields = [
|
|
12469
|
+
que for que in config_que_ids
|
|
12470
|
+
if que not in fields_by_que_id and not _is_known_system_view_que_id(que)
|
|
12471
|
+
]
|
|
12472
|
+
if missing_config_fields:
|
|
12473
|
+
stale_warnings.append(
|
|
12474
|
+
_stale_view_reference_warning(scope=group_name, view_name=patch.name, field_ids=missing_config_fields)
|
|
12475
|
+
)
|
|
12476
|
+
hierarchy_root_filter_groups: list[list[dict[str, Any]]] | None = None
|
|
12477
|
+
if patch.hierarchy is not None and patch.hierarchy.root_filter:
|
|
12478
|
+
hierarchy_root_filter_groups = []
|
|
12479
|
+
for root_filter_group in patch.hierarchy.root_filter:
|
|
12480
|
+
translated_group, hierarchy_filter_issues = _build_view_filter_groups(
|
|
12481
|
+
fields_by_que_id=fields_by_que_id,
|
|
12482
|
+
filters=root_filter_group,
|
|
12483
|
+
)
|
|
12484
|
+
for issue in hierarchy_filter_issues:
|
|
12485
|
+
if issue.get("warning_code") == "STALE_OPTION_REFERENCE":
|
|
12486
|
+
stale_warnings.append(
|
|
12487
|
+
_stale_view_reference_warning(
|
|
12488
|
+
scope="hierarchy.root_filter",
|
|
12489
|
+
view_name=patch.name,
|
|
12490
|
+
values=issue.get("details", {}).get("unresolved_values") or [],
|
|
12491
|
+
option=True,
|
|
12492
|
+
)
|
|
12493
|
+
)
|
|
12494
|
+
hierarchy_filter_errors = [issue for issue in hierarchy_filter_issues if issue.get("error_code")]
|
|
12495
|
+
if hierarchy_filter_errors:
|
|
12496
|
+
first_issue = hierarchy_filter_errors[0]
|
|
12497
|
+
return _failed(
|
|
12498
|
+
str(first_issue.get("error_code") or "UNKNOWN_VIEW_FIELD"),
|
|
12499
|
+
"view hierarchy root_filter references invalid fields or values",
|
|
12500
|
+
normalized_args=normalized_args,
|
|
12501
|
+
details={
|
|
12502
|
+
"app_key": app_key,
|
|
12503
|
+
"view_name": patch.name,
|
|
12504
|
+
**first_issue,
|
|
12505
|
+
},
|
|
12506
|
+
missing_fields=list(first_issue.get("missing_fields") or []),
|
|
12507
|
+
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
12508
|
+
)
|
|
12509
|
+
hierarchy_root_filter_groups.extend(translated_group)
|
|
12344
12510
|
translated_filters: list[list[dict[str, Any]]] | None
|
|
12345
12511
|
filter_issues: list[dict[str, Any]]
|
|
12346
12512
|
if patch.preserve_filters:
|
|
12347
12513
|
translated_filters = None
|
|
12348
12514
|
filter_issues = []
|
|
12515
|
+
elif patch.filters and isinstance(patch.filters[0], list):
|
|
12516
|
+
# 嵌套形态 [[rule,...],[rule,...]]:组间 OR、组内 AND(viewgraphLimit 多组),
|
|
12517
|
+
# 复用 button_limit 的多组编译;后端简易模式会从多组规则自动派生公式
|
|
12518
|
+
translated_filters, filter_issues = _build_view_button_limit_groups(
|
|
12519
|
+
fields_by_que_id=fields_by_que_id, groups=patch.filters
|
|
12520
|
+
)
|
|
12349
12521
|
else:
|
|
12350
|
-
translated_filters, filter_issues = _build_view_filter_groups(
|
|
12522
|
+
translated_filters, filter_issues = _build_view_filter_groups(fields_by_que_id=fields_by_que_id, filters=patch.filters)
|
|
12351
12523
|
if filter_issues:
|
|
12352
|
-
|
|
12353
|
-
|
|
12354
|
-
|
|
12355
|
-
|
|
12356
|
-
|
|
12357
|
-
|
|
12358
|
-
|
|
12359
|
-
|
|
12360
|
-
|
|
12361
|
-
|
|
12362
|
-
|
|
12363
|
-
|
|
12364
|
-
|
|
12365
|
-
|
|
12524
|
+
for issue in filter_issues:
|
|
12525
|
+
if issue.get("warning_code") == "STALE_OPTION_REFERENCE":
|
|
12526
|
+
stale_warnings.append(
|
|
12527
|
+
_stale_view_reference_warning(
|
|
12528
|
+
scope="filters",
|
|
12529
|
+
view_name=patch.name,
|
|
12530
|
+
values=issue.get("details", {}).get("unresolved_values") or [],
|
|
12531
|
+
option=True,
|
|
12532
|
+
)
|
|
12533
|
+
)
|
|
12534
|
+
filter_errors = [issue for issue in filter_issues if issue.get("error_code")]
|
|
12535
|
+
if filter_errors:
|
|
12536
|
+
first_issue = filter_errors[0]
|
|
12537
|
+
return _failed(
|
|
12538
|
+
str(first_issue.get("error_code") or "UNKNOWN_VIEW_FIELD"),
|
|
12539
|
+
"view filters reference invalid fields or values",
|
|
12540
|
+
normalized_args=normalized_args,
|
|
12541
|
+
details={
|
|
12542
|
+
"app_key": app_key,
|
|
12543
|
+
"view_name": patch.name,
|
|
12544
|
+
**first_issue,
|
|
12545
|
+
},
|
|
12546
|
+
missing_fields=list(first_issue.get("missing_fields") or []),
|
|
12547
|
+
allowed_values=first_issue.get("allowed_values") or {"view_types": [member.value for member in PublicViewType], "view.filter.operator": [member.value for member in ViewFilterOperator]},
|
|
12548
|
+
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
12549
|
+
)
|
|
12366
12550
|
if patch.preserve_query_conditions:
|
|
12367
12551
|
query_condition_payload = None
|
|
12368
12552
|
expected_query_conditions = None
|
|
12369
12553
|
query_condition_issues = []
|
|
12370
12554
|
else:
|
|
12371
12555
|
query_condition_payload, expected_query_conditions, query_condition_issues = _build_view_query_conditions_payload(
|
|
12372
|
-
|
|
12556
|
+
fields_by_que_id=fields_by_que_id,
|
|
12373
12557
|
query_conditions=patch.query_conditions,
|
|
12374
12558
|
)
|
|
12375
12559
|
if query_condition_issues:
|
|
@@ -12426,7 +12610,7 @@ class AiBuilderFacade:
|
|
|
12426
12610
|
expected_button_summary = None
|
|
12427
12611
|
elif patch.buttons is not None:
|
|
12428
12612
|
explicit_button_dtos, button_issues = _build_view_button_dtos(
|
|
12429
|
-
|
|
12613
|
+
fields_by_que_id=fields_by_que_id,
|
|
12430
12614
|
bindings=patch.buttons,
|
|
12431
12615
|
valid_custom_button_ids=valid_custom_button_ids,
|
|
12432
12616
|
)
|
|
@@ -12443,7 +12627,10 @@ class AiBuilderFacade:
|
|
|
12443
12627
|
},
|
|
12444
12628
|
missing_fields=list(first_issue.get("missing_fields") or []),
|
|
12445
12629
|
allowed_values=first_issue.get("allowed_values") or {"view_types": [member.value for member in PublicViewType], "view.filter.operator": [member.value for member in ViewFilterOperator]},
|
|
12446
|
-
suggested_next_call={
|
|
12630
|
+
suggested_next_call={
|
|
12631
|
+
"tool_name": "app_get_buttons",
|
|
12632
|
+
"arguments": {"profile": profile, "app_key": app_key},
|
|
12633
|
+
},
|
|
12447
12634
|
)
|
|
12448
12635
|
expected_button_summary = _normalize_expected_view_buttons_for_compare(
|
|
12449
12636
|
explicit_button_dtos or [],
|
|
@@ -12484,6 +12671,10 @@ class AiBuilderFacade:
|
|
|
12484
12671
|
existing_key = _extract_view_key(matched_existing_view)
|
|
12485
12672
|
created_key: str | None = None
|
|
12486
12673
|
system_view_list_type = _resolve_system_view_list_type(view_key=existing_key, view_name=patch.name) if existing_key else None
|
|
12674
|
+
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"}:
|
|
12675
|
+
# 环境受限开关:默认视图列序同步依赖 app apply/baseInfo 回读,在快照/脱敏环境上该读面可能不可用;
|
|
12676
|
+
# 视图写主体(view_update)不受影响,仅跳过同步与尾部校验。
|
|
12677
|
+
system_view_list_type = None
|
|
12487
12678
|
operation_phase = "view_update" if existing_key else "view_create"
|
|
12488
12679
|
query_condition_payload_for_apply = deepcopy(query_condition_payload)
|
|
12489
12680
|
expected_query_conditions_for_verify = deepcopy(expected_query_conditions)
|
|
@@ -12517,16 +12708,17 @@ class AiBuilderFacade:
|
|
|
12517
12708
|
views=self.views,
|
|
12518
12709
|
profile=profile,
|
|
12519
12710
|
source_viewgraph_key=existing_key,
|
|
12711
|
+
hierarchy_filter_groups=hierarchy_root_filter_groups,
|
|
12520
12712
|
schema=schema,
|
|
12521
12713
|
patch=patch,
|
|
12522
12714
|
view_filters=translated_filters,
|
|
12523
|
-
|
|
12715
|
+
fields_by_que_id=fields_by_que_id,
|
|
12524
12716
|
auth_override=view_auth_override,
|
|
12525
12717
|
explicit_button_dtos=explicit_button_dtos,
|
|
12526
12718
|
query_condition_payload=query_condition_payload_for_apply,
|
|
12527
12719
|
associated_resources_payload=associated_resources_payload_for_apply,
|
|
12528
12720
|
)
|
|
12529
|
-
self.
|
|
12721
|
+
self._apply_view_update(profile=profile, view_key=existing_key, payload=payload)
|
|
12530
12722
|
system_view_sync: dict[str, Any] | None = None
|
|
12531
12723
|
if system_view_list_type is not None and patch.type.value == "table":
|
|
12532
12724
|
operation_phase = "default_view_apply_config_sync"
|
|
@@ -12537,7 +12729,9 @@ class AiBuilderFacade:
|
|
|
12537
12729
|
payload=payload,
|
|
12538
12730
|
list_type=system_view_list_type,
|
|
12539
12731
|
schema=schema,
|
|
12540
|
-
visible_field_names=
|
|
12732
|
+
visible_field_names=[
|
|
12733
|
+
str((fields_by_que_id.get(que) or {}).get("name") or "") for que in apply_columns if que in fields_by_que_id
|
|
12734
|
+
],
|
|
12541
12735
|
)
|
|
12542
12736
|
if not bool(system_view_sync.get("verified")):
|
|
12543
12737
|
failure_entry = {
|
|
@@ -12562,24 +12756,23 @@ class AiBuilderFacade:
|
|
|
12562
12756
|
"apply_columns": apply_columns,
|
|
12563
12757
|
},
|
|
12564
12758
|
}
|
|
12565
|
-
|
|
12566
|
-
|
|
12759
|
+
failure_result = failure_entry
|
|
12760
|
+
view_result = failure_entry
|
|
12567
12761
|
continue
|
|
12568
12762
|
updated.append(patch.name)
|
|
12569
|
-
|
|
12570
|
-
|
|
12571
|
-
|
|
12572
|
-
|
|
12573
|
-
|
|
12574
|
-
|
|
12575
|
-
|
|
12576
|
-
|
|
12577
|
-
|
|
12578
|
-
|
|
12579
|
-
|
|
12580
|
-
|
|
12581
|
-
|
|
12582
|
-
)
|
|
12763
|
+
view_result = {
|
|
12764
|
+
"name": patch.name,
|
|
12765
|
+
"view_key": existing_key,
|
|
12766
|
+
"type": patch.type.value,
|
|
12767
|
+
"status": "updated",
|
|
12768
|
+
"expected_filters": deepcopy(translated_filters),
|
|
12769
|
+
"expected_buttons": deepcopy(expected_button_summary),
|
|
12770
|
+
"expected_query_conditions": deepcopy(expected_query_conditions_for_verify),
|
|
12771
|
+
"expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
|
|
12772
|
+
"expected_pinned": patch.pinned,
|
|
12773
|
+
"system_view_sync": system_view_sync,
|
|
12774
|
+
"apply_columns": deepcopy(apply_columns),
|
|
12775
|
+
}
|
|
12583
12776
|
record_action_button_intent(patch=patch, view_key=existing_key, index=ordinal - 1)
|
|
12584
12777
|
else:
|
|
12585
12778
|
template_key = _pick_view_template_key(existing_view_list, desired_type=patch.type.value)
|
|
@@ -12591,50 +12784,45 @@ class AiBuilderFacade:
|
|
|
12591
12784
|
views=self.views,
|
|
12592
12785
|
profile=profile,
|
|
12593
12786
|
source_viewgraph_key=created_key,
|
|
12787
|
+
hierarchy_filter_groups=hierarchy_root_filter_groups,
|
|
12594
12788
|
schema=schema,
|
|
12595
12789
|
patch=patch,
|
|
12596
12790
|
view_filters=translated_filters,
|
|
12597
|
-
|
|
12791
|
+
fields_by_que_id=fields_by_que_id,
|
|
12598
12792
|
auth_override=view_auth_override,
|
|
12599
12793
|
explicit_button_dtos=explicit_button_dtos,
|
|
12600
12794
|
query_condition_payload=query_condition_payload_for_apply,
|
|
12601
12795
|
associated_resources_payload=associated_resources_payload_for_apply,
|
|
12602
12796
|
)
|
|
12603
|
-
self.
|
|
12797
|
+
self._apply_view_update(profile=profile, view_key=created_key, payload=payload)
|
|
12604
12798
|
else:
|
|
12605
|
-
payload =
|
|
12799
|
+
payload = _build_view_creation_payload(
|
|
12606
12800
|
app_key=app_key,
|
|
12607
12801
|
base_info=base,
|
|
12608
12802
|
schema=schema,
|
|
12609
12803
|
patch=patch,
|
|
12610
12804
|
ordinal=ordinal,
|
|
12611
12805
|
view_filters=translated_filters,
|
|
12612
|
-
|
|
12806
|
+
fields_by_que_id=fields_by_que_id,
|
|
12613
12807
|
auth_override=view_auth_override,
|
|
12614
12808
|
explicit_button_dtos=explicit_button_dtos,
|
|
12615
12809
|
query_condition_payload=query_condition_payload_for_apply,
|
|
12616
12810
|
associated_resources_payload=associated_resources_payload_for_apply,
|
|
12811
|
+
hierarchy_filter_groups=hierarchy_root_filter_groups,
|
|
12617
12812
|
)
|
|
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
|
|
12813
|
+
created_key = self._apply_view_create(profile=profile, payload=payload)
|
|
12625
12814
|
created.append(patch.name)
|
|
12626
|
-
|
|
12627
|
-
|
|
12628
|
-
|
|
12629
|
-
|
|
12630
|
-
|
|
12631
|
-
|
|
12632
|
-
|
|
12633
|
-
|
|
12634
|
-
|
|
12635
|
-
|
|
12636
|
-
|
|
12637
|
-
)
|
|
12815
|
+
view_result = {
|
|
12816
|
+
"name": patch.name,
|
|
12817
|
+
"view_key": created_key,
|
|
12818
|
+
"type": patch.type.value,
|
|
12819
|
+
"status": "created",
|
|
12820
|
+
"expected_filters": deepcopy(translated_filters),
|
|
12821
|
+
"expected_buttons": deepcopy(expected_button_summary),
|
|
12822
|
+
"expected_query_conditions": deepcopy(expected_query_conditions_for_verify),
|
|
12823
|
+
"expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
|
|
12824
|
+
"expected_pinned": patch.pinned,
|
|
12825
|
+
}
|
|
12638
12826
|
record_action_button_intent(patch=patch, view_key=created_key, index=ordinal - 1)
|
|
12639
12827
|
except (QingflowApiError, RuntimeError) as error:
|
|
12640
12828
|
api_error = _coerce_api_error(error)
|
|
@@ -12661,17 +12849,20 @@ class AiBuilderFacade:
|
|
|
12661
12849
|
patch=patch,
|
|
12662
12850
|
ordinal=ordinal,
|
|
12663
12851
|
view_filters=translated_filters,
|
|
12664
|
-
|
|
12852
|
+
hierarchy_filter_groups=hierarchy_root_filter_groups,
|
|
12853
|
+
fields_by_que_id=fields_by_que_id,
|
|
12665
12854
|
auth_override=view_auth_override,
|
|
12666
12855
|
explicit_button_dtos=fallback_button_dtos,
|
|
12667
12856
|
query_condition_payload=query_condition_payload_for_apply,
|
|
12668
12857
|
associated_resources_payload=associated_resources_payload_for_apply,
|
|
12669
12858
|
)
|
|
12670
|
-
self.
|
|
12859
|
+
self._apply_view_update(profile=profile, view_key=target_key, payload=fallback_payload)
|
|
12671
12860
|
system_view_sync: dict[str, Any] | None = None
|
|
12672
12861
|
fallback_system_view_list_type = (
|
|
12673
12862
|
_resolve_system_view_list_type(view_key=target_key, view_name=patch.name)
|
|
12674
|
-
if patch.type.value == "table"
|
|
12863
|
+
if patch.type.value == "table"
|
|
12864
|
+
and target_key
|
|
12865
|
+
and os.environ.get("QINGFLOW_VIEWS_APPLY_SKIP_SYSTEM_VIEW_SYNC", "").strip().lower() not in {"1", "true", "yes", "on"}
|
|
12675
12866
|
else None
|
|
12676
12867
|
)
|
|
12677
12868
|
if fallback_system_view_list_type is not None:
|
|
@@ -12683,7 +12874,9 @@ class AiBuilderFacade:
|
|
|
12683
12874
|
payload=fallback_payload,
|
|
12684
12875
|
list_type=fallback_system_view_list_type,
|
|
12685
12876
|
schema=schema,
|
|
12686
|
-
visible_field_names=
|
|
12877
|
+
visible_field_names=[
|
|
12878
|
+
str((fields_by_que_id.get(que) or {}).get("name") or "") for que in apply_columns if que in fields_by_que_id
|
|
12879
|
+
],
|
|
12687
12880
|
)
|
|
12688
12881
|
if not bool(system_view_sync.get("verified")):
|
|
12689
12882
|
failure_entry = {
|
|
@@ -12708,44 +12901,42 @@ class AiBuilderFacade:
|
|
|
12708
12901
|
"apply_columns": apply_columns,
|
|
12709
12902
|
},
|
|
12710
12903
|
}
|
|
12711
|
-
|
|
12712
|
-
|
|
12904
|
+
failure_result = failure_entry
|
|
12905
|
+
view_result = failure_entry
|
|
12713
12906
|
continue
|
|
12714
12907
|
if existing_key:
|
|
12715
12908
|
updated.append(patch.name)
|
|
12716
|
-
|
|
12717
|
-
|
|
12718
|
-
|
|
12719
|
-
|
|
12720
|
-
|
|
12721
|
-
|
|
12722
|
-
|
|
12723
|
-
|
|
12724
|
-
|
|
12725
|
-
|
|
12726
|
-
|
|
12727
|
-
|
|
12728
|
-
|
|
12729
|
-
|
|
12730
|
-
)
|
|
12909
|
+
view_result = {
|
|
12910
|
+
"name": patch.name,
|
|
12911
|
+
"view_key": existing_key,
|
|
12912
|
+
"type": patch.type.value,
|
|
12913
|
+
"status": "updated",
|
|
12914
|
+
"fallback_applied": True,
|
|
12915
|
+
"expected_filters": deepcopy(translated_filters),
|
|
12916
|
+
"expected_buttons": deepcopy(expected_button_summary),
|
|
12917
|
+
"expected_query_conditions": deepcopy(expected_query_conditions_for_verify),
|
|
12918
|
+
"expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
|
|
12919
|
+
"expected_pinned": patch.pinned,
|
|
12920
|
+
"system_view_sync": system_view_sync,
|
|
12921
|
+
"apply_columns": deepcopy(apply_columns),
|
|
12922
|
+
}
|
|
12731
12923
|
record_action_button_intent(patch=patch, view_key=existing_key, index=ordinal - 1)
|
|
12732
12924
|
else:
|
|
12733
12925
|
created.append(patch.name)
|
|
12734
|
-
|
|
12735
|
-
|
|
12736
|
-
|
|
12737
|
-
|
|
12738
|
-
|
|
12739
|
-
|
|
12740
|
-
|
|
12741
|
-
|
|
12742
|
-
|
|
12743
|
-
|
|
12744
|
-
|
|
12745
|
-
|
|
12746
|
-
|
|
12747
|
-
|
|
12748
|
-
)
|
|
12926
|
+
view_result = {
|
|
12927
|
+
"name": patch.name,
|
|
12928
|
+
"view_key": created_key,
|
|
12929
|
+
"type": patch.type.value,
|
|
12930
|
+
"status": "created",
|
|
12931
|
+
"fallback_applied": True,
|
|
12932
|
+
"expected_filters": deepcopy(translated_filters),
|
|
12933
|
+
"expected_buttons": deepcopy(expected_button_summary),
|
|
12934
|
+
"expected_query_conditions": deepcopy(expected_query_conditions_for_verify),
|
|
12935
|
+
"expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
|
|
12936
|
+
"expected_pinned": patch.pinned,
|
|
12937
|
+
"system_view_sync": system_view_sync,
|
|
12938
|
+
"apply_columns": deepcopy(apply_columns),
|
|
12939
|
+
}
|
|
12749
12940
|
record_action_button_intent(patch=patch, view_key=created_key, index=ordinal - 1)
|
|
12750
12941
|
continue
|
|
12751
12942
|
fallback_payload = _build_minimal_view_payload(
|
|
@@ -12754,16 +12945,16 @@ class AiBuilderFacade:
|
|
|
12754
12945
|
patch=patch,
|
|
12755
12946
|
ordinal=ordinal,
|
|
12756
12947
|
view_filters=translated_filters,
|
|
12757
|
-
|
|
12948
|
+
hierarchy_filter_groups=hierarchy_root_filter_groups,
|
|
12949
|
+
fields_by_que_id=fields_by_que_id,
|
|
12758
12950
|
auth_override=view_auth_override,
|
|
12759
12951
|
explicit_button_dtos=explicit_button_dtos,
|
|
12760
12952
|
query_condition_payload=query_condition_payload_for_apply,
|
|
12761
12953
|
associated_resources_payload=associated_resources_payload_for_apply,
|
|
12762
12954
|
)
|
|
12763
|
-
self.
|
|
12955
|
+
self._apply_view_create(profile=profile, payload=fallback_payload)
|
|
12764
12956
|
created.append(patch.name)
|
|
12765
|
-
|
|
12766
|
-
{
|
|
12957
|
+
view_result = {
|
|
12767
12958
|
"name": patch.name,
|
|
12768
12959
|
"view_key": created_key,
|
|
12769
12960
|
"type": patch.type.value,
|
|
@@ -12773,7 +12964,6 @@ class AiBuilderFacade:
|
|
|
12773
12964
|
"expected_query_conditions": deepcopy(expected_query_conditions_for_verify),
|
|
12774
12965
|
"expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
|
|
12775
12966
|
}
|
|
12776
|
-
)
|
|
12777
12967
|
record_action_button_intent(patch=patch, view_key=created_key, index=ordinal - 1)
|
|
12778
12968
|
continue
|
|
12779
12969
|
except (QingflowApiError, RuntimeError) as fallback_error:
|
|
@@ -12799,11 +12989,16 @@ class AiBuilderFacade:
|
|
|
12799
12989
|
"view_name": patch.name,
|
|
12800
12990
|
"view_type": patch.type.value,
|
|
12801
12991
|
"columns": patch.columns,
|
|
12802
|
-
"
|
|
12803
|
-
"filters": [
|
|
12804
|
-
|
|
12805
|
-
|
|
12806
|
-
|
|
12992
|
+
"group_que_id": patch.group_que_id,
|
|
12993
|
+
"filters": [
|
|
12994
|
+
(item.model_dump(mode="json") if hasattr(item, "model_dump") else item)
|
|
12995
|
+
if not isinstance(item, list)
|
|
12996
|
+
else [sub.model_dump(mode="json") if hasattr(sub, "model_dump") else sub for sub in item]
|
|
12997
|
+
for item in patch.filters
|
|
12998
|
+
],
|
|
12999
|
+
"start_que_id": patch.start_que_id,
|
|
13000
|
+
"end_que_id": patch.end_que_id,
|
|
13001
|
+
"title_que_id": patch.title_que_id,
|
|
12807
13002
|
"operation": "sync_default_view" if operation_phase == "default_view_apply_config_sync" else ("update" if existing_key or created_key else "create"),
|
|
12808
13003
|
"list_type": system_view_list_type,
|
|
12809
13004
|
"transport_error": {
|
|
@@ -12813,24 +13008,69 @@ class AiBuilderFacade:
|
|
|
12813
13008
|
},
|
|
12814
13009
|
**_view_apply_failure_diagnostics(
|
|
12815
13010
|
patch=patch,
|
|
12816
|
-
|
|
13011
|
+
fields_by_que_id=fields_by_que_id,
|
|
12817
13012
|
backend_code=api_error.backend_code,
|
|
12818
13013
|
operation_phase=operation_phase,
|
|
12819
13014
|
),
|
|
12820
13015
|
},
|
|
12821
13016
|
}
|
|
12822
|
-
|
|
12823
|
-
|
|
13017
|
+
failure_result = failure_entry
|
|
13018
|
+
view_result = failure_entry
|
|
12824
13019
|
continue
|
|
12825
13020
|
view_mutation_ms += _duration_ms(view_mutation_started_at)
|
|
13021
|
+
for item in ((view_result,) if view_result is not None else ()):
|
|
13022
|
+
requested_pinned = item.get("expected_pinned")
|
|
13023
|
+
view_key = str(item.get("view_key") or "").strip()
|
|
13024
|
+
if (
|
|
13025
|
+
str(item.get("status") or "") not in {"created", "updated"}
|
|
13026
|
+
or not isinstance(requested_pinned, bool)
|
|
13027
|
+
or not view_key
|
|
13028
|
+
):
|
|
13029
|
+
continue
|
|
13030
|
+
try:
|
|
13031
|
+
pin_result = self._set_view_pinned(
|
|
13032
|
+
profile=profile,
|
|
13033
|
+
app_key=app_key,
|
|
13034
|
+
view_key=view_key,
|
|
13035
|
+
pinned=requested_pinned,
|
|
13036
|
+
)
|
|
13037
|
+
item["pinned_state"] = pin_result
|
|
13038
|
+
if not bool(pin_result.get("verified")):
|
|
13039
|
+
failure_result = {
|
|
13040
|
+
"name": item.get("name"),
|
|
13041
|
+
"view_key": view_key,
|
|
13042
|
+
"type": item.get("type"),
|
|
13043
|
+
"status": "failed",
|
|
13044
|
+
"error_code": "VIEW_PIN_STATE_READBACK_MISMATCH",
|
|
13045
|
+
"message": "view configuration was saved, but its pinned state did not verify",
|
|
13046
|
+
"operation": "set_pinned",
|
|
13047
|
+
"details": pin_result,
|
|
13048
|
+
}
|
|
13049
|
+
except (QingflowApiError, RuntimeError, ValueError) as error:
|
|
13050
|
+
api_error = _coerce_api_error(error)
|
|
13051
|
+
pin_failure = {
|
|
13052
|
+
"name": item.get("name"),
|
|
13053
|
+
"view_key": view_key,
|
|
13054
|
+
"type": item.get("type"),
|
|
13055
|
+
"status": "failed",
|
|
13056
|
+
"error_code": "VIEW_PIN_STATE_APPLY_FAILED",
|
|
13057
|
+
"message": "view configuration was saved, but updating its pinned state failed",
|
|
13058
|
+
"request_id": api_error.request_id,
|
|
13059
|
+
"backend_code": api_error.backend_code,
|
|
13060
|
+
"http_status": api_error.http_status,
|
|
13061
|
+
"operation": "set_pinned",
|
|
13062
|
+
"details": {"requested_pinned": requested_pinned},
|
|
13063
|
+
}
|
|
13064
|
+
item["pinned_state"] = {"requested_pinned": requested_pinned, "verified": False}
|
|
13065
|
+
failure_result = pin_failure
|
|
12826
13066
|
successful_action_view_keys = {
|
|
12827
13067
|
str(item.get("view_key") or "").strip()
|
|
12828
|
-
for item in
|
|
13068
|
+
for item in ((view_result,) if view_result is not None else ())
|
|
12829
13069
|
if str(item.get("status") or "") in {"created", "updated"} and str(item.get("view_key") or "").strip()
|
|
12830
13070
|
}
|
|
12831
13071
|
successful_action_view_names = {
|
|
12832
13072
|
str(item.get("name") or "").strip()
|
|
12833
|
-
for item in
|
|
13073
|
+
for item in ((view_result,) if view_result is not None else ())
|
|
12834
13074
|
if str(item.get("status") or "") in {"created", "updated"} and str(item.get("name") or "").strip()
|
|
12835
13075
|
}
|
|
12836
13076
|
skipped_action_button_intents = [
|
|
@@ -12881,12 +13121,12 @@ class AiBuilderFacade:
|
|
|
12881
13121
|
action_buttons_failed = bool(action_button_intents) and not (action_buttons_verified and view_action_button_bindings_verified)
|
|
12882
13122
|
action_buttons_retry_payload = action_buttons_result.get("retry_payload") if isinstance(action_buttons_result.get("retry_payload"), dict) else None
|
|
12883
13123
|
needs_view_list_readback = bool(created or updated)
|
|
12884
|
-
|
|
13124
|
+
verified_views: list[dict[str, Any]] | None = []
|
|
12885
13125
|
verified_views_unavailable = False
|
|
12886
13126
|
if needs_view_list_readback:
|
|
12887
13127
|
try:
|
|
12888
13128
|
view_readback_started_at = time.perf_counter()
|
|
12889
|
-
|
|
13129
|
+
verified_views, verified_views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
12890
13130
|
except (QingflowApiError, RuntimeError) as error:
|
|
12891
13131
|
view_readback_ms += _duration_ms(view_readback_started_at)
|
|
12892
13132
|
api_error = _coerce_api_error(error)
|
|
@@ -12900,23 +13140,23 @@ class AiBuilderFacade:
|
|
|
12900
13140
|
view_readback_ms += _duration_ms(view_readback_started_at)
|
|
12901
13141
|
verified_names = {
|
|
12902
13142
|
_extract_view_name(item)
|
|
12903
|
-
for item in (
|
|
13143
|
+
for item in (verified_views or [])
|
|
12904
13144
|
if isinstance(item, dict) and _extract_view_name(item)
|
|
12905
13145
|
}
|
|
12906
13146
|
verified_by_key = {
|
|
12907
13147
|
_extract_view_key(item): item
|
|
12908
|
-
for item in (
|
|
13148
|
+
for item in (verified_views or [])
|
|
12909
13149
|
if isinstance(item, dict) and _extract_view_key(item)
|
|
12910
13150
|
}
|
|
12911
13151
|
verified_view_keys_by_name: dict[str, list[str]] = {}
|
|
12912
|
-
for item in (
|
|
13152
|
+
for item in (verified_views or []):
|
|
12913
13153
|
if not isinstance(item, dict):
|
|
12914
13154
|
continue
|
|
12915
13155
|
name = _extract_view_name(item)
|
|
12916
13156
|
key = _extract_view_key(item)
|
|
12917
13157
|
if name and key:
|
|
12918
13158
|
verified_view_keys_by_name.setdefault(name, []).append(key)
|
|
12919
|
-
|
|
13159
|
+
verification_result: dict[str, Any] | None = None
|
|
12920
13160
|
filter_readback_pending = False
|
|
12921
13161
|
filter_mismatches: list[dict[str, Any]] = []
|
|
12922
13162
|
query_condition_readback_pending = False
|
|
@@ -12943,7 +13183,7 @@ class AiBuilderFacade:
|
|
|
12943
13183
|
return cached
|
|
12944
13184
|
|
|
12945
13185
|
view_verification_started_at = time.perf_counter()
|
|
12946
|
-
for item in
|
|
13186
|
+
for item in ((view_result,) if view_result is not None else ()):
|
|
12947
13187
|
status = str(item.get("status") or "")
|
|
12948
13188
|
name = str(item.get("name") or "")
|
|
12949
13189
|
item_view_key = str(item.get("view_key") or "").strip()
|
|
@@ -12986,7 +13226,7 @@ class AiBuilderFacade:
|
|
|
12986
13226
|
verification_entry["readback_ambiguous"] = True
|
|
12987
13227
|
verification_entry["matching_view_keys"] = matched_keys
|
|
12988
13228
|
filter_readback_pending = True
|
|
12989
|
-
|
|
13229
|
+
verification_result = verification_entry
|
|
12990
13230
|
continue
|
|
12991
13231
|
try:
|
|
12992
13232
|
config_result, config_error = read_view_config_for_verification(verification_key)
|
|
@@ -13004,11 +13244,11 @@ class AiBuilderFacade:
|
|
|
13004
13244
|
filters_verified = filters_semantically_equivalent and actual_data_scope == expected_data_scope
|
|
13005
13245
|
public_expected_filters = _public_view_filter_groups_from_match_rules(
|
|
13006
13246
|
expected_filter_summary,
|
|
13007
|
-
|
|
13247
|
+
fields_by_que_id=fields_by_que_id,
|
|
13008
13248
|
)
|
|
13009
13249
|
public_actual_filters = _public_view_filter_groups_from_match_rules(
|
|
13010
13250
|
semantic_actual_filters,
|
|
13011
|
-
|
|
13251
|
+
fields_by_que_id=fields_by_que_id,
|
|
13012
13252
|
)
|
|
13013
13253
|
verification_entry["filters_verified"] = filters_verified
|
|
13014
13254
|
verification_entry["view_key"] = verification_key
|
|
@@ -13058,7 +13298,7 @@ class AiBuilderFacade:
|
|
|
13058
13298
|
verification_entry["readback_ambiguous"] = True
|
|
13059
13299
|
verification_entry["matching_view_keys"] = matched_keys
|
|
13060
13300
|
query_condition_readback_pending = True
|
|
13061
|
-
|
|
13301
|
+
verification_result = verification_entry
|
|
13062
13302
|
continue
|
|
13063
13303
|
try:
|
|
13064
13304
|
config_result, config_error = read_view_config_for_verification(verification_key)
|
|
@@ -13107,7 +13347,7 @@ class AiBuilderFacade:
|
|
|
13107
13347
|
verification_entry["readback_ambiguous"] = True
|
|
13108
13348
|
verification_entry["matching_view_keys"] = matched_keys
|
|
13109
13349
|
associated_resource_readback_pending = True
|
|
13110
|
-
|
|
13350
|
+
verification_result = verification_entry
|
|
13111
13351
|
continue
|
|
13112
13352
|
try:
|
|
13113
13353
|
config_result, config_error = read_view_config_for_verification(verification_key)
|
|
@@ -13162,7 +13402,7 @@ class AiBuilderFacade:
|
|
|
13162
13402
|
verification_entry["readback_ambiguous"] = True
|
|
13163
13403
|
verification_entry["matching_view_keys"] = matched_keys
|
|
13164
13404
|
button_readback_pending = True
|
|
13165
|
-
|
|
13405
|
+
verification_result = verification_entry
|
|
13166
13406
|
continue
|
|
13167
13407
|
try:
|
|
13168
13408
|
config_result, config_error = read_view_config_for_verification(verification_key)
|
|
@@ -13210,51 +13450,45 @@ class AiBuilderFacade:
|
|
|
13210
13450
|
"category": api_error.category,
|
|
13211
13451
|
}
|
|
13212
13452
|
button_readback_pending = True
|
|
13213
|
-
|
|
13453
|
+
verification_result = verification_entry
|
|
13214
13454
|
elif status == "removed":
|
|
13215
|
-
|
|
13216
|
-
|
|
13217
|
-
|
|
13218
|
-
|
|
13219
|
-
|
|
13220
|
-
|
|
13221
|
-
|
|
13222
|
-
|
|
13223
|
-
|
|
13224
|
-
|
|
13225
|
-
|
|
13226
|
-
}
|
|
13227
|
-
)
|
|
13455
|
+
verification_result = {
|
|
13456
|
+
"name": name,
|
|
13457
|
+
"view_key": item.get("view_key"),
|
|
13458
|
+
"type": item.get("type"),
|
|
13459
|
+
"status": "removed",
|
|
13460
|
+
"present_in_readback": False,
|
|
13461
|
+
"removed_verified": True,
|
|
13462
|
+
"delete_executed": bool(item.get("delete_executed")),
|
|
13463
|
+
"readback_status": item.get("readback_status") or "deleted",
|
|
13464
|
+
"safe_to_retry_delete": False,
|
|
13465
|
+
}
|
|
13228
13466
|
elif status == "readback_pending" and item.get("operation") == "delete":
|
|
13229
13467
|
readback_status = str(item.get("readback_status") or "unavailable")
|
|
13230
|
-
|
|
13231
|
-
|
|
13232
|
-
|
|
13233
|
-
|
|
13234
|
-
|
|
13235
|
-
|
|
13236
|
-
|
|
13237
|
-
|
|
13238
|
-
|
|
13239
|
-
|
|
13240
|
-
|
|
13241
|
-
|
|
13242
|
-
}
|
|
13243
|
-
)
|
|
13468
|
+
verification_result = {
|
|
13469
|
+
"name": name,
|
|
13470
|
+
"view_key": item.get("view_key"),
|
|
13471
|
+
"type": item.get("type"),
|
|
13472
|
+
"status": "readback_pending",
|
|
13473
|
+
"present_in_readback": True if readback_status == "still_exists" else None,
|
|
13474
|
+
"removed_verified": False,
|
|
13475
|
+
"delete_executed": bool(item.get("delete_executed")),
|
|
13476
|
+
"readback_status": readback_status,
|
|
13477
|
+
"safe_to_retry_delete": False,
|
|
13478
|
+
"error_code": item.get("error_code"),
|
|
13479
|
+
}
|
|
13244
13480
|
else:
|
|
13245
|
-
|
|
13246
|
-
|
|
13247
|
-
|
|
13248
|
-
|
|
13249
|
-
|
|
13250
|
-
|
|
13251
|
-
|
|
13252
|
-
}
|
|
13253
|
-
)
|
|
13481
|
+
verification_result = {
|
|
13482
|
+
"name": item.get("name"),
|
|
13483
|
+
"type": item.get("type"),
|
|
13484
|
+
"status": "failed",
|
|
13485
|
+
"present_in_readback": None,
|
|
13486
|
+
"error_code": item.get("error_code"),
|
|
13487
|
+
}
|
|
13254
13488
|
view_verification_ms += _duration_ms(view_verification_started_at)
|
|
13255
13489
|
removed_delete_results = [
|
|
13256
13490
|
item
|
|
13257
|
-
for item in
|
|
13491
|
+
for item in ((view_result,) if view_result is not None else ())
|
|
13258
13492
|
if item.get("operation") == "delete" and bool(item.get("delete_executed"))
|
|
13259
13493
|
]
|
|
13260
13494
|
removed_verified = all(str(item.get("readback_status") or "") == "deleted" for item in removed_delete_results)
|
|
@@ -13269,19 +13503,19 @@ class AiBuilderFacade:
|
|
|
13269
13503
|
view_associated_resources_verified = verified and not associated_resource_readback_pending and not associated_resource_mismatches
|
|
13270
13504
|
view_buttons_verified = verified and not button_readback_pending and not button_mismatches and not custom_button_readback_pending
|
|
13271
13505
|
noop = not created and not updated and not removed and not action_button_write_executed
|
|
13272
|
-
if
|
|
13506
|
+
if failure_result is not None:
|
|
13273
13507
|
successful_changes = bool(created or updated or removed or action_button_write_succeeded)
|
|
13274
|
-
first_failure =
|
|
13508
|
+
first_failure = failure_result
|
|
13275
13509
|
response = {
|
|
13276
13510
|
"status": "partial_success" if successful_changes else "failed",
|
|
13277
13511
|
"error_code": "VIEW_APPLY_PARTIAL" if successful_changes else "VIEW_APPLY_FAILED",
|
|
13278
13512
|
"recoverable": True,
|
|
13279
|
-
"message": "
|
|
13513
|
+
"message": "view operation completed with a verification failure" if successful_changes else "view operation failed",
|
|
13280
13514
|
"normalized_args": normalized_args,
|
|
13281
13515
|
"missing_fields": [],
|
|
13282
13516
|
"allowed_values": {"view_types": [member.value for member in PublicViewType], "view.filter.operator": [member.value for member in ViewFilterOperator]},
|
|
13283
13517
|
"details": {
|
|
13284
|
-
"
|
|
13518
|
+
"view_result": deepcopy(view_result) if view_result is not None else None,
|
|
13285
13519
|
"filter_mismatches": filter_mismatches,
|
|
13286
13520
|
"query_condition_mismatches": query_condition_mismatches,
|
|
13287
13521
|
"associated_resource_mismatches": associated_resource_mismatches,
|
|
@@ -13299,7 +13533,8 @@ class AiBuilderFacade:
|
|
|
13299
13533
|
"http_status": first_failure.get("http_status"),
|
|
13300
13534
|
"noop": noop,
|
|
13301
13535
|
"warnings": (
|
|
13302
|
-
(
|
|
13536
|
+
list(stale_warnings)
|
|
13537
|
+
+ (
|
|
13303
13538
|
[_warning("VIEW_FILTERS_UNVERIFIED", "view definitions may exist, but saved filter behavior is not fully verified")]
|
|
13304
13539
|
if (filter_readback_pending or filter_mismatches)
|
|
13305
13540
|
else []
|
|
@@ -13339,13 +13574,14 @@ class AiBuilderFacade:
|
|
|
13339
13574
|
"action_buttons_verified": action_buttons_verified,
|
|
13340
13575
|
"view_button_bindings_verified": view_action_button_bindings_verified,
|
|
13341
13576
|
"views_read_unavailable": verified_views_unavailable,
|
|
13342
|
-
"
|
|
13577
|
+
"view": deepcopy(verification_result) if verification_result is not None else None,
|
|
13343
13578
|
"custom_button_readback_pending": custom_button_readback_pending,
|
|
13344
13579
|
"custom_button_readback_pending_entries": deepcopy(custom_button_readback_pending_entries),
|
|
13345
13580
|
},
|
|
13346
13581
|
"app_key": app_key,
|
|
13347
13582
|
"app_name": app_name,
|
|
13348
|
-
"
|
|
13583
|
+
"view_result": deepcopy(view_result) if view_result is not None else None,
|
|
13584
|
+
"views_diff": {"created": created, "updated": updated, "removed": removed, "failed": [failure_result]},
|
|
13349
13585
|
"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
13586
|
"write_executed": bool(created or updated or removed or action_button_write_executed),
|
|
13351
13587
|
"write_succeeded": bool(created or updated or removed or action_button_write_succeeded),
|
|
@@ -13353,6 +13589,7 @@ class AiBuilderFacade:
|
|
|
13353
13589
|
}
|
|
13354
13590
|
return finish_with_publish(response)
|
|
13355
13591
|
warnings: list[dict[str, Any]] = []
|
|
13592
|
+
warnings.extend(stale_warnings)
|
|
13356
13593
|
if filter_readback_pending or filter_mismatches:
|
|
13357
13594
|
warnings.append(_warning("VIEW_FILTERS_UNVERIFIED", "view definitions were applied, but saved filter behavior is not fully verified"))
|
|
13358
13595
|
if query_condition_readback_pending or query_condition_mismatches:
|
|
@@ -13408,50 +13645,351 @@ class AiBuilderFacade:
|
|
|
13408
13645
|
),
|
|
13409
13646
|
"normalized_args": normalized_args,
|
|
13410
13647
|
"missing_fields": [],
|
|
13411
|
-
"allowed_values": {"view_types": [member.value for member in PublicViewType], "view.filter.operator": [member.value for member in ViewFilterOperator], "view.associated_resources.limit_type": ["all", "select"]},
|
|
13648
|
+
"allowed_values": {"view_types": [member.value for member in PublicViewType], "view.filter.operator": [member.value for member in ViewFilterOperator], "view.associated_resources.limit_type": ["all", "select"]},
|
|
13649
|
+
"details": {
|
|
13650
|
+
**({"filter_mismatches": filter_mismatches} if filter_mismatches else {}),
|
|
13651
|
+
**({"query_condition_mismatches": query_condition_mismatches} if query_condition_mismatches else {}),
|
|
13652
|
+
**({"associated_resource_mismatches": associated_resource_mismatches} if associated_resource_mismatches else {}),
|
|
13653
|
+
**({"button_mismatches": button_mismatches} if button_mismatches else {}),
|
|
13654
|
+
"view_result": deepcopy(view_result) if view_result is not None else None,
|
|
13655
|
+
**({"action_buttons_result": action_buttons_result} if action_button_intents else {}),
|
|
13656
|
+
**(
|
|
13657
|
+
{"custom_button_readback_pending": deepcopy(custom_button_readback_pending_entries)}
|
|
13658
|
+
if custom_button_readback_pending_entries
|
|
13659
|
+
else {}
|
|
13660
|
+
),
|
|
13661
|
+
},
|
|
13662
|
+
"request_id": None,
|
|
13663
|
+
"suggested_next_call": None if all_verified else (action_buttons_retry_payload if action_buttons_failed and action_buttons_retry_payload else {"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}}),
|
|
13664
|
+
"noop": noop,
|
|
13665
|
+
"warnings": warnings,
|
|
13666
|
+
"verification": {
|
|
13667
|
+
"views_verified": verified,
|
|
13668
|
+
"view_filters_verified": view_filters_verified,
|
|
13669
|
+
"view_query_conditions_verified": view_query_conditions_verified,
|
|
13670
|
+
"view_associated_resources_verified": view_associated_resources_verified,
|
|
13671
|
+
"view_buttons_verified": view_buttons_verified,
|
|
13672
|
+
"action_buttons_verified": action_buttons_verified,
|
|
13673
|
+
"view_button_bindings_verified": view_action_button_bindings_verified,
|
|
13674
|
+
"views_read_unavailable": verified_views_unavailable,
|
|
13675
|
+
"filter_readback_pending": filter_readback_pending,
|
|
13676
|
+
"query_condition_readback_pending": query_condition_readback_pending,
|
|
13677
|
+
"associated_resource_readback_pending": associated_resource_readback_pending,
|
|
13678
|
+
"button_readback_pending": button_readback_pending,
|
|
13679
|
+
"delete_readback_pending": delete_readback_pending,
|
|
13680
|
+
"custom_button_readback_pending": custom_button_readback_pending,
|
|
13681
|
+
"custom_button_readback_pending_entries": deepcopy(custom_button_readback_pending_entries),
|
|
13682
|
+
"view": deepcopy(verification_result) if verification_result is not None else None,
|
|
13683
|
+
},
|
|
13684
|
+
"app_key": app_key,
|
|
13685
|
+
"app_name": app_name,
|
|
13686
|
+
"view_result": deepcopy(view_result) if view_result is not None else None,
|
|
13687
|
+
"views_diff": {"created": created, "updated": updated, "removed": removed, "failed": []},
|
|
13688
|
+
"verified": all_verified,
|
|
13689
|
+
"write_executed": bool(created or updated or removed or action_button_write_executed),
|
|
13690
|
+
"write_succeeded": bool(created or updated or removed or action_button_write_succeeded),
|
|
13691
|
+
"safe_to_retry": not bool(created or updated or removed or action_button_write_executed),
|
|
13692
|
+
}
|
|
13693
|
+
return finish_with_publish(response)
|
|
13694
|
+
|
|
13695
|
+
def app_views_schema(
|
|
13696
|
+
self,
|
|
13697
|
+
*,
|
|
13698
|
+
schema_version: str | None = None,
|
|
13699
|
+
schema_kind: str | None = None,
|
|
13700
|
+
) -> JSONObject:
|
|
13701
|
+
"""读取版本化视图声明 schema;detail kind 用于获取专项配置定义。"""
|
|
13702
|
+
normalized_args = {"schema_version": schema_version, "schema_kind": schema_kind}
|
|
13703
|
+
try:
|
|
13704
|
+
payload = views_schema_registry().schema(schema_version=schema_version, schema_kind=schema_kind)
|
|
13705
|
+
except ViewsContractError as error:
|
|
13706
|
+
return _failed(
|
|
13707
|
+
error.error_code,
|
|
13708
|
+
str(error.message),
|
|
13709
|
+
normalized_args=normalized_args,
|
|
13710
|
+
details={"reason_path": error.path or "", **(error.details or {})},
|
|
13711
|
+
suggested_next_call={"tool_name": "app_views_schema", "arguments": {"schema_kind": "summary"}},
|
|
13712
|
+
)
|
|
13713
|
+
return {
|
|
13714
|
+
"status": "success",
|
|
13715
|
+
"error_code": None,
|
|
13716
|
+
"recoverable": False,
|
|
13717
|
+
"message": "read views declaration schema",
|
|
13718
|
+
"normalized_args": normalized_args,
|
|
13719
|
+
"missing_fields": [],
|
|
13720
|
+
"allowed_values": {"schema_kind": payload["supportedSchemaKinds"]},
|
|
13721
|
+
"details": {},
|
|
13722
|
+
"request_id": None,
|
|
13723
|
+
"suggested_next_call": None,
|
|
13724
|
+
"warnings": [],
|
|
13725
|
+
"verification": {"schema_loaded": True},
|
|
13726
|
+
**payload,
|
|
13727
|
+
}
|
|
13728
|
+
|
|
13729
|
+
def app_views_get(
|
|
13730
|
+
self,
|
|
13731
|
+
*,
|
|
13732
|
+
profile: str,
|
|
13733
|
+
app_key: str,
|
|
13734
|
+
schema_version: str | None = None,
|
|
13735
|
+
schema_kind: str = "summary",
|
|
13736
|
+
) -> JSONObject:
|
|
13737
|
+
"""读取应用的 canonical 视图声明数组(summary 级投影或 complete 反向编译)。"""
|
|
13738
|
+
normalized_args = {"app_key": app_key, "schema_version": schema_version, "schema_kind": schema_kind}
|
|
13739
|
+
try:
|
|
13740
|
+
context = views_schema_registry().resolve_schema_version(schema_version)
|
|
13741
|
+
except ViewsContractError as error:
|
|
13742
|
+
return _failed(
|
|
13743
|
+
error.error_code,
|
|
13744
|
+
str(error.message),
|
|
13745
|
+
normalized_args=normalized_args,
|
|
13746
|
+
details={"reason_path": error.path or "", **(error.details or {})},
|
|
13747
|
+
suggested_next_call={"tool_name": "app_views_schema", "arguments": {"schema_kind": "summary"}},
|
|
13748
|
+
)
|
|
13749
|
+
try:
|
|
13750
|
+
views, _views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=False)
|
|
13751
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
13752
|
+
api_error = _coerce_api_error(error)
|
|
13753
|
+
return _failed_from_api_error(
|
|
13754
|
+
"VIEWS_READ_FAILED",
|
|
13755
|
+
api_error,
|
|
13756
|
+
normalized_args=normalized_args,
|
|
13757
|
+
details=_with_state_read_blocked_details({"app_key": app_key}, resource="views", error=api_error),
|
|
13758
|
+
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
13759
|
+
)
|
|
13760
|
+
schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
13761
|
+
parsed_schema = _parse_schema(schema)
|
|
13762
|
+
# 读面 summary 面向 agent 展示字段名:从原始 schema 递归收集(同名不丢);
|
|
13763
|
+
# 系统列(queId<=5)用公开名映射,未知系统列以 queId 展示兜底
|
|
13764
|
+
name_by_que_id: dict[int, str] = {
|
|
13765
|
+
**{system_id: name for system_id, name in _SYSTEM_VIEW_QUE_NAMES.items()},
|
|
13766
|
+
**_collect_field_id_to_name(schema),
|
|
13767
|
+
}
|
|
13768
|
+
normalized_kind = str(schema_kind or "summary").strip().lower()
|
|
13769
|
+
if normalized_kind == "complete":
|
|
13770
|
+
return self._app_views_get_complete(
|
|
13771
|
+
profile=profile,
|
|
13772
|
+
app_key=app_key,
|
|
13773
|
+
views=views,
|
|
13774
|
+
parsed_schema=parsed_schema,
|
|
13775
|
+
normalized_args=normalized_args,
|
|
13776
|
+
schema_version=schema_version,
|
|
13777
|
+
)
|
|
13778
|
+
projected: list[dict[str, Any]] = []
|
|
13779
|
+
scope_map = {"ALL": "all", "ONESELF": "oneself", "DEPT": "dept", "SUBORDINATE": "subordinate", "CUSTOM": "custom"}
|
|
13780
|
+
for view in views if isinstance(views, list) else []:
|
|
13781
|
+
if not isinstance(view, dict):
|
|
13782
|
+
continue
|
|
13783
|
+
entry: dict[str, Any] = {
|
|
13784
|
+
"view_key": _extract_view_key(view),
|
|
13785
|
+
"name": _extract_view_name(view),
|
|
13786
|
+
"type": _normalize_view_type_name(view.get("viewgraphType") or view.get("type")),
|
|
13787
|
+
}
|
|
13788
|
+
columns = [name_by_que_id.get(que_id, str(que_id)) for que_id in view.get("viewgraphQueIds") or [] if isinstance(que_id, int)]
|
|
13789
|
+
if columns:
|
|
13790
|
+
entry["columns"] = columns
|
|
13791
|
+
group_que_id = _coerce_positive_int(view.get("groupQueId"))
|
|
13792
|
+
if group_que_id is not None and group_que_id in name_by_que_id:
|
|
13793
|
+
entry["group_by"] = name_by_que_id[group_que_id]
|
|
13794
|
+
gantt_config = view.get("viewgraphGanttConfigVO")
|
|
13795
|
+
if isinstance(gantt_config, dict):
|
|
13796
|
+
for source, target in (
|
|
13797
|
+
("startTimeQueId", "start_field"),
|
|
13798
|
+
("endTimeQueId", "end_field"),
|
|
13799
|
+
("titleQueId", "title_field"),
|
|
13800
|
+
):
|
|
13801
|
+
gantt_que_id = _coerce_positive_int(gantt_config.get(source))
|
|
13802
|
+
if gantt_que_id is not None and gantt_que_id in name_by_que_id:
|
|
13803
|
+
entry[target] = name_by_que_id[gantt_que_id]
|
|
13804
|
+
title_que_id = _coerce_positive_int(view.get("titleQue"))
|
|
13805
|
+
if title_que_id is not None and title_que_id in name_by_que_id:
|
|
13806
|
+
entry.setdefault("title_field", name_by_que_id[title_que_id])
|
|
13807
|
+
data_permission_type = view.get("dataPermissionType")
|
|
13808
|
+
if data_permission_type:
|
|
13809
|
+
entry["data_permission"] = {
|
|
13810
|
+
"type": "config" if str(data_permission_type).upper() == "CONFIG" else "custom",
|
|
13811
|
+
"scope": scope_map.get(str(view.get("dataScope") or "").upper(), "all"),
|
|
13812
|
+
}
|
|
13813
|
+
projected.append(entry)
|
|
13814
|
+
return {
|
|
13815
|
+
"status": "success",
|
|
13816
|
+
"error_code": None,
|
|
13817
|
+
"recoverable": False,
|
|
13818
|
+
"message": "read canonical views declarations",
|
|
13819
|
+
"normalized_args": normalized_args,
|
|
13820
|
+
"missing_fields": [],
|
|
13821
|
+
"allowed_values": {"view_types": [member.value for member in PublicViewType]},
|
|
13822
|
+
"details": {},
|
|
13823
|
+
"request_id": None,
|
|
13824
|
+
"suggested_next_call": None,
|
|
13825
|
+
"warnings": [],
|
|
13826
|
+
"verification": {"views_read": len(projected)},
|
|
13827
|
+
"schemaVersion": context.schema_version,
|
|
13828
|
+
"apiVersion": context.api_version,
|
|
13829
|
+
"latestVersion": views_schema_registry().latest_version,
|
|
13830
|
+
"app_key": app_key,
|
|
13831
|
+
"views": projected,
|
|
13832
|
+
}
|
|
13833
|
+
|
|
13834
|
+
def _app_views_get_complete(
|
|
13835
|
+
self,
|
|
13836
|
+
*,
|
|
13837
|
+
profile: str,
|
|
13838
|
+
app_key: str,
|
|
13839
|
+
views: Any,
|
|
13840
|
+
parsed_schema: dict[str, Any],
|
|
13841
|
+
normalized_args: JSONObject,
|
|
13842
|
+
schema_version: str | None,
|
|
13843
|
+
) -> JSONObject:
|
|
13844
|
+
"""complete 档 canonical:逐视图 GET /view/{key}/viewConfig → 反向编译。"""
|
|
13845
|
+
from .views_extract import extract_complete_view_declaration, summarize_gaps
|
|
13846
|
+
|
|
13847
|
+
fields_by_que_id = _collect_fields_by_que_id(parsed_schema["fields"])
|
|
13848
|
+
projected: list[dict[str, Any]] = []
|
|
13849
|
+
read_errors: list[dict[str, Any]] = []
|
|
13850
|
+
all_gaps: list[dict[str, Any]] = []
|
|
13851
|
+
for view in views if isinstance(views, list) else []:
|
|
13852
|
+
if not isinstance(view, dict):
|
|
13853
|
+
continue
|
|
13854
|
+
view_key = _extract_view_key(view)
|
|
13855
|
+
view_name = _extract_view_name(view)
|
|
13856
|
+
if not view_key:
|
|
13857
|
+
continue
|
|
13858
|
+
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")):
|
|
13859
|
+
# 系统视图(列表/申请页等,viewgraphKey 为固定数字 id):不在公开声明语义内,直接跳过。
|
|
13860
|
+
continue
|
|
13861
|
+
try:
|
|
13862
|
+
config = self.views.view_get_config(profile=profile, viewgraph_key=view_key).get("result") or {}
|
|
13863
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
13864
|
+
api_error = _coerce_api_error(error)
|
|
13865
|
+
read_errors.append(
|
|
13866
|
+
{
|
|
13867
|
+
"view_key": view_key,
|
|
13868
|
+
"name": view_name,
|
|
13869
|
+
"error_code": "VIEW_CONFIG_READ_FAILED",
|
|
13870
|
+
"http_status": api_error.http_status,
|
|
13871
|
+
"message": str(api_error.message or error),
|
|
13872
|
+
}
|
|
13873
|
+
)
|
|
13874
|
+
continue
|
|
13875
|
+
if not isinstance(config, dict) or not config:
|
|
13876
|
+
read_errors.append({"view_key": view_key, "name": view_name, "error_code": "VIEW_CONFIG_EMPTY", "message": "viewConfig read returned no data"})
|
|
13877
|
+
continue
|
|
13878
|
+
declaration, gaps = extract_complete_view_declaration(config, fields_by_que_id=fields_by_que_id)
|
|
13879
|
+
if not declaration:
|
|
13880
|
+
read_errors.append(
|
|
13881
|
+
{
|
|
13882
|
+
"view_key": view_key,
|
|
13883
|
+
"name": view_name,
|
|
13884
|
+
"error_code": "VIEW_NOT_EXTRACTABLE",
|
|
13885
|
+
"message": "view configuration cannot be represented completely by the public contract; see gaps",
|
|
13886
|
+
"gaps": gaps,
|
|
13887
|
+
}
|
|
13888
|
+
)
|
|
13889
|
+
continue
|
|
13890
|
+
if gaps:
|
|
13891
|
+
all_gaps.append({"view_key": view_key, "gaps": gaps})
|
|
13892
|
+
projected.append(declaration)
|
|
13893
|
+
gap_summary: dict[str, int] = {}
|
|
13894
|
+
for item in all_gaps:
|
|
13895
|
+
for kind, count in summarize_gaps(item["gaps"]).items():
|
|
13896
|
+
gap_summary[kind] = gap_summary.get(kind, 0) + count
|
|
13897
|
+
context = views_schema_registry().resolve_schema_version(schema_version)
|
|
13898
|
+
return {
|
|
13899
|
+
"status": "success",
|
|
13900
|
+
"error_code": None,
|
|
13901
|
+
"recoverable": False,
|
|
13902
|
+
"message": "read canonical views declarations (complete)",
|
|
13903
|
+
"normalized_args": normalized_args,
|
|
13904
|
+
"missing_fields": [],
|
|
13905
|
+
"allowed_values": {"view_types": [member.value for member in PublicViewType]},
|
|
13412
13906
|
"details": {
|
|
13413
|
-
|
|
13414
|
-
|
|
13415
|
-
|
|
13416
|
-
|
|
13417
|
-
**({"action_buttons_result": action_buttons_result} if action_button_intents else {}),
|
|
13418
|
-
**(
|
|
13419
|
-
{"custom_button_readback_pending": deepcopy(custom_button_readback_pending_entries)}
|
|
13420
|
-
if custom_button_readback_pending_entries
|
|
13421
|
-
else {}
|
|
13422
|
-
),
|
|
13907
|
+
"extraction_gap_summary": gap_summary,
|
|
13908
|
+
"extraction_gaps": all_gaps[:50],
|
|
13909
|
+
"config_read_errors": read_errors[:50],
|
|
13910
|
+
"config_read_error_count": len(read_errors),
|
|
13423
13911
|
},
|
|
13424
13912
|
"request_id": None,
|
|
13425
|
-
"suggested_next_call": None
|
|
13426
|
-
"
|
|
13427
|
-
"
|
|
13428
|
-
"
|
|
13429
|
-
|
|
13430
|
-
|
|
13431
|
-
"view_query_conditions_verified": view_query_conditions_verified,
|
|
13432
|
-
"view_associated_resources_verified": view_associated_resources_verified,
|
|
13433
|
-
"view_buttons_verified": view_buttons_verified,
|
|
13434
|
-
"action_buttons_verified": action_buttons_verified,
|
|
13435
|
-
"view_button_bindings_verified": view_action_button_bindings_verified,
|
|
13436
|
-
"views_read_unavailable": verified_views_unavailable,
|
|
13437
|
-
"filter_readback_pending": filter_readback_pending,
|
|
13438
|
-
"query_condition_readback_pending": query_condition_readback_pending,
|
|
13439
|
-
"associated_resource_readback_pending": associated_resource_readback_pending,
|
|
13440
|
-
"button_readback_pending": button_readback_pending,
|
|
13441
|
-
"delete_readback_pending": delete_readback_pending,
|
|
13442
|
-
"custom_button_readback_pending": custom_button_readback_pending,
|
|
13443
|
-
"custom_button_readback_pending_entries": deepcopy(custom_button_readback_pending_entries),
|
|
13444
|
-
"by_view": verification_by_view,
|
|
13445
|
-
},
|
|
13913
|
+
"suggested_next_call": None,
|
|
13914
|
+
"warnings": [],
|
|
13915
|
+
"verification": {"views_read": len(projected), "views_with_gaps": len(all_gaps), "read_errors": len(read_errors)},
|
|
13916
|
+
"schemaVersion": context.schema_version,
|
|
13917
|
+
"apiVersion": context.api_version,
|
|
13918
|
+
"latestVersion": views_schema_registry().latest_version,
|
|
13446
13919
|
"app_key": app_key,
|
|
13447
|
-
"
|
|
13448
|
-
|
|
13449
|
-
|
|
13450
|
-
|
|
13451
|
-
|
|
13452
|
-
|
|
13920
|
+
"views": projected,
|
|
13921
|
+
}
|
|
13922
|
+
|
|
13923
|
+
def app_views_validate(
|
|
13924
|
+
self,
|
|
13925
|
+
*,
|
|
13926
|
+
declaration: JSONObject,
|
|
13927
|
+
schema_version: str | None = None,
|
|
13928
|
+
) -> JSONObject:
|
|
13929
|
+
"""纯离线校验一条视图声明;不访问后端。"""
|
|
13930
|
+
normalized_args = {"schema_version": schema_version}
|
|
13931
|
+
resolved_version = str(schema_version or (declaration.get("schema_version") if isinstance(declaration, dict) else None) or "").strip() or None
|
|
13932
|
+
try:
|
|
13933
|
+
context = views_schema_registry().resolve_schema_version(resolved_version)
|
|
13934
|
+
except ViewsContractError as error:
|
|
13935
|
+
return _failed(
|
|
13936
|
+
error.error_code,
|
|
13937
|
+
str(error.message),
|
|
13938
|
+
normalized_args=normalized_args,
|
|
13939
|
+
details={"reason_path": error.path or "", **(error.details or {})},
|
|
13940
|
+
suggested_next_call={"tool_name": "app_views_schema", "arguments": {"schema_kind": "complete"}},
|
|
13941
|
+
)
|
|
13942
|
+
issues: list[dict[str, Any]] = []
|
|
13943
|
+
batch_keys = ("views", "upsert_views", "upsertViews", "apps")
|
|
13944
|
+
if not isinstance(declaration, dict):
|
|
13945
|
+
issues.append({"path": "/", "code": "INVALID_SPEC", "message": "views declaration must be a JSON object"})
|
|
13946
|
+
elif any(key in declaration for key in batch_keys):
|
|
13947
|
+
issues.append(
|
|
13948
|
+
{
|
|
13949
|
+
"path": "/",
|
|
13950
|
+
"code": "VIEW_BATCH_CREATE_UNSUPPORTED",
|
|
13951
|
+
"message": "views schema validates one view declaration; batch envelopes are unsupported",
|
|
13952
|
+
}
|
|
13953
|
+
)
|
|
13954
|
+
else:
|
|
13955
|
+
declaration_item = {
|
|
13956
|
+
key: value
|
|
13957
|
+
for key, value in declaration.items()
|
|
13958
|
+
if key not in {"schema_version", "schemaVersion", "apiVersion"}
|
|
13959
|
+
}
|
|
13960
|
+
try:
|
|
13961
|
+
parsed = ViewUpsertPatch.model_validate(declaration_item)
|
|
13962
|
+
context.validate_declaration(compact_none_values(public_view_upsert_payload(parsed)))
|
|
13963
|
+
except ValidationError as exc:
|
|
13964
|
+
first_error = exc.errors()[0]
|
|
13965
|
+
issues.append(
|
|
13966
|
+
{
|
|
13967
|
+
"path": f"/{'/'.join(str(part) for part in first_error.get('loc', ()))}".rstrip("/"),
|
|
13968
|
+
"code": "INVALID_SPEC",
|
|
13969
|
+
"message": str(first_error.get("msg") or exc),
|
|
13970
|
+
}
|
|
13971
|
+
)
|
|
13972
|
+
except ViewsContractError as error:
|
|
13973
|
+
issues.append({"path": error.path or "/", "code": error.error_code, "message": str(error.message)})
|
|
13974
|
+
ok = not issues
|
|
13975
|
+
return {
|
|
13976
|
+
"status": "success" if ok else "failed",
|
|
13977
|
+
"error_code": None if ok else issues[0]["code"],
|
|
13978
|
+
"recoverable": True,
|
|
13979
|
+
"message": "views declaration is valid" if ok else "views declaration failed validation",
|
|
13980
|
+
"normalized_args": normalized_args,
|
|
13981
|
+
"missing_fields": [],
|
|
13982
|
+
"allowed_values": {},
|
|
13983
|
+
"details": {"issues": issues},
|
|
13984
|
+
"request_id": None,
|
|
13985
|
+
"suggested_next_call": None,
|
|
13986
|
+
"warnings": [],
|
|
13987
|
+
"verification": {"validated_entries": 1 if ok else 0},
|
|
13988
|
+
"ok": ok,
|
|
13989
|
+
"schemaVersion": context.schema_version,
|
|
13990
|
+
"apiVersion": context.api_version,
|
|
13991
|
+
"issues": issues,
|
|
13453
13992
|
}
|
|
13454
|
-
return finish_with_publish(response)
|
|
13455
13993
|
|
|
13456
13994
|
def app_publish_verify(
|
|
13457
13995
|
self,
|
|
@@ -15429,6 +15967,76 @@ class AiBuilderFacade:
|
|
|
15429
15967
|
result = legacy_normalized or normalized_views
|
|
15430
15968
|
return (result, False, None) if include_error else (result, False)
|
|
15431
15969
|
|
|
15970
|
+
def _set_view_pinned(
|
|
15971
|
+
self,
|
|
15972
|
+
*,
|
|
15973
|
+
profile: str,
|
|
15974
|
+
app_key: str,
|
|
15975
|
+
view_key: str,
|
|
15976
|
+
pinned: bool,
|
|
15977
|
+
) -> dict[str, Any]:
|
|
15978
|
+
"""Move one view between the application-level fixed and ordinary lists."""
|
|
15979
|
+
target_key = str(view_key or "").strip()
|
|
15980
|
+
if not target_key:
|
|
15981
|
+
raise ValueError("view_key is required to update the pinned state")
|
|
15982
|
+
response = self.views.view_list(profile=profile, app_key=app_key)
|
|
15983
|
+
raw_groups = response.get("result") if isinstance(response, dict) else None
|
|
15984
|
+
if not isinstance(raw_groups, list):
|
|
15985
|
+
raise ValueError("view list does not expose ordinal groups")
|
|
15986
|
+
|
|
15987
|
+
group_keys: dict[str, list[str]] = {
|
|
15988
|
+
"FIXED_VIEW_LIST": [],
|
|
15989
|
+
"VIEW_LIST": [],
|
|
15990
|
+
}
|
|
15991
|
+
for group in raw_groups:
|
|
15992
|
+
if not isinstance(group, dict):
|
|
15993
|
+
continue
|
|
15994
|
+
ordinal_type = str(group.get("ordinalType") or "").strip()
|
|
15995
|
+
if ordinal_type not in group_keys:
|
|
15996
|
+
continue
|
|
15997
|
+
view_list = group.get("viewList")
|
|
15998
|
+
if not isinstance(view_list, list):
|
|
15999
|
+
continue
|
|
16000
|
+
group_keys[ordinal_type] = [
|
|
16001
|
+
key
|
|
16002
|
+
for item in view_list
|
|
16003
|
+
if isinstance(item, dict)
|
|
16004
|
+
and (key := _extract_view_key(item))
|
|
16005
|
+
and key != target_key
|
|
16006
|
+
]
|
|
16007
|
+
|
|
16008
|
+
target_group = "FIXED_VIEW_LIST" if pinned else "VIEW_LIST"
|
|
16009
|
+
group_keys[target_group].append(target_key)
|
|
16010
|
+
self.views.view_reorder(
|
|
16011
|
+
profile=profile,
|
|
16012
|
+
app_key=app_key,
|
|
16013
|
+
payload=[
|
|
16014
|
+
{"ordinalType": "FIXED_VIEW_LIST", "viewKeyList": group_keys["FIXED_VIEW_LIST"]},
|
|
16015
|
+
{"ordinalType": "VIEW_LIST", "viewKeyList": group_keys["VIEW_LIST"]},
|
|
16016
|
+
],
|
|
16017
|
+
)
|
|
16018
|
+
|
|
16019
|
+
readback = self.views.view_list(profile=profile, app_key=app_key)
|
|
16020
|
+
readback_groups = readback.get("result") if isinstance(readback, dict) else None
|
|
16021
|
+
actual_group: str | None = None
|
|
16022
|
+
if isinstance(readback_groups, list):
|
|
16023
|
+
for group in readback_groups:
|
|
16024
|
+
if not isinstance(group, dict):
|
|
16025
|
+
continue
|
|
16026
|
+
ordinal_type = str(group.get("ordinalType") or "").strip()
|
|
16027
|
+
for item in group.get("viewList") or []:
|
|
16028
|
+
if isinstance(item, dict) and _extract_view_key(item) == target_key:
|
|
16029
|
+
actual_group = ordinal_type
|
|
16030
|
+
break
|
|
16031
|
+
if actual_group is not None:
|
|
16032
|
+
break
|
|
16033
|
+
return {
|
|
16034
|
+
"requested_pinned": pinned,
|
|
16035
|
+
"actual_pinned": actual_group == "FIXED_VIEW_LIST" if actual_group is not None else None,
|
|
16036
|
+
"verified": actual_group == target_group,
|
|
16037
|
+
"actual_ordinal_type": actual_group,
|
|
16038
|
+
}
|
|
16039
|
+
|
|
15432
16040
|
def _load_workflow_result(
|
|
15433
16041
|
self,
|
|
15434
16042
|
*,
|
|
@@ -16856,7 +17464,7 @@ def _resolve_custom_button_view_button_ref(
|
|
|
16856
17464
|
"reason_path": reason_path,
|
|
16857
17465
|
"button_ref": button_ref,
|
|
16858
17466
|
"message": "button_ref id does not exist in the current app draft or was removed in this apply",
|
|
16859
|
-
"next_action": "call
|
|
17467
|
+
"next_action": "call app_get_buttons with button_text to find an existing button_id, or use a client_key from upsert_buttons",
|
|
16860
17468
|
}
|
|
16861
17469
|
ref_text = str(button_ref or "").strip()
|
|
16862
17470
|
if ref_text in client_key_map:
|
|
@@ -19226,40 +19834,44 @@ def _chart_apply_batching_context(
|
|
|
19226
19834
|
def _view_apply_failure_diagnostics(
|
|
19227
19835
|
*,
|
|
19228
19836
|
patch: ViewUpsertPatch,
|
|
19229
|
-
|
|
19837
|
+
fields_by_que_id: dict[int, dict[str, Any]],
|
|
19230
19838
|
backend_code: Any,
|
|
19231
19839
|
operation_phase: str,
|
|
19232
19840
|
) -> dict[str, Any]:
|
|
19233
|
-
references: list[tuple[
|
|
19841
|
+
references: list[tuple[int, str]] = []
|
|
19234
19842
|
|
|
19235
|
-
def add_reference(
|
|
19236
|
-
|
|
19237
|
-
if
|
|
19238
|
-
references.append((
|
|
19843
|
+
def add_reference(value: Any, source: str) -> None:
|
|
19844
|
+
que_id = _coerce_positive_int(value)
|
|
19845
|
+
if que_id is not None:
|
|
19846
|
+
references.append((que_id, source))
|
|
19239
19847
|
|
|
19240
19848
|
for column in patch.columns:
|
|
19241
19849
|
add_reference(column, "columns")
|
|
19242
|
-
add_reference(patch.
|
|
19243
|
-
add_reference(patch.
|
|
19244
|
-
add_reference(patch.
|
|
19245
|
-
add_reference(patch.
|
|
19246
|
-
for
|
|
19247
|
-
|
|
19850
|
+
add_reference(patch.group_que_id, "group_que_id")
|
|
19851
|
+
add_reference(patch.start_que_id, "start_que_id")
|
|
19852
|
+
add_reference(patch.end_que_id, "end_que_id")
|
|
19853
|
+
add_reference(patch.title_que_id, "title_que_id")
|
|
19854
|
+
for item in patch.filters:
|
|
19855
|
+
if isinstance(item, list):
|
|
19856
|
+
for rule in item:
|
|
19857
|
+
add_reference(getattr(rule, "que_id", None), "filters")
|
|
19858
|
+
else:
|
|
19859
|
+
add_reference(getattr(item, "que_id", None), "filters")
|
|
19248
19860
|
|
|
19249
|
-
seen: set[str] = set()
|
|
19861
|
+
seen: set[tuple[int, str]] = set()
|
|
19250
19862
|
field_entries: list[dict[str, Any]] = []
|
|
19251
|
-
for
|
|
19252
|
-
key =
|
|
19863
|
+
for que_id, source in references:
|
|
19864
|
+
key = (que_id, source)
|
|
19253
19865
|
if key in seen:
|
|
19254
19866
|
continue
|
|
19255
19867
|
seen.add(key)
|
|
19256
|
-
field =
|
|
19868
|
+
field = fields_by_que_id.get(que_id) or {}
|
|
19257
19869
|
field_entries.append(
|
|
19258
19870
|
{
|
|
19259
|
-
"field_name":
|
|
19871
|
+
"field_name": str(field.get("name") or que_id),
|
|
19260
19872
|
"source": source,
|
|
19261
19873
|
"field_id": field.get("field_id"),
|
|
19262
|
-
"que_id": field.get("que_id"),
|
|
19874
|
+
"que_id": field.get("que_id") or que_id,
|
|
19263
19875
|
"field_type": field.get("type"),
|
|
19264
19876
|
"que_type": field.get("que_type"),
|
|
19265
19877
|
"exists_in_schema": bool(field),
|
|
@@ -19277,7 +19889,7 @@ def _view_apply_failure_diagnostics(
|
|
|
19277
19889
|
"recommended_minimal_retry": {
|
|
19278
19890
|
"name": patch.name,
|
|
19279
19891
|
"type": patch.type.value,
|
|
19280
|
-
"columns": [field_entries[0]["
|
|
19892
|
+
"columns": [field_entries[0]["que_id"]] if field_entries else [],
|
|
19281
19893
|
**({"filters": [rule.model_dump(mode="json", exclude_none=True) for rule in patch.filters]} if patch.filters else {}),
|
|
19282
19894
|
},
|
|
19283
19895
|
}
|
|
@@ -20356,6 +20968,8 @@ def _normalize_portal_view_type(view: dict[str, Any]) -> str:
|
|
|
20356
20968
|
"ganttView": "ganttView",
|
|
20357
20969
|
"hierarchy": "hierarchyView",
|
|
20358
20970
|
"hierarchyView": "hierarchyView",
|
|
20971
|
+
"calendar": "calendarView",
|
|
20972
|
+
"calendarView": "calendarView",
|
|
20359
20973
|
}
|
|
20360
20974
|
return mapping.get(raw, "tableView")
|
|
20361
20975
|
|
|
@@ -21186,8 +21800,10 @@ def _infer_field_type(question: dict[str, Any]) -> str:
|
|
|
21186
21800
|
|
|
21187
21801
|
|
|
21188
21802
|
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
21803
|
que_id = _coerce_positive_int(question.get("queId"))
|
|
21804
|
+
# 无标题字段用「字段{que_id}」兜底命名,与提取器(eval_replay/views get)对称,
|
|
21805
|
+
# 否则视图列引用无标题字段时 UNKNOWN_VIEW_FIELD
|
|
21806
|
+
name = str(question.get("queTitle") or "").strip() or (f"字段{que_id}" if que_id else "")
|
|
21191
21807
|
que_type = _coerce_positive_int(question.get("queType"))
|
|
21192
21808
|
field_type = _infer_field_type(question)
|
|
21193
21809
|
field_id = field_id_hint or f"field_{que_id or _slugify(name, default='x')}"
|
|
@@ -21357,6 +21973,17 @@ def _parse_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
|
|
21357
21973
|
for question in inner_row or []:
|
|
21358
21974
|
if not isinstance(question, dict):
|
|
21359
21975
|
continue
|
|
21976
|
+
if _coerce_positive_int(question.get("queType")) == 24:
|
|
21977
|
+
# 嵌套段落:不注册段落本身,递归解析其 innerQuestions
|
|
21978
|
+
for nested_row in question.get("innerQuestions", []) or []:
|
|
21979
|
+
for nested_question in (nested_row if isinstance(nested_row, list) else [nested_row]):
|
|
21980
|
+
if not isinstance(nested_question, dict):
|
|
21981
|
+
continue
|
|
21982
|
+
nested_field = _parse_field(nested_question)
|
|
21983
|
+
if str(nested_field["name"] or "").strip():
|
|
21984
|
+
fields_by_name.setdefault(nested_field["name"], nested_field)
|
|
21985
|
+
labels.append(nested_field["name"])
|
|
21986
|
+
continue
|
|
21360
21987
|
field = _parse_field(question)
|
|
21361
21988
|
fields_by_name.setdefault(field["name"], field)
|
|
21362
21989
|
labels.append(field["name"])
|
|
@@ -21382,8 +22009,16 @@ def _parse_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
|
|
21382
22009
|
for question in row:
|
|
21383
22010
|
if not isinstance(question, dict):
|
|
21384
22011
|
continue
|
|
22012
|
+
if _coerce_positive_int(question.get("queType")) == 24:
|
|
22013
|
+
# 同行混入段落容器:不注册段落本身(它的子字段在自己 row),跳过避免同名占位挡住真实字段
|
|
22014
|
+
continue
|
|
21385
22015
|
field = _parse_field(question)
|
|
21386
|
-
fields_by_name.
|
|
22016
|
+
existing = fields_by_name.get(field["name"])
|
|
22017
|
+
# 同名冲突:保留信息更丰富的那个(表格字段带 subfields,被同名普通字段覆盖会丢子字段引用面);
|
|
22018
|
+
# 但 queId 索引必须收录全部同名字段(_collect_fields_by_que_id 按 queId 消费,同名不同 id 各自有效)
|
|
22019
|
+
if existing is None or (not existing.get("subfields") and field.get("subfields")):
|
|
22020
|
+
fields_by_name[field["name"]] = field
|
|
22021
|
+
|
|
21387
22022
|
labels.append(field["name"])
|
|
21388
22023
|
if labels:
|
|
21389
22024
|
root_rows.append(labels)
|
|
@@ -22608,6 +23243,23 @@ def _apply_field_mutation(field: dict[str, Any], mutation: Any) -> None:
|
|
|
22608
23243
|
field.pop("_question_rebuild_required", None)
|
|
22609
23244
|
|
|
22610
23245
|
|
|
23246
|
+
class AmbiguousFieldSelector(ValueError):
|
|
23247
|
+
"""name-only selector 命中多个同名字段。
|
|
23248
|
+
|
|
23249
|
+
方案 A(duplicate-field-name):后端以 queId 为字段身份、允许同父级同名;
|
|
23250
|
+
name 命中多个字段时禁止静默取第一个,必须显式失败并给出候选 queId。
|
|
23251
|
+
compiler 包装层(app_form)把它转写为 AMBIGUOUS_FIELD_SELECTOR 错误码。
|
|
23252
|
+
"""
|
|
23253
|
+
|
|
23254
|
+
def __init__(self, location: str, candidates: list[int]) -> None:
|
|
23255
|
+
self.location = location
|
|
23256
|
+
self.candidates = candidates
|
|
23257
|
+
super().__init__(
|
|
23258
|
+
f"{location} matches multiple fields with the same name; "
|
|
23259
|
+
f"use queId to disambiguate (candidate queIds: {sorted(candidates)})"
|
|
23260
|
+
)
|
|
23261
|
+
|
|
23262
|
+
|
|
22611
23263
|
def _resolve_field_selector_with_uniqueness(
|
|
22612
23264
|
*,
|
|
22613
23265
|
fields: list[dict[str, Any]],
|
|
@@ -22623,7 +23275,12 @@ def _resolve_field_selector_with_uniqueness(
|
|
|
22623
23275
|
name = str(selector.name or "").strip()
|
|
22624
23276
|
matched = [field for field in fields if str(field.get("name") or "").strip() == name]
|
|
22625
23277
|
if len(matched) > 1:
|
|
22626
|
-
|
|
23278
|
+
candidate_ids = [
|
|
23279
|
+
ref
|
|
23280
|
+
for candidate in matched
|
|
23281
|
+
if (ref := _coerce_any_int(candidate.get("que_id"))) is not None
|
|
23282
|
+
]
|
|
23283
|
+
raise AmbiguousFieldSelector(location, candidate_ids)
|
|
22627
23284
|
else:
|
|
22628
23285
|
matched = []
|
|
22629
23286
|
if len(matched) != 1:
|
|
@@ -26066,7 +26723,11 @@ def _merge_view_summary_with_config(
|
|
|
26066
26723
|
return summary
|
|
26067
26724
|
if isinstance(config.get("auth"), dict):
|
|
26068
26725
|
summary["visibility_summary"] = _visibility_summary(_public_visibility_from_member_auth(config.get("auth")))
|
|
26069
|
-
legacy_columns = [
|
|
26726
|
+
legacy_columns = [
|
|
26727
|
+
_SYSTEM_VIEW_QUE_NAMES.get(value if isinstance(value, int) else None, str(value))
|
|
26728
|
+
for value in (summary.get("columns") or [])
|
|
26729
|
+
if str(value or "").strip()
|
|
26730
|
+
]
|
|
26070
26731
|
question_entries = _extract_view_question_entries(config.get("viewgraphQuestions"))
|
|
26071
26732
|
canonical_question_entries = _extract_view_question_entries(question_list)
|
|
26072
26733
|
question_entries_by_id = {
|
|
@@ -26212,7 +26873,7 @@ def _merge_view_summary_with_config(
|
|
|
26212
26873
|
)
|
|
26213
26874
|
summary["filters"] = _public_view_filter_groups_from_match_rules(
|
|
26214
26875
|
config.get("viewgraphLimit"),
|
|
26215
|
-
|
|
26876
|
+
fields_by_que_id=field_lookup,
|
|
26216
26877
|
)
|
|
26217
26878
|
config_enriched = True
|
|
26218
26879
|
if any(key in config for key in ("asosChartVisible", "asosChartConfig", "asosChartIdList", "limitType")):
|
|
@@ -26455,12 +27116,16 @@ def _normalize_print_tpls(value: Any) -> list[dict[str, Any]]:
|
|
|
26455
27116
|
items: list[dict[str, Any]] = []
|
|
26456
27117
|
for item in value:
|
|
26457
27118
|
if isinstance(item, dict):
|
|
26458
|
-
tpl_id = item.get("
|
|
27119
|
+
tpl_id = item.get("printKey")
|
|
27120
|
+
if tpl_id is None:
|
|
27121
|
+
tpl_id = item.get("printTplId")
|
|
26459
27122
|
if tpl_id is None:
|
|
26460
27123
|
tpl_id = item.get("templateId")
|
|
26461
27124
|
if tpl_id is None:
|
|
26462
27125
|
tpl_id = item.get("id")
|
|
26463
|
-
tpl_name = item.get("
|
|
27126
|
+
tpl_name = item.get("printName")
|
|
27127
|
+
if tpl_name is None:
|
|
27128
|
+
tpl_name = item.get("printTplName")
|
|
26464
27129
|
if tpl_name is None:
|
|
26465
27130
|
tpl_name = item.get("templateName")
|
|
26466
27131
|
if tpl_name is None:
|
|
@@ -26904,7 +27569,7 @@ def _build_grouped_view_button_config(button_config_dtos: list[dict[str, Any]])
|
|
|
26904
27569
|
|
|
26905
27570
|
def _build_view_button_dtos(
|
|
26906
27571
|
*,
|
|
26907
|
-
|
|
27572
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
26908
27573
|
bindings: list[ViewButtonBindingPatch],
|
|
26909
27574
|
valid_custom_button_ids: set[int],
|
|
26910
27575
|
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
@@ -26913,7 +27578,7 @@ def _build_view_button_dtos(
|
|
|
26913
27578
|
for binding in bindings:
|
|
26914
27579
|
dto, binding_issues = _serialize_view_button_binding(
|
|
26915
27580
|
binding=binding,
|
|
26916
|
-
|
|
27581
|
+
fields_by_que_id=fields_by_que_id,
|
|
26917
27582
|
valid_custom_button_ids=valid_custom_button_ids,
|
|
26918
27583
|
)
|
|
26919
27584
|
if binding_issues:
|
|
@@ -26926,7 +27591,7 @@ def _build_view_button_dtos(
|
|
|
26926
27591
|
def _serialize_view_button_binding(
|
|
26927
27592
|
*,
|
|
26928
27593
|
binding: ViewButtonBindingPatch,
|
|
26929
|
-
|
|
27594
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
26930
27595
|
valid_custom_button_ids: set[int],
|
|
26931
27596
|
allow_unverified_custom_button_id: bool = False,
|
|
26932
27597
|
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
@@ -26941,10 +27606,11 @@ def _serialize_view_button_binding(
|
|
|
26941
27606
|
"reason_path": "buttons[].button_id",
|
|
26942
27607
|
"missing_fields": [],
|
|
26943
27608
|
"details": {"button_id": binding.button_id},
|
|
27609
|
+
"next_action": "call app_get_buttons with the intended button_text to reuse an existing custom button; create only if absent via app_custom_buttons_apply",
|
|
26944
27610
|
}
|
|
26945
27611
|
]
|
|
26946
27612
|
translated_limits, limit_issues = _build_view_button_limit_groups(
|
|
26947
|
-
|
|
27613
|
+
fields_by_que_id=fields_by_que_id,
|
|
26948
27614
|
groups=binding.button_limit,
|
|
26949
27615
|
)
|
|
26950
27616
|
if limit_issues:
|
|
@@ -26970,7 +27636,7 @@ def _serialize_view_button_binding(
|
|
|
26970
27636
|
|
|
26971
27637
|
def _build_view_button_limit_groups(
|
|
26972
27638
|
*,
|
|
26973
|
-
|
|
27639
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
26974
27640
|
groups: list[list[ViewFilterRulePatch]],
|
|
26975
27641
|
) -> tuple[list[list[dict[str, Any]]], list[dict[str, Any]]]:
|
|
26976
27642
|
translated_groups: list[list[dict[str, Any]]] = []
|
|
@@ -26984,14 +27650,14 @@ def _build_view_button_limit_groups(
|
|
|
26984
27650
|
raw_rule = raw_rule.model_dump(mode="json")
|
|
26985
27651
|
if not isinstance(raw_rule, dict):
|
|
26986
27652
|
continue
|
|
26987
|
-
|
|
26988
|
-
field =
|
|
27653
|
+
rule_que_id = _coerce_positive_int(raw_rule.get("que_id"))
|
|
27654
|
+
field = fields_by_que_id.get(rule_que_id) if rule_que_id is not None else None
|
|
26989
27655
|
if field is None:
|
|
26990
27656
|
issues.append(
|
|
26991
27657
|
{
|
|
26992
27658
|
"error_code": "UNKNOWN_VIEW_FIELD",
|
|
26993
|
-
"missing_fields": [
|
|
26994
|
-
"reason_path": "buttons[].button_limit[].
|
|
27659
|
+
"missing_fields": [str(raw_rule.get("que_id") or "")],
|
|
27660
|
+
"reason_path": "buttons[].button_limit[].que_id",
|
|
26995
27661
|
}
|
|
26996
27662
|
)
|
|
26997
27663
|
continue
|
|
@@ -27001,6 +27667,11 @@ def _build_view_button_limit_groups(
|
|
|
27001
27667
|
issue["reason_path"] = "buttons[].button_limit[].values"
|
|
27002
27668
|
issues.append(issue)
|
|
27003
27669
|
continue
|
|
27670
|
+
stale_warning = translated_rule.pop("_stale_warning", None)
|
|
27671
|
+
if stale_warning is not None:
|
|
27672
|
+
stale_warning = deepcopy(stale_warning)
|
|
27673
|
+
stale_warning["reason_path"] = "buttons[].button_limit[].values"
|
|
27674
|
+
issues.append(stale_warning)
|
|
27004
27675
|
translated_rules.append(translated_rule)
|
|
27005
27676
|
if translated_rules:
|
|
27006
27677
|
translated_groups.append(translated_rules)
|
|
@@ -27162,7 +27833,7 @@ def _custom_button_view_configs_from_view_summaries(views: list[dict[str, Any]])
|
|
|
27162
27833
|
placement = _public_view_button_placement(entry.get("config_type")) or entry.get("placement")
|
|
27163
27834
|
raw_button_limit = deepcopy(entry.get("button_limit") or [])
|
|
27164
27835
|
public_button_limit = (
|
|
27165
|
-
_public_view_filter_groups_from_match_rules(raw_button_limit,
|
|
27836
|
+
_public_view_filter_groups_from_match_rules(raw_button_limit, fields_by_que_id=field_lookup)
|
|
27166
27837
|
if raw_button_limit
|
|
27167
27838
|
else []
|
|
27168
27839
|
)
|
|
@@ -27360,6 +28031,11 @@ def _field_id_for_name(fields: list[dict[str, Any]], name: str) -> str:
|
|
|
27360
28031
|
for field in fields:
|
|
27361
28032
|
if field["name"] == name:
|
|
27362
28033
|
return field["field_id"]
|
|
28034
|
+
# 表格子字段是视图列/筛选的合法引用面:顶层未命中时递归 subfields
|
|
28035
|
+
for field in fields:
|
|
28036
|
+
for sub in field.get("subfields") or []:
|
|
28037
|
+
if isinstance(sub, dict) and sub.get("name") == name:
|
|
28038
|
+
return str(sub.get("field_id") or f"field_{sub.get('que_id') or ''}")
|
|
27363
28039
|
raise KeyError(name)
|
|
27364
28040
|
|
|
27365
28041
|
|
|
@@ -27459,7 +28135,7 @@ def _build_public_workflow_spec(*, nodes: list[dict[str, Any]], transitions: lis
|
|
|
27459
28135
|
|
|
27460
28136
|
def _build_flow_condition_matrix(
|
|
27461
28137
|
*,
|
|
27462
|
-
|
|
28138
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
27463
28139
|
node: dict[str, Any],
|
|
27464
28140
|
) -> tuple[list[list[dict[str, Any]]], list[dict[str, Any]]]:
|
|
27465
28141
|
if str(node.get("type") or "") != "condition":
|
|
@@ -27477,7 +28153,7 @@ def _build_flow_condition_matrix(
|
|
|
27477
28153
|
if not isinstance(raw_rule, dict):
|
|
27478
28154
|
continue
|
|
27479
28155
|
field_name = str(raw_rule.get("field_name") or "").strip()
|
|
27480
|
-
field =
|
|
28156
|
+
field = fields_by_que_id.get(field_name)
|
|
27481
28157
|
if field is None:
|
|
27482
28158
|
issues.append(
|
|
27483
28159
|
{
|
|
@@ -27524,7 +28200,9 @@ def _translate_flow_condition_rule(*, field: dict[str, Any], rule: dict[str, Any
|
|
|
27524
28200
|
base["judgeType"] = JUDGE_LESS_OR_EQUAL
|
|
27525
28201
|
base["judgeValues"] = [_stringify_condition_value(values[0])]
|
|
27526
28202
|
elif operator == "is_empty":
|
|
27527
|
-
|
|
28203
|
+
# 选项/成员/部门类字段的空值,后端历史形态是 EQUAL_ANY(9)+空值(该类字段用
|
|
28204
|
+
# ANY 语义选值,"没选任何值"也是 9 判型);文本/数字类才是 EQUAL(0)+空值
|
|
28205
|
+
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
28206
|
base["judgeValues"] = []
|
|
27529
28207
|
elif operator == "not_empty":
|
|
27530
28208
|
base["judgeType"] = JUDGE_UNEQUAL
|
|
@@ -27553,7 +28231,7 @@ def _empty_view_query_conditions_payload() -> dict[str, Any]:
|
|
|
27553
28231
|
|
|
27554
28232
|
def _build_view_query_conditions_payload(
|
|
27555
28233
|
*,
|
|
27556
|
-
|
|
28234
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
27557
28235
|
query_conditions: Any,
|
|
27558
28236
|
) -> tuple[dict[str, Any] | None, dict[str, Any] | None, list[dict[str, Any]]]:
|
|
27559
28237
|
if query_conditions is None:
|
|
@@ -27593,7 +28271,7 @@ def _build_view_query_conditions_payload(
|
|
|
27593
28271
|
compiled_row: list[int] = []
|
|
27594
28272
|
for column_index, selector in enumerate(row):
|
|
27595
28273
|
field, issue = _resolve_query_condition_field(
|
|
27596
|
-
|
|
28274
|
+
fields_by_que_id=fields_by_que_id,
|
|
27597
28275
|
selector=selector,
|
|
27598
28276
|
reason_path=f"query_conditions.rows[{row_index}][{column_index}]",
|
|
27599
28277
|
)
|
|
@@ -27611,14 +28289,16 @@ def _build_view_query_conditions_payload(
|
|
|
27611
28289
|
"queryConditionStatus": enabled,
|
|
27612
28290
|
"queryConditionExact": exact,
|
|
27613
28291
|
"hideBeforeQueryCondition": hide_before_query,
|
|
27614
|
-
|
|
28292
|
+
# 后端 ViewgraphInfo.queryCondition 是 List<ViewgraphQueryConditionConfig>({title?,queIds}),
|
|
28293
|
+
# 不能发裸 [[queId]](fastjson expect {, actual [ → 发现 #7)
|
|
28294
|
+
"queryCondition": [{"queIds": row} for row in rows] if enabled else [],
|
|
27615
28295
|
}
|
|
27616
28296
|
return payload, _normalize_view_query_conditions_for_compare(payload), []
|
|
27617
28297
|
|
|
27618
28298
|
|
|
27619
28299
|
def _resolve_query_condition_field(
|
|
27620
28300
|
*,
|
|
27621
|
-
|
|
28301
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
27622
28302
|
selector: Any,
|
|
27623
28303
|
reason_path: str,
|
|
27624
28304
|
) -> tuple[dict[str, Any], dict[str, Any] | None]:
|
|
@@ -27630,8 +28310,8 @@ def _resolve_query_condition_field(
|
|
|
27630
28310
|
"missing_fields": [],
|
|
27631
28311
|
"message": "query condition field selector cannot be empty",
|
|
27632
28312
|
}
|
|
27633
|
-
fields = [field for field in
|
|
27634
|
-
field =
|
|
28313
|
+
fields = [field for field in fields_by_que_id.values() if isinstance(field, dict)]
|
|
28314
|
+
field = fields_by_que_id.get(raw)
|
|
27635
28315
|
if field is None:
|
|
27636
28316
|
for candidate in fields:
|
|
27637
28317
|
if raw == str(candidate.get("field_id") or "").strip():
|
|
@@ -27642,7 +28322,7 @@ def _resolve_query_condition_field(
|
|
|
27642
28322
|
field = candidate
|
|
27643
28323
|
break
|
|
27644
28324
|
if field is None:
|
|
27645
|
-
subfield_parent = _find_subtable_subfield_parent(
|
|
28325
|
+
subfield_parent = _find_subtable_subfield_parent(fields_by_que_id=fields_by_que_id, selector=raw)
|
|
27646
28326
|
if subfield_parent is not None:
|
|
27647
28327
|
return {}, {
|
|
27648
28328
|
"error_code": "INVALID_QUERY_CONDITION_FIELD",
|
|
@@ -27690,10 +28370,10 @@ def _resolve_query_condition_field(
|
|
|
27690
28370
|
|
|
27691
28371
|
def _find_subtable_subfield_parent(
|
|
27692
28372
|
*,
|
|
27693
|
-
|
|
28373
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
27694
28374
|
selector: str,
|
|
27695
28375
|
) -> dict[str, Any] | None:
|
|
27696
|
-
for field in
|
|
28376
|
+
for field in fields_by_que_id.values():
|
|
27697
28377
|
if not isinstance(field, dict) or str(field.get("type") or "") != FieldType.subtable.value:
|
|
27698
28378
|
continue
|
|
27699
28379
|
for subfield in field.get("subfields") or []:
|
|
@@ -27754,6 +28434,9 @@ def _normalize_view_query_conditions_for_compare(config: Any) -> dict[str, Any]:
|
|
|
27754
28434
|
rows: list[list[int]] = []
|
|
27755
28435
|
if isinstance(raw_rows, list):
|
|
27756
28436
|
for raw_row in raw_rows:
|
|
28437
|
+
if isinstance(raw_row, dict):
|
|
28438
|
+
# ViewgraphQueryConditionConfig 形态({title?,queIds})
|
|
28439
|
+
raw_row = raw_row.get("queIds") or []
|
|
27757
28440
|
if not isinstance(raw_row, list):
|
|
27758
28441
|
continue
|
|
27759
28442
|
compiled_row = [
|
|
@@ -27816,11 +28499,18 @@ def _build_view_associated_resources_payload(
|
|
|
27816
28499
|
]
|
|
27817
28500
|
available_by_id = _associated_resource_index(available_resources)
|
|
27818
28501
|
if limit_type == "all":
|
|
28502
|
+
# limitType=ALL 时后端保留原 idList 不清(语义忽略但存储保留);
|
|
28503
|
+
# 透传声明的 id 列表,避免 apply 清空→回读 idList 不匹配→READBACK_MISMATCH
|
|
28504
|
+
all_ids = [
|
|
28505
|
+
item_id
|
|
28506
|
+
for item_id in (_coerce_positive_int(raw) for raw in raw_item_ids)
|
|
28507
|
+
if item_id is not None
|
|
28508
|
+
]
|
|
27819
28509
|
payload = {
|
|
27820
28510
|
"asosChartVisible": True,
|
|
27821
28511
|
"asosChartConfig": {
|
|
27822
28512
|
"limitType": ASSOCIATED_RESOURCE_LIMIT_TYPE_ALL,
|
|
27823
|
-
"asosChartIdList":
|
|
28513
|
+
"asosChartIdList": all_ids,
|
|
27824
28514
|
},
|
|
27825
28515
|
}
|
|
27826
28516
|
expected = _extract_view_associated_resources_config(payload, available_resources=available_resources)
|
|
@@ -27841,26 +28531,19 @@ def _build_view_associated_resources_payload(
|
|
|
27841
28531
|
]
|
|
27842
28532
|
item_ids.append(item_id)
|
|
27843
28533
|
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
|
-
]
|
|
28534
|
+
# 兼容存量脏引用:关联资源 id 指向闭包外/已下架的图表或视图时,剔除后继续写而非整视图失败;
|
|
28535
|
+
# 剔除明细记入 expected 供回读校验与响应展示
|
|
28536
|
+
resolvable_ids = [item_id for item_id in item_ids if item_id in available_by_id]
|
|
27856
28537
|
payload = {
|
|
27857
28538
|
"asosChartVisible": True,
|
|
27858
28539
|
"asosChartConfig": {
|
|
27859
28540
|
"limitType": ASSOCIATED_RESOURCE_LIMIT_TYPE_SELECT,
|
|
27860
|
-
"asosChartIdList":
|
|
28541
|
+
"asosChartIdList": resolvable_ids,
|
|
27861
28542
|
},
|
|
27862
28543
|
}
|
|
27863
28544
|
expected = _extract_view_associated_resources_config(payload, available_resources=available_resources)
|
|
28545
|
+
if isinstance(expected, dict) and missing_ids:
|
|
28546
|
+
expected["dropped_unresolvable_associated_item_ids"] = missing_ids
|
|
27864
28547
|
return payload, expected, []
|
|
27865
28548
|
|
|
27866
28549
|
|
|
@@ -28597,6 +29280,10 @@ _VIEW_PARTIAL_PATCH_KEY_ALIASES = {
|
|
|
28597
29280
|
"view_key": "view_key",
|
|
28598
29281
|
"viewKey": "view_key",
|
|
28599
29282
|
"viewgraphKey": "view_key",
|
|
29283
|
+
"pinned": "pinned",
|
|
29284
|
+
"fixed": "pinned",
|
|
29285
|
+
"is_pinned": "pinned",
|
|
29286
|
+
"isPinned": "pinned",
|
|
28600
29287
|
"type": "type",
|
|
28601
29288
|
"view_type": "type",
|
|
28602
29289
|
"viewType": "type",
|
|
@@ -28604,17 +29291,25 @@ _VIEW_PARTIAL_PATCH_KEY_ALIASES = {
|
|
|
28604
29291
|
"column_names": "columns",
|
|
28605
29292
|
"columnNames": "columns",
|
|
28606
29293
|
"fields": "columns",
|
|
28607
|
-
"
|
|
28608
|
-
"
|
|
29294
|
+
"group_que_id": "group_que_id",
|
|
29295
|
+
"groupQueId": "group_que_id",
|
|
29296
|
+
"group_by": "group_que_id",
|
|
29297
|
+
"groupBy": "group_que_id",
|
|
28609
29298
|
"filters": "filters",
|
|
28610
29299
|
"filter_rules": "filters",
|
|
28611
29300
|
"filterRules": "filters",
|
|
28612
|
-
"
|
|
28613
|
-
"
|
|
28614
|
-
"
|
|
28615
|
-
"
|
|
28616
|
-
"
|
|
28617
|
-
"
|
|
29301
|
+
"start_que_id": "start_que_id",
|
|
29302
|
+
"startQueId": "start_que_id",
|
|
29303
|
+
"start_field": "start_que_id",
|
|
29304
|
+
"startField": "start_que_id",
|
|
29305
|
+
"end_que_id": "end_que_id",
|
|
29306
|
+
"endQueId": "end_que_id",
|
|
29307
|
+
"end_field": "end_que_id",
|
|
29308
|
+
"endField": "end_que_id",
|
|
29309
|
+
"title_que_id": "title_que_id",
|
|
29310
|
+
"titleQueId": "title_que_id",
|
|
29311
|
+
"title_field": "title_que_id",
|
|
29312
|
+
"titleField": "title_que_id",
|
|
28618
29313
|
"buttons": "buttons",
|
|
28619
29314
|
"visibility": "visibility",
|
|
28620
29315
|
"query_conditions": "query_conditions",
|
|
@@ -28701,13 +29396,14 @@ def _normalize_view_partial_unset(raw_unset: Any, *, reason_path: str) -> tuple[
|
|
|
28701
29396
|
|
|
28702
29397
|
_VIEW_PARTIAL_SET_KEYS = {
|
|
28703
29398
|
"name",
|
|
29399
|
+
"pinned",
|
|
28704
29400
|
"type",
|
|
28705
29401
|
"columns",
|
|
28706
|
-
"
|
|
29402
|
+
"group_que_id",
|
|
28707
29403
|
"filters",
|
|
28708
|
-
"
|
|
28709
|
-
"
|
|
28710
|
-
"
|
|
29404
|
+
"start_que_id",
|
|
29405
|
+
"end_que_id",
|
|
29406
|
+
"title_que_id",
|
|
28711
29407
|
"buttons",
|
|
28712
29408
|
"visibility",
|
|
28713
29409
|
"query_conditions",
|
|
@@ -28733,6 +29429,108 @@ def _field_name_by_id(field_names_by_id: dict[int, str], field_id: Any) -> str |
|
|
|
28733
29429
|
return name or None
|
|
28734
29430
|
|
|
28735
29431
|
|
|
29432
|
+
def _collect_fields_by_que_id(fields: list[dict[str, Any]], *, schema: dict[str, Any] | None = None) -> dict[int, dict[str, Any]]:
|
|
29433
|
+
"""构建 queId→field 索引。
|
|
29434
|
+
|
|
29435
|
+
传 schema 时直接从原始 formQues/baseQues 递归收集(同名冲突不丢字段——名字索引
|
|
29436
|
+
只保留第一个,但同名不同 queId 的字段对 queId 引用各自有效);
|
|
29437
|
+
不传 schema 时从解析后的 fields 列表递归 subfields 构建。
|
|
29438
|
+
"""
|
|
29439
|
+
index: dict[int, dict[str, Any]] = {}
|
|
29440
|
+
|
|
29441
|
+
type_by_question_type = {v: k for k, v in QUESTION_TYPE_TO_FIELD_TYPE.items()}
|
|
29442
|
+
|
|
29443
|
+
def collect_raw(question: dict[str, Any]) -> None:
|
|
29444
|
+
que_id = _coerce_positive_int(question.get("queId"))
|
|
29445
|
+
if que_id is not None and que_id not in index:
|
|
29446
|
+
que_type = _coerce_positive_int(question.get("queType"))
|
|
29447
|
+
# 用 _parse_field 解析(含 options/option_details,与 _parse_schema 对齐);
|
|
29448
|
+
# 直收原始 dict 的 queTitle/queType 不够——选项字段需 options 才能解析值→id
|
|
29449
|
+
parsed = _parse_field(question)
|
|
29450
|
+
index[que_id] = parsed
|
|
29451
|
+
for key in ("subQuestions", "innerQuestions"):
|
|
29452
|
+
for child in question.get(key) or []:
|
|
29453
|
+
if isinstance(child, list):
|
|
29454
|
+
for inner in child:
|
|
29455
|
+
if isinstance(inner, dict):
|
|
29456
|
+
collect_raw(inner)
|
|
29457
|
+
elif isinstance(child, dict):
|
|
29458
|
+
collect_raw(child)
|
|
29459
|
+
|
|
29460
|
+
if schema is not None:
|
|
29461
|
+
for base_question in schema.get("baseQues", []) or []:
|
|
29462
|
+
if isinstance(base_question, dict):
|
|
29463
|
+
collect_raw(base_question)
|
|
29464
|
+
for line in schema.get("formQues", []) or []:
|
|
29465
|
+
for question in (line if isinstance(line, list) else [line]):
|
|
29466
|
+
if isinstance(question, dict):
|
|
29467
|
+
collect_raw(question)
|
|
29468
|
+
return index
|
|
29469
|
+
|
|
29470
|
+
def collect(field: dict[str, Any]) -> None:
|
|
29471
|
+
que_id = _coerce_positive_int(field.get("que_id"))
|
|
29472
|
+
if que_id is not None and que_id not in index:
|
|
29473
|
+
index[que_id] = field
|
|
29474
|
+
for sub in field.get("subfields") or []:
|
|
29475
|
+
if isinstance(sub, dict):
|
|
29476
|
+
collect(sub)
|
|
29477
|
+
|
|
29478
|
+
for field in fields:
|
|
29479
|
+
if isinstance(field, dict):
|
|
29480
|
+
collect(field)
|
|
29481
|
+
return index
|
|
29482
|
+
|
|
29483
|
+
|
|
29484
|
+
def _collect_field_id_to_name(schema: dict[str, Any]) -> dict[int, str]:
|
|
29485
|
+
"""从 form schema 收集 queId→queTitle(含子字段/段落内字段;同名时各 id 独立映射)。"""
|
|
29486
|
+
id_to_name: dict[int, str] = {}
|
|
29487
|
+
|
|
29488
|
+
def collect(question: dict[str, Any]) -> None:
|
|
29489
|
+
que_id = _coerce_positive_int(question.get("queId"))
|
|
29490
|
+
if que_id is not None and que_id not in id_to_name:
|
|
29491
|
+
title = str(question.get("queTitle") or "").strip()
|
|
29492
|
+
if title:
|
|
29493
|
+
id_to_name[que_id] = title
|
|
29494
|
+
for key in ("subQuestions", "innerQuestions"):
|
|
29495
|
+
for child in question.get(key) or []:
|
|
29496
|
+
if isinstance(child, list):
|
|
29497
|
+
for inner in child:
|
|
29498
|
+
if isinstance(inner, dict):
|
|
29499
|
+
collect(inner)
|
|
29500
|
+
elif isinstance(child, dict):
|
|
29501
|
+
collect(child)
|
|
29502
|
+
|
|
29503
|
+
for base_question in schema.get("baseQues", []) or []:
|
|
29504
|
+
if isinstance(base_question, dict):
|
|
29505
|
+
collect(base_question)
|
|
29506
|
+
for line in schema.get("formQues", []) or []:
|
|
29507
|
+
for question in (line if isinstance(line, list) else [line]):
|
|
29508
|
+
if isinstance(question, dict):
|
|
29509
|
+
collect(question)
|
|
29510
|
+
return id_to_name
|
|
29511
|
+
|
|
29512
|
+
|
|
29513
|
+
def _resolve_field_ref_preserving(
|
|
29514
|
+
config: dict[str, Any],
|
|
29515
|
+
config_key: str,
|
|
29516
|
+
name: str | None,
|
|
29517
|
+
field_map: dict[str, int],
|
|
29518
|
+
id_to_name: dict[int, str],
|
|
29519
|
+
) -> int | None:
|
|
29520
|
+
"""同名字段歧义消解(字段引用类:card title/cover、board group 等)。
|
|
29521
|
+
|
|
29522
|
+
契约按字段名引用,同 form 内同名字段时按名解析会取到错误的一个。
|
|
29523
|
+
UPDATE 场景读面原 queId 可用:若原 queId 的字段名与声明名一致,保留原 queId(保真);
|
|
29524
|
+
只有声明名与原值名字不同(用户真改了字段)才按名重新解析。
|
|
29525
|
+
"""
|
|
29526
|
+
if not name:
|
|
29527
|
+
return None
|
|
29528
|
+
original = _coerce_positive_int(config.get(config_key))
|
|
29529
|
+
if original is not None and id_to_name.get(original) == name:
|
|
29530
|
+
return original
|
|
29531
|
+
return field_map.get(name)
|
|
29532
|
+
|
|
29533
|
+
|
|
28736
29534
|
def _view_upsert_payload_from_existing_view(
|
|
28737
29535
|
*,
|
|
28738
29536
|
config: dict[str, Any],
|
|
@@ -28742,25 +29540,28 @@ def _view_upsert_payload_from_existing_view(
|
|
|
28742
29540
|
) -> dict[str, Any]:
|
|
28743
29541
|
view_type = _normalize_view_type_name(config.get("viewgraphType") or summary.get("type"))
|
|
28744
29542
|
configured_columns = [
|
|
28745
|
-
|
|
28746
|
-
for
|
|
28747
|
-
if
|
|
29543
|
+
que_id
|
|
29544
|
+
for item in (config.get("viewgraphQueIds") or [])
|
|
29545
|
+
if (que_id := _coerce_positive_int(item)) is not None
|
|
29546
|
+
and (field_names_by_id.get(que_id) or "") not in _KNOWN_SYSTEM_VIEW_COLUMNS
|
|
28748
29547
|
]
|
|
28749
29548
|
columns = [
|
|
28750
|
-
|
|
28751
|
-
for
|
|
28752
|
-
if
|
|
29549
|
+
que_id
|
|
29550
|
+
for item in (summary.get("apply_column_ids") or summary.get("columns") or [])
|
|
29551
|
+
if (que_id := _coerce_positive_int(item)) is not None
|
|
29552
|
+
and (field_names_by_id.get(que_id) or "") not in _KNOWN_SYSTEM_VIEW_COLUMNS
|
|
28753
29553
|
]
|
|
28754
29554
|
if configured_columns and (not columns or len(configured_columns) >= len(columns)):
|
|
28755
29555
|
columns = configured_columns
|
|
28756
29556
|
display_config = summary.get("display_config") if isinstance(summary.get("display_config"), dict) else {}
|
|
28757
|
-
|
|
28758
|
-
|
|
29557
|
+
group_que_id = _coerce_positive_int(config.get("groupQueId") or summary.get("group_que_id") or display_config.get("group_que_id"))
|
|
29558
|
+
title_que_id = _coerce_positive_int(config.get("titleQue") or display_config.get("title_que_id"))
|
|
28759
29559
|
gantt_config = config.get("viewgraphGanttConfigVO") if isinstance(config.get("viewgraphGanttConfigVO"), dict) else {}
|
|
28760
|
-
|
|
28761
|
-
|
|
28762
|
-
|
|
29560
|
+
start_que_id = _coerce_positive_int(gantt_config.get("startTimeQueId"))
|
|
29561
|
+
end_que_id = _coerce_positive_int(gantt_config.get("endTimeQueId"))
|
|
29562
|
+
gantt_title_que_id = _coerce_positive_int(gantt_config.get("titleQueId"))
|
|
28763
29563
|
query_conditions = _extract_view_query_conditions_config(config)
|
|
29564
|
+
query_conditions["rows"] = query_conditions.get("row_field_ids") or []
|
|
28764
29565
|
query_conditions.pop("row_field_ids", None)
|
|
28765
29566
|
associated_resources = _extract_view_associated_resources_config(config)
|
|
28766
29567
|
payload: dict[str, Any] = {
|
|
@@ -28768,7 +29569,6 @@ def _view_upsert_payload_from_existing_view(
|
|
|
28768
29569
|
"view_key": view_key,
|
|
28769
29570
|
"type": view_type or "table",
|
|
28770
29571
|
"columns": columns,
|
|
28771
|
-
"group_by": group_by,
|
|
28772
29572
|
"filters": [],
|
|
28773
29573
|
"query_conditions": query_conditions,
|
|
28774
29574
|
"associated_resources": {
|
|
@@ -28777,15 +29577,17 @@ def _view_upsert_payload_from_existing_view(
|
|
|
28777
29577
|
"associated_item_ids": list(associated_resources.get("configured_associated_item_ids") or []),
|
|
28778
29578
|
},
|
|
28779
29579
|
}
|
|
28780
|
-
if
|
|
28781
|
-
payload["
|
|
29580
|
+
if group_que_id is not None:
|
|
29581
|
+
payload["group_que_id"] = group_que_id
|
|
29582
|
+
if title_que_id is not None:
|
|
29583
|
+
payload["title_que_id"] = title_que_id
|
|
28782
29584
|
if view_type == "gantt":
|
|
28783
|
-
if
|
|
28784
|
-
payload["
|
|
28785
|
-
if
|
|
28786
|
-
payload["
|
|
28787
|
-
if
|
|
28788
|
-
payload["
|
|
29585
|
+
if start_que_id is not None:
|
|
29586
|
+
payload["start_que_id"] = start_que_id
|
|
29587
|
+
if end_que_id is not None:
|
|
29588
|
+
payload["end_que_id"] = end_que_id
|
|
29589
|
+
if gantt_title_que_id is not None:
|
|
29590
|
+
payload["title_que_id"] = gantt_title_que_id
|
|
28789
29591
|
return payload
|
|
28790
29592
|
|
|
28791
29593
|
|
|
@@ -28796,7 +29598,7 @@ def _first_present(mapping: dict[str, Any], *keys: str) -> Any:
|
|
|
28796
29598
|
return None
|
|
28797
29599
|
|
|
28798
29600
|
|
|
28799
|
-
def
|
|
29601
|
+
def _build_view_creation_payload(
|
|
28800
29602
|
*,
|
|
28801
29603
|
app_key: str,
|
|
28802
29604
|
base_info: dict[str, Any],
|
|
@@ -28804,23 +29606,24 @@ def _build_view_create_payload(
|
|
|
28804
29606
|
patch: ViewUpsertPatch,
|
|
28805
29607
|
ordinal: int,
|
|
28806
29608
|
view_filters: list[list[dict[str, Any]]] | None,
|
|
28807
|
-
|
|
29609
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
28808
29610
|
auth_override: dict[str, Any] | None = None,
|
|
28809
29611
|
explicit_button_dtos: list[dict[str, Any]] | None = None,
|
|
28810
29612
|
query_condition_payload: dict[str, Any] | None = None,
|
|
28811
29613
|
associated_resources_payload: dict[str, Any] | None = None,
|
|
29614
|
+
hierarchy_filter_groups: list[list[dict[str, Any]]] | None = None,
|
|
28812
29615
|
) -> JSONObject:
|
|
28813
29616
|
entity = _entity_spec_from_app(base_info=base_info, schema=schema, views=None)
|
|
28814
29617
|
parsed_schema = _parse_schema(schema)
|
|
28815
|
-
|
|
28816
|
-
field_ids = [
|
|
29618
|
+
visible_que_ids = _resolve_view_visible_que_ids(patch)
|
|
29619
|
+
field_ids = [f"field_{que}" for que in visible_que_ids]
|
|
28817
29620
|
gantt_config = _build_public_gantt_payload(parsed_schema["fields"], extract_field_map(schema), patch)
|
|
28818
29621
|
view_spec = ViewSpec(
|
|
28819
29622
|
view_id=_slugify(patch.name, default=f"view_{uuid4().hex[:6]}"),
|
|
28820
29623
|
name=patch.name,
|
|
28821
29624
|
type=patch.type.value,
|
|
28822
29625
|
field_ids=field_ids,
|
|
28823
|
-
group_by_field_id=
|
|
29626
|
+
group_by_field_id=f"field_{patch.group_que_id}" if patch.group_que_id else None,
|
|
28824
29627
|
config=gantt_config,
|
|
28825
29628
|
)
|
|
28826
29629
|
from ..solution.spec_models import EntitySpec
|
|
@@ -28829,7 +29632,6 @@ def _build_view_create_payload(
|
|
|
28829
29632
|
compiled = compile_views(EntitySpec.model_validate({**entity, "views": [view_spec.model_dump(mode="json")]}))[0]
|
|
28830
29633
|
field_map = extract_field_map(schema)
|
|
28831
29634
|
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
29635
|
payload["appKey"] = app_key
|
|
28834
29636
|
payload["ordinal"] = ordinal
|
|
28835
29637
|
payload["viewgraphQueIds"] = visible_que_ids
|
|
@@ -28839,19 +29641,27 @@ def _build_view_create_payload(
|
|
|
28839
29641
|
payload.setdefault("viewgraphSorts", [{"queId": 0, "beingSortAscend": True, "queType": 8}])
|
|
28840
29642
|
if patch.type.value in {"card", "board", "gantt"}:
|
|
28841
29643
|
payload["beingShowTitleQue"] = True
|
|
28842
|
-
payload["titleQue"] =
|
|
29644
|
+
payload["titleQue"] = (
|
|
29645
|
+
_declared_title_que_id(patch)
|
|
29646
|
+
or gantt_config.get("titleQueId")
|
|
29647
|
+
or (visible_que_ids[0] if visible_que_ids else None)
|
|
29648
|
+
)
|
|
28843
29649
|
if patch.type.value == "board":
|
|
28844
|
-
payload["groupQueId"] =
|
|
29650
|
+
payload["groupQueId"] = patch.group_que_id
|
|
28845
29651
|
return _hydrate_view_backend_payload(
|
|
28846
29652
|
payload=payload,
|
|
28847
29653
|
view_type=patch.type.value,
|
|
28848
29654
|
visible_que_id_values=visible_que_ids,
|
|
28849
|
-
group_que_id=
|
|
29655
|
+
group_que_id=patch.group_que_id,
|
|
28850
29656
|
view_filters=view_filters,
|
|
28851
29657
|
gantt_payload=gantt_config,
|
|
28852
29658
|
button_config_dtos=explicit_button_dtos,
|
|
28853
29659
|
query_condition_payload=query_condition_payload,
|
|
28854
29660
|
associated_resources_payload=associated_resources_payload,
|
|
29661
|
+
patch=patch,
|
|
29662
|
+
field_map=field_map,
|
|
29663
|
+
fields_by_que_id=fields_by_que_id,
|
|
29664
|
+
hierarchy_filter_groups=hierarchy_filter_groups,
|
|
28855
29665
|
)
|
|
28856
29666
|
|
|
28857
29667
|
|
|
@@ -28989,11 +29799,28 @@ def _normalize_view_type_name(value: Any) -> str:
|
|
|
28989
29799
|
return "gantt"
|
|
28990
29800
|
if "board" in normalized:
|
|
28991
29801
|
return "board"
|
|
29802
|
+
if "hierarchy" in normalized:
|
|
29803
|
+
return "hierarchy"
|
|
29804
|
+
if "calendar" in normalized:
|
|
29805
|
+
return "calendar"
|
|
28992
29806
|
if "card" in normalized:
|
|
28993
29807
|
return "card"
|
|
28994
29808
|
return "table"
|
|
28995
29809
|
|
|
28996
29810
|
|
|
29811
|
+
def _declared_title_que_id(patch: ViewUpsertPatch) -> int | None:
|
|
29812
|
+
"""声明优先的标题字段解析:类型专属配置(card/gantt)→ 顶层 title_que_id。
|
|
29813
|
+
|
|
29814
|
+
存量视图的标题字段可能不在展示列集内(如系统字段/独立引用),不得用「第一列」
|
|
29815
|
+
覆盖声明值——第一列只作为无声明时的回退(#148 r2 一致率 titleQue 26 例根因)。
|
|
29816
|
+
"""
|
|
29817
|
+
if patch.card is not None and patch.card.title_que_id:
|
|
29818
|
+
return patch.card.title_que_id
|
|
29819
|
+
if patch.gantt is not None and patch.gantt.title_que_id:
|
|
29820
|
+
return patch.gantt.title_que_id
|
|
29821
|
+
return patch.title_que_id or None
|
|
29822
|
+
|
|
29823
|
+
|
|
28997
29824
|
def _build_view_update_payload(
|
|
28998
29825
|
*,
|
|
28999
29826
|
views: ViewTools,
|
|
@@ -29002,20 +29829,20 @@ def _build_view_update_payload(
|
|
|
29002
29829
|
schema: dict[str, Any],
|
|
29003
29830
|
patch: ViewUpsertPatch,
|
|
29004
29831
|
view_filters: list[list[dict[str, Any]]] | None,
|
|
29005
|
-
|
|
29832
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
29006
29833
|
auth_override: dict[str, Any] | None = None,
|
|
29007
29834
|
explicit_button_dtos: list[dict[str, Any]] | None = None,
|
|
29008
29835
|
query_condition_payload: dict[str, Any] | None = None,
|
|
29009
29836
|
associated_resources_payload: dict[str, Any] | None = None,
|
|
29837
|
+
hierarchy_filter_groups: list[list[dict[str, Any]]] | None = None,
|
|
29010
29838
|
) -> JSONObject:
|
|
29011
29839
|
config_response = views.view_get_config(profile=profile, viewgraph_key=source_viewgraph_key)
|
|
29012
29840
|
config = config_response.get("result") if isinstance(config_response.get("result"), dict) else {}
|
|
29013
29841
|
payload = deepcopy(config)
|
|
29014
29842
|
parsed_schema = _parse_schema(schema)
|
|
29015
29843
|
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]
|
|
29844
|
+
visible_que_ids = _resolve_view_visible_que_ids(patch)
|
|
29845
|
+
visible_que_id_values = list(visible_que_ids)
|
|
29019
29846
|
gantt_payload = _build_public_gantt_payload(parsed_schema["fields"], field_map, patch)
|
|
29020
29847
|
|
|
29021
29848
|
for key in (
|
|
@@ -29037,8 +29864,25 @@ def _build_view_update_payload(
|
|
|
29037
29864
|
payload["auth"] = deepcopy(auth_override if isinstance(auth_override, dict) else payload.get("auth") or default_member_auth())
|
|
29038
29865
|
if "viewName" in payload:
|
|
29039
29866
|
payload["viewName"] = patch.name
|
|
29040
|
-
|
|
29041
|
-
|
|
29867
|
+
# 保留系统列在业务列之间的位置;业务列顺序和增删仍由声明决定。
|
|
29868
|
+
existing_que_ids = dict.fromkeys(
|
|
29869
|
+
que_id
|
|
29870
|
+
for que_id in (_coerce_any_int(raw) for raw in (config.get("viewgraphQueIds") or []))
|
|
29871
|
+
if que_id is not None
|
|
29872
|
+
)
|
|
29873
|
+
business_que_ids = iter(que_id for que_id in visible_que_id_values if que_id > 5)
|
|
29874
|
+
merged_que_ids: list[int] = []
|
|
29875
|
+
for que_id in existing_que_ids:
|
|
29876
|
+
if que_id <= 5:
|
|
29877
|
+
merged_que_ids.append(que_id)
|
|
29878
|
+
elif que_id in visible_que_id_values:
|
|
29879
|
+
merged_que_ids.append(next(business_que_ids))
|
|
29880
|
+
merged_que_ids.extend(business_que_ids)
|
|
29881
|
+
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)
|
|
29882
|
+
payload["viewgraphQueIds"] = merged_que_ids
|
|
29883
|
+
payload["viewgraphQuestions"] = _build_viewgraph_questions(
|
|
29884
|
+
schema, merged_que_ids, existing_by_que_id=_flatten_viewgraph_questions(config.get("viewgraphQuestions"))
|
|
29885
|
+
)
|
|
29042
29886
|
payload.setdefault("sortType", "defaultSort")
|
|
29043
29887
|
payload.setdefault("viewgraphSorts", [{"queId": 0, "beingSortAscend": True, "queType": 8}])
|
|
29044
29888
|
payload.setdefault("beingPinNavigate", True)
|
|
@@ -29062,31 +29906,93 @@ def _build_view_update_payload(
|
|
|
29062
29906
|
elif normalized_type == "card":
|
|
29063
29907
|
payload["viewgraphType"] = "cardView"
|
|
29064
29908
|
payload["beingShowTitleQue"] = True
|
|
29065
|
-
payload["titleQue"] = visible_que_id_values[0] if visible_que_id_values else None
|
|
29909
|
+
payload["titleQue"] = _declared_title_que_id(patch) or (visible_que_id_values[0] if visible_que_id_values else None)
|
|
29066
29910
|
payload.pop("groupQueId", None)
|
|
29067
29911
|
elif normalized_type == "board":
|
|
29068
29912
|
payload["viewgraphType"] = "boardView"
|
|
29069
29913
|
payload["beingShowTitleQue"] = True
|
|
29070
|
-
payload["titleQue"] = visible_que_id_values[0] if visible_que_id_values else None
|
|
29071
|
-
payload["groupQueId"] =
|
|
29914
|
+
payload["titleQue"] = _declared_title_que_id(patch) or (visible_que_id_values[0] if visible_que_id_values else None)
|
|
29915
|
+
payload["groupQueId"] = patch.group_que_id
|
|
29072
29916
|
elif normalized_type == "gantt":
|
|
29073
29917
|
payload["viewgraphType"] = "ganttView"
|
|
29074
29918
|
payload["beingShowTitleQue"] = True
|
|
29075
|
-
payload["titleQue"] = gantt_payload.get("titleQueId") or (visible_que_id_values[0] if visible_que_id_values else None)
|
|
29919
|
+
payload["titleQue"] = _declared_title_que_id(patch) or gantt_payload.get("titleQueId") or (visible_que_id_values[0] if visible_que_id_values else None)
|
|
29920
|
+
payload.pop("groupQueId", None)
|
|
29921
|
+
elif normalized_type == "hierarchy":
|
|
29922
|
+
payload["viewgraphType"] = "hierarchyView"
|
|
29923
|
+
payload["beingShowTitleQue"] = True
|
|
29924
|
+
payload.pop("groupQueId", None)
|
|
29925
|
+
elif normalized_type == "calendar":
|
|
29926
|
+
payload["viewgraphType"] = "calendarView"
|
|
29927
|
+
payload["beingShowTitleQue"] = True
|
|
29928
|
+
payload.pop("groupQueId", None)
|
|
29929
|
+
elif normalized_type == "hierarchy":
|
|
29930
|
+
payload["viewgraphType"] = "hierarchyView"
|
|
29931
|
+
payload["beingShowTitleQue"] = True
|
|
29932
|
+
payload.pop("groupQueId", None)
|
|
29933
|
+
elif normalized_type == "calendar":
|
|
29934
|
+
payload["viewgraphType"] = "calendarView"
|
|
29935
|
+
payload["beingShowTitleQue"] = True
|
|
29076
29936
|
payload.pop("groupQueId", None)
|
|
29077
29937
|
return _hydrate_view_backend_payload(
|
|
29078
29938
|
payload=payload,
|
|
29079
29939
|
view_type=normalized_type,
|
|
29080
29940
|
visible_que_id_values=visible_que_id_values,
|
|
29081
|
-
group_que_id=
|
|
29941
|
+
group_que_id=patch.group_que_id,
|
|
29082
29942
|
view_filters=view_filters,
|
|
29083
29943
|
gantt_payload=gantt_payload,
|
|
29084
29944
|
button_config_dtos=button_config_dtos,
|
|
29085
29945
|
query_condition_payload=query_condition_payload,
|
|
29086
29946
|
associated_resources_payload=associated_resources_payload,
|
|
29947
|
+
patch=patch,
|
|
29948
|
+
field_map=field_map,
|
|
29949
|
+
fields_by_que_id=fields_by_que_id,
|
|
29950
|
+
hierarchy_filter_groups=hierarchy_filter_groups,
|
|
29951
|
+
schema=schema,
|
|
29087
29952
|
)
|
|
29088
29953
|
|
|
29089
29954
|
|
|
29955
|
+
# 系统列 queId→公开名(后端语义;读面展示与 viewgraphQueIds 透传时的名称解析)
|
|
29956
|
+
_SYSTEM_VIEW_QUE_NAMES = {
|
|
29957
|
+
0: "编号",
|
|
29958
|
+
1: "提交人",
|
|
29959
|
+
2: "提交时间",
|
|
29960
|
+
3: "更新时间",
|
|
29961
|
+
4: "流程标题",
|
|
29962
|
+
5: "当前流程状态",
|
|
29963
|
+
-17: "数据ID",
|
|
29964
|
+
-19: "删除时间",
|
|
29965
|
+
-20: "删除人",
|
|
29966
|
+
-21: "处理节点",
|
|
29967
|
+
-22: "抄送节点",
|
|
29968
|
+
-23: "处理时间",
|
|
29969
|
+
-24: "抄送时间",
|
|
29970
|
+
-25: "接收时间",
|
|
29971
|
+
}
|
|
29972
|
+
|
|
29973
|
+
_SYSTEM_VIEW_QUE_IDS = frozenset(
|
|
29974
|
+
{
|
|
29975
|
+
*_SYSTEM_VIEW_QUE_NAMES,
|
|
29976
|
+
-1,
|
|
29977
|
+
-2,
|
|
29978
|
+
-3,
|
|
29979
|
+
-4,
|
|
29980
|
+
-5,
|
|
29981
|
+
-6,
|
|
29982
|
+
-7,
|
|
29983
|
+
-8,
|
|
29984
|
+
-18,
|
|
29985
|
+
-30,
|
|
29986
|
+
-31,
|
|
29987
|
+
}
|
|
29988
|
+
)
|
|
29989
|
+
|
|
29990
|
+
|
|
29991
|
+
def _is_known_system_view_que_id(value: Any) -> bool:
|
|
29992
|
+
que_id = _coerce_any_int(value)
|
|
29993
|
+
return que_id in _SYSTEM_VIEW_QUE_IDS if que_id is not None else False
|
|
29994
|
+
|
|
29995
|
+
|
|
29090
29996
|
_KNOWN_SYSTEM_VIEW_COLUMNS = {
|
|
29091
29997
|
"编号",
|
|
29092
29998
|
"当前流程状态",
|
|
@@ -29110,16 +30016,16 @@ def _build_minimal_view_payload(
|
|
|
29110
30016
|
patch: ViewUpsertPatch,
|
|
29111
30017
|
ordinal: int,
|
|
29112
30018
|
view_filters: list[list[dict[str, Any]]] | None,
|
|
29113
|
-
|
|
30019
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
29114
30020
|
auth_override: dict[str, Any] | None = None,
|
|
29115
30021
|
explicit_button_dtos: list[dict[str, Any]] | None = None,
|
|
29116
30022
|
query_condition_payload: dict[str, Any] | None = None,
|
|
29117
30023
|
associated_resources_payload: dict[str, Any] | None = None,
|
|
30024
|
+
hierarchy_filter_groups: list[list[dict[str, Any]]] | None = None,
|
|
29118
30025
|
) -> JSONObject:
|
|
29119
30026
|
field_map = extract_field_map(schema)
|
|
29120
30027
|
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]
|
|
30028
|
+
visible_que_id_values = _resolve_view_visible_que_ids(patch)
|
|
29123
30029
|
gantt_payload = _build_public_gantt_payload(parsed_schema["fields"], field_map, patch)
|
|
29124
30030
|
payload: JSONObject = {
|
|
29125
30031
|
"appKey": app_key,
|
|
@@ -29129,6 +30035,8 @@ def _build_minimal_view_payload(
|
|
|
29129
30035
|
"card": "cardView",
|
|
29130
30036
|
"board": "boardView",
|
|
29131
30037
|
"gantt": "ganttView",
|
|
30038
|
+
"hierarchy": "hierarchyView",
|
|
30039
|
+
"calendar": "calendarView",
|
|
29132
30040
|
}[patch.type.value],
|
|
29133
30041
|
"ordinal": ordinal,
|
|
29134
30042
|
"viewgraphQueIds": visible_que_id_values,
|
|
@@ -29139,17 +30047,21 @@ def _build_minimal_view_payload(
|
|
|
29139
30047
|
payload["beingShowTitleQue"] = True
|
|
29140
30048
|
payload["titleQue"] = gantt_payload.get("titleQueId") or (visible_que_id_values[0] if visible_que_id_values else None)
|
|
29141
30049
|
if patch.type.value == "board":
|
|
29142
|
-
payload["groupQueId"] =
|
|
30050
|
+
payload["groupQueId"] = patch.group_que_id
|
|
29143
30051
|
return _hydrate_view_backend_payload(
|
|
29144
30052
|
payload=payload,
|
|
29145
30053
|
view_type=patch.type.value,
|
|
29146
30054
|
visible_que_id_values=visible_que_id_values,
|
|
29147
|
-
group_que_id=
|
|
30055
|
+
group_que_id=patch.group_que_id,
|
|
29148
30056
|
view_filters=view_filters,
|
|
29149
30057
|
gantt_payload=gantt_payload,
|
|
29150
30058
|
button_config_dtos=explicit_button_dtos,
|
|
29151
30059
|
query_condition_payload=query_condition_payload,
|
|
29152
30060
|
associated_resources_payload=associated_resources_payload,
|
|
30061
|
+
patch=patch,
|
|
30062
|
+
field_map=field_map,
|
|
30063
|
+
fields_by_que_id=fields_by_que_id,
|
|
30064
|
+
hierarchy_filter_groups=hierarchy_filter_groups,
|
|
29153
30065
|
)
|
|
29154
30066
|
|
|
29155
30067
|
|
|
@@ -29164,6 +30076,11 @@ def _hydrate_view_backend_payload(
|
|
|
29164
30076
|
button_config_dtos: list[dict[str, Any]] | None = None,
|
|
29165
30077
|
query_condition_payload: dict[str, Any] | None = None,
|
|
29166
30078
|
associated_resources_payload: dict[str, Any] | None = None,
|
|
30079
|
+
patch: ViewUpsertPatch | None = None,
|
|
30080
|
+
field_map: dict[str, int] | None = None,
|
|
30081
|
+
fields_by_que_id: dict[str, dict[str, Any]] | None = None,
|
|
30082
|
+
hierarchy_filter_groups: list[list[dict[str, Any]]] | None = None,
|
|
30083
|
+
schema: dict[str, Any] | None = None,
|
|
29167
30084
|
) -> JSONObject:
|
|
29168
30085
|
data = deepcopy(payload)
|
|
29169
30086
|
data.setdefault("beingPinNavigate", True)
|
|
@@ -29233,26 +30150,239 @@ def _hydrate_view_backend_payload(
|
|
|
29233
30150
|
data["titleQue"] = (gantt_payload or {}).get("titleQueId") or (visible_que_id_values[0] if visible_que_id_values else None)
|
|
29234
30151
|
data["groupQueId"] = None
|
|
29235
30152
|
data["viewgraphGanttConfigVO"] = deepcopy(gantt_payload) if gantt_payload else None
|
|
30153
|
+
elif view_type == "hierarchy":
|
|
30154
|
+
data["viewgraphType"] = "hierarchyView"
|
|
30155
|
+
data["beingShowTitleQue"] = True
|
|
30156
|
+
data["groupQueId"] = None
|
|
30157
|
+
data["viewgraphGanttConfigVO"] = None
|
|
30158
|
+
elif view_type == "calendar":
|
|
30159
|
+
data["viewgraphType"] = "calendarView"
|
|
30160
|
+
data["beingShowTitleQue"] = True
|
|
30161
|
+
data["groupQueId"] = None
|
|
30162
|
+
data["viewgraphGanttConfigVO"] = None
|
|
29236
30163
|
if view_filters is not None:
|
|
29237
30164
|
data["viewgraphLimit"] = deepcopy(view_filters)
|
|
29238
30165
|
data["viewgraphLimitType"] = 1
|
|
29239
30166
|
data["viewgraphLimitFormula"] = ""
|
|
29240
30167
|
data["dataPermissionType"] = "CUSTOM"
|
|
29241
30168
|
data["dataScope"] = "CUSTOM" if view_filters else "ALL"
|
|
30169
|
+
if patch is not None:
|
|
30170
|
+
_apply_view_config_groups(
|
|
30171
|
+
data,
|
|
30172
|
+
patch=patch,
|
|
30173
|
+
view_type=view_type,
|
|
30174
|
+
visible_que_id_values=visible_que_id_values,
|
|
30175
|
+
field_map=field_map or {},
|
|
30176
|
+
fields_by_que_id=fields_by_que_id or {},
|
|
30177
|
+
hierarchy_filter_groups=hierarchy_filter_groups,
|
|
30178
|
+
config=payload if isinstance(payload, dict) else None,
|
|
30179
|
+
id_to_name=_collect_field_id_to_name(schema) if isinstance(schema, dict) else None,
|
|
30180
|
+
)
|
|
29242
30181
|
_apply_view_query_conditions_payload(data, query_condition_payload)
|
|
29243
30182
|
_apply_view_associated_resources_payload(data, associated_resources_payload)
|
|
29244
30183
|
return data
|
|
29245
30184
|
|
|
29246
30185
|
|
|
29247
|
-
|
|
29248
|
-
|
|
29249
|
-
|
|
29250
|
-
|
|
29251
|
-
|
|
29252
|
-
|
|
30186
|
+
_DATA_PERMISSION_SCOPE_MAP = {
|
|
30187
|
+
"all": "ALL",
|
|
30188
|
+
"oneself": "ONESELF",
|
|
30189
|
+
"dept": "DEPT",
|
|
30190
|
+
"subordinate": "SUBORDINATE",
|
|
30191
|
+
"custom": "CUSTOM",
|
|
30192
|
+
}
|
|
30193
|
+
|
|
30194
|
+
|
|
30195
|
+
def _apply_view_config_groups(
|
|
30196
|
+
data: JSONObject,
|
|
30197
|
+
*,
|
|
30198
|
+
patch: ViewUpsertPatch,
|
|
30199
|
+
view_type: str,
|
|
30200
|
+
visible_que_id_values: list[int],
|
|
30201
|
+
field_map: dict[str, int],
|
|
30202
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
30203
|
+
hierarchy_filter_groups: list[list[dict[str, Any]]] | None,
|
|
30204
|
+
config: dict[str, Any] | None = None,
|
|
30205
|
+
id_to_name: dict[int, str] | None = None,
|
|
30206
|
+
) -> None:
|
|
30207
|
+
"""编译 complete 档新字段组:data_permission/sort/card/hierarchy/display/record/share。"""
|
|
30208
|
+
if patch.data_permission is not None:
|
|
30209
|
+
data["dataPermissionType"] = "CONFIG" if patch.data_permission.type == "config" else "CUSTOM"
|
|
30210
|
+
data["dataScope"] = _DATA_PERMISSION_SCOPE_MAP.get(patch.data_permission.scope, "ALL")
|
|
30211
|
+
if patch.sort is not None:
|
|
30212
|
+
if patch.sort:
|
|
30213
|
+
sorts: list[dict[str, Any]] = []
|
|
30214
|
+
for rule in patch.sort:
|
|
30215
|
+
field = fields_by_que_id.get(rule.que_id) or {}
|
|
30216
|
+
que_type = _coerce_positive_int(field.get("que_type")) or FIELD_TYPE_TO_QUESTION_TYPE.get(
|
|
30217
|
+
str(field.get("type") or FieldType.text.value), 2
|
|
30218
|
+
)
|
|
30219
|
+
sorts.append(
|
|
30220
|
+
{
|
|
30221
|
+
"queId": rule.que_id,
|
|
30222
|
+
"beingSortAscend": bool(rule.ascending),
|
|
30223
|
+
"queType": que_type,
|
|
30224
|
+
}
|
|
30225
|
+
)
|
|
30226
|
+
data["sortType"] = "customSort"
|
|
30227
|
+
data["viewgraphSorts"] = sorts
|
|
30228
|
+
else:
|
|
30229
|
+
data["sortType"] = "defaultSort"
|
|
30230
|
+
data["viewgraphSorts"] = [{"queId": 0, "beingSortAscend": True, "queType": 8}]
|
|
30231
|
+
card = patch.card
|
|
30232
|
+
config = config or {}
|
|
30233
|
+
id_to_name = id_to_name or {}
|
|
30234
|
+
if card is not None:
|
|
30235
|
+
if card.title_que_id is not None:
|
|
30236
|
+
data["titleQue"] = card.title_que_id
|
|
30237
|
+
if card.cover_que_id is not None:
|
|
30238
|
+
data["frontCoverQueId"] = card.cover_que_id
|
|
30239
|
+
if card.clipping_mode is not None:
|
|
30240
|
+
data["clippingMode"] = card.clipping_mode
|
|
30241
|
+
if card.image_adaption is not None:
|
|
30242
|
+
data["beingImageAdaption"] = card.image_adaption
|
|
30243
|
+
if card.show_field_title is not None:
|
|
30244
|
+
data["beingShowQueTitle"] = card.show_field_title
|
|
30245
|
+
if card.show_title is not None:
|
|
30246
|
+
data["beingShowTitleQue"] = card.show_title
|
|
30247
|
+
if card.show_cover is not None:
|
|
30248
|
+
data["beingShowCover"] = card.show_cover
|
|
30249
|
+
if card.group_que_id is not None:
|
|
30250
|
+
data["groupQueId"] = card.group_que_id
|
|
30251
|
+
if card.group_color is not None:
|
|
30252
|
+
data["beingGroupColor"] = card.group_color
|
|
30253
|
+
if patch.hierarchy is not None and view_type == "hierarchy":
|
|
30254
|
+
data["viewgraphHierarchyConfigVO"] = {
|
|
30255
|
+
"titleQueId": patch.hierarchy.title_que_id,
|
|
30256
|
+
"childrenQueId": patch.hierarchy.children_que_id,
|
|
30257
|
+
"rootNodeFilter": deepcopy(hierarchy_filter_groups) if hierarchy_filter_groups is not None else [],
|
|
30258
|
+
}
|
|
30259
|
+
data["titleQue"] = patch.hierarchy.title_que_id
|
|
30260
|
+
if patch.calendar is not None and view_type == "calendar":
|
|
30261
|
+
data["viewgraphCalendarConfigVO"] = {
|
|
30262
|
+
"titleQueId": patch.calendar.title_que_id,
|
|
30263
|
+
"dateQueId": patch.calendar.date_que_id,
|
|
30264
|
+
"colorQueId": patch.calendar.color_que_id,
|
|
30265
|
+
}
|
|
30266
|
+
data["titleQue"] = patch.calendar.title_que_id
|
|
30267
|
+
display = patch.display
|
|
30268
|
+
if display is not None:
|
|
30269
|
+
if display.row_height is not None:
|
|
30270
|
+
data["defaultRowHigh"] = display.row_height
|
|
30271
|
+
if display.pin_navigate is not None:
|
|
30272
|
+
data["beingPinNavigate"] = display.pin_navigate
|
|
30273
|
+
if display.remark is not None:
|
|
30274
|
+
data["remark"] = display.remark
|
|
30275
|
+
record = patch.record
|
|
30276
|
+
if record is not None:
|
|
30277
|
+
if record.audit_visible is not None:
|
|
30278
|
+
data["beingAuditRecordVisible"] = record.audit_visible
|
|
30279
|
+
if record.data_log_visible is not None:
|
|
30280
|
+
data["beingDataLogVisible"] = record.data_log_visible
|
|
30281
|
+
if record.qrobot_record_visible is not None:
|
|
30282
|
+
data["beingQrobotRecordVisible"] = record.qrobot_record_visible
|
|
30283
|
+
if record.workflow_future_visible is not None:
|
|
30284
|
+
data["beingWorkflowNodeFutureListVisible"] = record.workflow_future_visible
|
|
30285
|
+
if record.comment is not None:
|
|
30286
|
+
data["beingCommentStatus"] = record.comment
|
|
30287
|
+
share = patch.share
|
|
30288
|
+
if share is not None:
|
|
30289
|
+
if share.need_pass is not None:
|
|
30290
|
+
data["beingNeedPass"] = share.need_pass
|
|
30291
|
+
data["needPass"] = share.need_pass
|
|
30292
|
+
if share.password is None:
|
|
30293
|
+
# 声明未带密码(提取语义:不回传敏感值)——显式清空 payload 里读面带来的旧密码;
|
|
30294
|
+
# 后端 isNotBlank(空)跳过校验且不更新密码列,原密码保留(Java AbstractViewgraphStrategicService:424)
|
|
30295
|
+
data["viewgraphPass"] = ""
|
|
30296
|
+
data.pop("pass", None)
|
|
30297
|
+
if share.password is not None:
|
|
30298
|
+
data["viewgraphPass"] = share.password
|
|
30299
|
+
if share.force_login is not None:
|
|
30300
|
+
data["beingForceLogin"] = share.force_login
|
|
30301
|
+
if share.print_enabled is not None:
|
|
30302
|
+
data["beingPrintStatus"] = share.print_enabled
|
|
30303
|
+
|
|
30304
|
+
|
|
30305
|
+
def _stale_view_reference_warning(
|
|
30306
|
+
*,
|
|
30307
|
+
scope: str,
|
|
30308
|
+
view_name: str,
|
|
30309
|
+
field_ids: list[Any] | None = None,
|
|
30310
|
+
values: list[Any] | None = None,
|
|
30311
|
+
option: bool = False,
|
|
30312
|
+
) -> dict[str, Any]:
|
|
30313
|
+
"""存量死引用(已删字段/已删选项)透传警告:后端保存视图不校验引用存在性并原样透传,
|
|
30314
|
+
CLI 同口径透传写回,仅以警告留痕,不再整视图拒绝(apply(get(S))≡S 重放兼容)。"""
|
|
30315
|
+
warning = _warning(
|
|
30316
|
+
"STALE_OPTION_REFERENCE" if option else "STALE_FIELD_REFERENCE",
|
|
30317
|
+
(
|
|
30318
|
+
f"view {scope} references option values not present in the current form; original values passed through (backend tolerates stale references)"
|
|
30319
|
+
if option
|
|
30320
|
+
else f"view {scope} references fields not present in the current form; ids passed through (backend tolerates stale references)"
|
|
30321
|
+
),
|
|
30322
|
+
scope=scope,
|
|
30323
|
+
view_name=view_name,
|
|
30324
|
+
)
|
|
30325
|
+
if field_ids:
|
|
30326
|
+
warning["field_ids"] = list(field_ids)
|
|
30327
|
+
if values:
|
|
30328
|
+
warning["values"] = list(values)
|
|
30329
|
+
return warning
|
|
30330
|
+
|
|
30331
|
+
|
|
30332
|
+
def _iter_view_config_group_field_names(patch: ViewUpsertPatch) -> list[tuple[str, list[int]]]:
|
|
30333
|
+
"""枚举 complete 档新字段组引用的 queId(用于 UNKNOWN_VIEW_FIELD 校验)。"""
|
|
30334
|
+
groups: list[tuple[str, list[int]]] = []
|
|
30335
|
+
if patch.sort:
|
|
30336
|
+
groups.append(("sort", [rule.que_id for rule in patch.sort]))
|
|
30337
|
+
if patch.card is not None:
|
|
30338
|
+
groups.append(
|
|
30339
|
+
(
|
|
30340
|
+
"card",
|
|
30341
|
+
[
|
|
30342
|
+
que
|
|
30343
|
+
for que in (patch.card.title_que_id, patch.card.cover_que_id, patch.card.group_que_id)
|
|
30344
|
+
if que
|
|
30345
|
+
],
|
|
30346
|
+
)
|
|
30347
|
+
)
|
|
30348
|
+
if patch.gantt is not None:
|
|
30349
|
+
gantt_ids = [
|
|
30350
|
+
que
|
|
30351
|
+
for que in (patch.gantt.title_que_id, patch.gantt.start_que_id, patch.gantt.end_que_id)
|
|
30352
|
+
if que
|
|
30353
|
+
]
|
|
30354
|
+
gantt_ids.extend(group.que_id for group in patch.gantt.groups or [])
|
|
30355
|
+
for dep in patch.gantt.dependencies or []:
|
|
30356
|
+
gantt_ids.extend([dep.dependency_que_id, dep.predecessor_que_id])
|
|
30357
|
+
if gantt_ids:
|
|
30358
|
+
groups.append(("gantt", gantt_ids))
|
|
30359
|
+
if patch.hierarchy is not None:
|
|
30360
|
+
groups.append(("hierarchy", [patch.hierarchy.title_que_id, patch.hierarchy.children_que_id]))
|
|
30361
|
+
if patch.calendar is not None:
|
|
30362
|
+
calendar_ids = [patch.calendar.title_que_id, patch.calendar.date_que_id]
|
|
30363
|
+
if patch.calendar.color_que_id is not None:
|
|
30364
|
+
calendar_ids.append(patch.calendar.color_que_id)
|
|
30365
|
+
groups.append(("calendar", calendar_ids))
|
|
30366
|
+
return groups
|
|
30367
|
+
|
|
30368
|
+
|
|
30369
|
+
|
|
30370
|
+
def _resolve_view_visible_que_ids(patch: ViewUpsertPatch) -> list[int]:
|
|
30371
|
+
"""收集视图引用的业务字段和已知系统字段 queId。"""
|
|
30372
|
+
ordered: list[int] = []
|
|
30373
|
+
extra_ids: list[int | None] = []
|
|
30374
|
+
if patch.hierarchy is not None:
|
|
30375
|
+
extra_ids.extend([patch.hierarchy.title_que_id, patch.hierarchy.children_que_id])
|
|
30376
|
+
if patch.calendar is not None:
|
|
30377
|
+
extra_ids.extend([patch.calendar.title_que_id, patch.calendar.date_que_id, patch.calendar.color_que_id])
|
|
30378
|
+
for value in [*patch.columns, patch.title_que_id, patch.start_que_id, patch.end_que_id, patch.group_que_id, *extra_ids]:
|
|
30379
|
+
que_id = _coerce_any_int(value)
|
|
30380
|
+
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:
|
|
30381
|
+
ordered.append(que_id)
|
|
29253
30382
|
return ordered
|
|
29254
30383
|
|
|
29255
30384
|
|
|
30385
|
+
|
|
29256
30386
|
def _build_public_gantt_payload(
|
|
29257
30387
|
fields: list[dict[str, Any]],
|
|
29258
30388
|
field_map: dict[str, int],
|
|
@@ -29260,22 +30390,35 @@ def _build_public_gantt_payload(
|
|
|
29260
30390
|
) -> dict[str, Any]:
|
|
29261
30391
|
if patch.type.value != "gantt":
|
|
29262
30392
|
return {}
|
|
29263
|
-
|
|
29264
|
-
|
|
29265
|
-
|
|
30393
|
+
gantt_config = patch.gantt
|
|
30394
|
+
title_que_id = (
|
|
30395
|
+
getattr(gantt_config, "title_que_id", None)
|
|
30396
|
+
or patch.title_que_id
|
|
30397
|
+
or (patch.columns[0] if patch.columns else None)
|
|
30398
|
+
)
|
|
30399
|
+
start_que_id = getattr(gantt_config, "start_que_id", None) or patch.start_que_id
|
|
30400
|
+
end_que_id = getattr(gantt_config, "end_que_id", None) or patch.end_que_id
|
|
30401
|
+
dependency = gantt_config.dependencies[0] if gantt_config is not None and gantt_config.dependencies else None
|
|
30402
|
+
auto_calibration = bool(getattr(gantt_config, "auto_calibration", False) or False)
|
|
29266
30403
|
return {
|
|
29267
|
-
"titleQueId":
|
|
29268
|
-
"startTimeQueId":
|
|
29269
|
-
"endTimeQueId":
|
|
29270
|
-
"defaultTimeDimension": "week",
|
|
29271
|
-
"ganttGroupVOList": [
|
|
30404
|
+
"titleQueId": title_que_id,
|
|
30405
|
+
"startTimeQueId": start_que_id,
|
|
30406
|
+
"endTimeQueId": end_que_id,
|
|
30407
|
+
"defaultTimeDimension": getattr(gantt_config, "default_time_dimension", None) or "week",
|
|
30408
|
+
"ganttGroupVOList": [
|
|
30409
|
+
{
|
|
30410
|
+
"groupQueId": group.que_id,
|
|
30411
|
+
"ordinal": group.ordinal if group.ordinal is not None else index,
|
|
30412
|
+
}
|
|
30413
|
+
for index, group in enumerate((gantt_config.groups if gantt_config is not None else []), start=1)
|
|
30414
|
+
],
|
|
29272
30415
|
"ganttDependencyVO": {
|
|
29273
|
-
"dependencyQueId": None,
|
|
29274
|
-
"predecessorTaskQueId": None,
|
|
29275
|
-
"
|
|
29276
|
-
"
|
|
29277
|
-
"endEndOptionId": None,
|
|
29278
|
-
"endStartOptionId": None,
|
|
30416
|
+
"dependencyQueId": dependency.dependency_que_id if dependency else None,
|
|
30417
|
+
"predecessorTaskQueId": dependency.predecessor_que_id if dependency else None,
|
|
30418
|
+
"startStartOptionId": dependency.start_start_option_id if dependency else None,
|
|
30419
|
+
"startEndOptionId": dependency.start_end_option_id if dependency else None,
|
|
30420
|
+
"endEndOptionId": dependency.end_end_option_id if dependency else None,
|
|
30421
|
+
"endStartOptionId": dependency.end_start_option_id if dependency else None,
|
|
29279
30422
|
},
|
|
29280
30423
|
"ganttAutoCalibrationVO": {
|
|
29281
30424
|
"autoCalibrationRuleVO": {
|
|
@@ -29286,15 +30429,15 @@ def _build_public_gantt_payload(
|
|
|
29286
30429
|
"endStartFinish": True,
|
|
29287
30430
|
"endEndFinish": False,
|
|
29288
30431
|
},
|
|
29289
|
-
"beingAutoCalibration":
|
|
29290
|
-
"userAutoCalibration":
|
|
30432
|
+
"beingAutoCalibration": auto_calibration,
|
|
30433
|
+
"userAutoCalibration": auto_calibration,
|
|
29291
30434
|
},
|
|
29292
30435
|
}
|
|
29293
30436
|
|
|
29294
30437
|
|
|
29295
30438
|
def _build_view_filter_groups(
|
|
29296
30439
|
*,
|
|
29297
|
-
|
|
30440
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
29298
30441
|
filters: list[Any],
|
|
29299
30442
|
) -> tuple[list[list[dict[str, Any]]], list[dict[str, Any]]]:
|
|
29300
30443
|
translated_rules: list[dict[str, Any]] = []
|
|
@@ -29304,14 +30447,14 @@ def _build_view_filter_groups(
|
|
|
29304
30447
|
raw_rule = raw_rule.model_dump(mode="json")
|
|
29305
30448
|
if not isinstance(raw_rule, dict):
|
|
29306
30449
|
continue
|
|
29307
|
-
|
|
29308
|
-
field =
|
|
30450
|
+
rule_que_id = _coerce_positive_int(raw_rule.get("que_id"))
|
|
30451
|
+
field = fields_by_que_id.get(rule_que_id) if rule_que_id is not None else None
|
|
29309
30452
|
if field is None:
|
|
29310
30453
|
issues.append(
|
|
29311
30454
|
{
|
|
29312
30455
|
"error_code": "UNKNOWN_VIEW_FIELD",
|
|
29313
|
-
"missing_fields": [
|
|
29314
|
-
"reason_path": "filters[].
|
|
30456
|
+
"missing_fields": [str(raw_rule.get("que_id") or "")],
|
|
30457
|
+
"reason_path": "filters[].que_id",
|
|
29315
30458
|
}
|
|
29316
30459
|
)
|
|
29317
30460
|
continue
|
|
@@ -29319,6 +30462,9 @@ def _build_view_filter_groups(
|
|
|
29319
30462
|
if issue:
|
|
29320
30463
|
issues.append(issue)
|
|
29321
30464
|
continue
|
|
30465
|
+
stale_warning = translated_rule.pop("_stale_warning", None)
|
|
30466
|
+
if stale_warning is not None:
|
|
30467
|
+
issues.append(stale_warning)
|
|
29322
30468
|
translated_rules.append(translated_rule)
|
|
29323
30469
|
return ([translated_rules] if translated_rules else []), issues
|
|
29324
30470
|
|
|
@@ -29329,9 +30475,9 @@ def _translate_view_filter_rule(*, field: dict[str, Any], rule: dict[str, Any])
|
|
|
29329
30475
|
field_type = str(field.get("type") or FieldType.text.value)
|
|
29330
30476
|
que_id = _coerce_positive_int(field.get("que_id")) or 0
|
|
29331
30477
|
que_type = _coerce_positive_int(field.get("que_type")) or FIELD_TYPE_TO_QUESTION_TYPE.get(field_type, 2)
|
|
29332
|
-
judge_values, judge_value_details,
|
|
29333
|
-
if
|
|
29334
|
-
return {},
|
|
30478
|
+
judge_values, judge_value_details, value_issue = _resolve_view_filter_values(field=field, values=values)
|
|
30479
|
+
if value_issue and value_issue.get("error_code"):
|
|
30480
|
+
return {}, value_issue
|
|
29335
30481
|
payload: dict[str, Any] = {
|
|
29336
30482
|
"queId": que_id,
|
|
29337
30483
|
"queTitle": str(field.get("name") or ""),
|
|
@@ -29363,6 +30509,9 @@ def _translate_view_filter_rule(*, field: dict[str, Any], rule: dict[str, Any])
|
|
|
29363
30509
|
elif operator == "not_empty":
|
|
29364
30510
|
payload["judgeType"] = JUDGE_UNEQUAL
|
|
29365
30511
|
payload["judgeValues"] = []
|
|
30512
|
+
if value_issue is not None:
|
|
30513
|
+
# 死选项透传:规则照常写入,warning 随 payload 携带由上层收集(_build_view_filter_groups 弹出)
|
|
30514
|
+
payload["_stale_warning"] = value_issue
|
|
29366
30515
|
return payload, None
|
|
29367
30516
|
|
|
29368
30517
|
|
|
@@ -29393,21 +30542,23 @@ def _resolve_view_filter_values(
|
|
|
29393
30542
|
unresolved_values.append(_stringify_condition_value(value))
|
|
29394
30543
|
continue
|
|
29395
30544
|
resolved_details.append({"id": resolved.get("id"), "value": str(resolved.get("value") or resolved.get("id"))})
|
|
29396
|
-
if unresolved_values:
|
|
30545
|
+
if unresolved_values and not resolved_details:
|
|
30546
|
+
# 全部值都解析不出(选项已删):原值透传(后端对存量死选项引用原样保存),
|
|
30547
|
+
# 以 STALE_OPTION_REFERENCE 警告留痕,不再整视图失败
|
|
29397
30548
|
return (
|
|
29398
|
-
[],
|
|
29399
|
-
|
|
30549
|
+
[_stringify_condition_value(value) for value in values],
|
|
30550
|
+
_build_view_filter_value_details(values),
|
|
29400
30551
|
{
|
|
29401
|
-
"
|
|
30552
|
+
"warning_code": "STALE_OPTION_REFERENCE",
|
|
29402
30553
|
"reason_path": "filters[].values",
|
|
29403
30554
|
"missing_fields": [],
|
|
29404
|
-
"allowed_values": {"filter_values": sorted(option_by_value.keys())},
|
|
29405
30555
|
"details": {
|
|
29406
30556
|
"field_name": str(field.get("name") or ""),
|
|
29407
30557
|
"unresolved_values": unresolved_values,
|
|
29408
30558
|
},
|
|
29409
30559
|
},
|
|
29410
30560
|
)
|
|
30561
|
+
# 部分值解析不出:剔除已删选项值继续写(兼容存量脏数据,不再整视图失败)
|
|
29411
30562
|
return (
|
|
29412
30563
|
[str(detail["id"]) for detail in resolved_details if detail.get("id") is not None],
|
|
29413
30564
|
[_coerce_view_filter_value_detail(detail) for detail in resolved_details],
|
|
@@ -29451,6 +30602,10 @@ def _build_view_filter_value_details(values: list[Any]) -> list[dict[str, Any]]:
|
|
|
29451
30602
|
details.append(_coerce_view_filter_value_detail({"id": item_id, "value": item_value if item_value is not None else str(item_id)}))
|
|
29452
30603
|
elif isinstance(value, int):
|
|
29453
30604
|
details.append(_coerce_view_filter_value_detail({"id": value, "value": str(value)}))
|
|
30605
|
+
elif isinstance(value, str):
|
|
30606
|
+
# text/number 字段:AnswerValue.id 是 Integer(选项 id),字符串值无 id;
|
|
30607
|
+
# 后端 formula getId()==null → 走 getValue() 分支构建 ""value"" 形式
|
|
30608
|
+
details.append(_coerce_view_filter_value_detail({"id": None, "value": value}))
|
|
29454
30609
|
return details
|
|
29455
30610
|
|
|
29456
30611
|
|
|
@@ -29617,11 +30772,11 @@ def _view_filter_groups_semantic_readback(expected: Any, actual: Any) -> tuple[b
|
|
|
29617
30772
|
def _public_view_filter_groups_from_match_rules(
|
|
29618
30773
|
groups: Any,
|
|
29619
30774
|
*,
|
|
29620
|
-
|
|
30775
|
+
fields_by_que_id: dict[str, dict[str, Any]],
|
|
29621
30776
|
) -> list[list[dict[str, Any]]]:
|
|
29622
30777
|
fields_by_que_id = {
|
|
29623
30778
|
_coerce_positive_int(field.get("que_id")): field
|
|
29624
|
-
for field in
|
|
30779
|
+
for field in fields_by_que_id.values()
|
|
29625
30780
|
if isinstance(field, dict) and _coerce_positive_int(field.get("que_id")) is not None
|
|
29626
30781
|
}
|
|
29627
30782
|
public_groups: list[list[dict[str, Any]]] = []
|
|
@@ -29632,7 +30787,7 @@ def _public_view_filter_groups_from_match_rules(
|
|
|
29632
30787
|
field = fields_by_que_id.get(que_id) or {}
|
|
29633
30788
|
values = _public_view_filter_rule_values(rule, field=field)
|
|
29634
30789
|
public_rule: dict[str, Any] = {
|
|
29635
|
-
"
|
|
30790
|
+
"que_id": que_id,
|
|
29636
30791
|
"operator": _public_view_filter_operator_from_judge_type(rule.get("judgeType"), values=values),
|
|
29637
30792
|
}
|
|
29638
30793
|
if public_rule["operator"] == "in":
|
|
@@ -29657,6 +30812,8 @@ def _public_view_filter_operator_from_judge_type(judge_type: Any, *, values: lis
|
|
|
29657
30812
|
if normalized in {JUDGE_INCLUDE, JUDGE_FUZZY_MATCH}:
|
|
29658
30813
|
return "contains"
|
|
29659
30814
|
if normalized in {JUDGE_EQUAL_ANY, JUDGE_INCLUDE_ANY}:
|
|
30815
|
+
# ANY 判型+空值 = "该字段未选任何值"的合法存量形态(judgeType 保真回写,
|
|
30816
|
+
# 转 is_empty 会丢判型——后端 9 与 0 是不同判定域,实测回读不同)
|
|
29660
30817
|
return "in"
|
|
29661
30818
|
if normalized == JUDGE_GREATER_OR_EQUAL:
|
|
29662
30819
|
return "gte"
|