@qingflow-tech/qingflow-cli 1.1.50-beta.5 → 1.1.51
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 +4 -4
- package/npm/lib/runtime.mjs +13 -29
- package/package.json +1 -1
- package/pyproject.toml +2 -8
- package/skills/qingflow-cli/SKILL.md +322 -93
- package/skills/qingflow-cli/manifest.yaml +1 -1
- package/skills/qingflow-cli/reference/00-INDEX.md +4 -4
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md +9 -3
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +137 -3
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_EXPLORATION_REPORT.md +4 -6
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_FIELD_DATA_TYPES.md +14 -14
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +19 -20
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +14 -18
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md +12 -7
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +1 -2
- package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +2 -3
- package/skills/qingflow-cli/reference/builder/30-schema-fields.md +5 -9
- package/skills/qingflow-cli/reference/builder/50-views.md +191 -192
- package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +0 -2
- 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/core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +95 -76
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_EXPLORATION_REPORT.md +4 -6
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_FIELD_DATA_TYPES.md +13 -13
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +19 -20
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +14 -18
- package/skills/qingflow-cli/reference/examples/README.md +1 -1
- package/skills/qingflow-cli/reference/examples/views/views_batch_full.example.json +159 -0
- package/skills/qingflow-cli/reference/examples/views/views_upsert_table_minimal.example.json +36 -30
- package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +1 -2
- package/skills/qingflow-cli/reference/record/analysis/README.md +113 -21
- package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +10 -10
- package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +9 -9
- package/skills/qingflow-cli/reference/record/analysis/business-context.md +1 -1
- package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +10 -10
- package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +80 -52
- package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +2 -2
- package/skills/qingflow-cli/reference/views_upsert_table_minimal.example.json +42 -0
- package/skills/qingflow-common/SKILL.md +4 -7
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/app_form_compat.py +0 -3
- package/src/qingflow_mcp/app_form_validator.py +17 -69
- package/src/qingflow_mcp/builder_facade/app_form.py +100 -497
- package/src/qingflow_mcp/builder_facade/models.py +17 -227
- package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +0 -1
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +30 -64
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +10 -6
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +10 -6
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +18 -11
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +10 -6
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +10 -6
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +10 -6
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +15 -46
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +27 -61
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/formula.json +0 -43
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +12 -7
- package/src/qingflow_mcp/builder_facade/service.py +539 -1671
- package/src/qingflow_mcp/builder_facade/workflow_spec.py +1 -1
- package/src/qingflow_mcp/cli/commands/__init__.py +1 -2
- package/src/qingflow_mcp/cli/commands/builder.py +55 -76
- package/src/qingflow_mcp/cli/commands/record.py +1 -1
- package/src/qingflow_mcp/cli/context.py +0 -3
- package/src/qingflow_mcp/cli/formatters.py +1 -97
- package/src/qingflow_mcp/config.py +1 -0
- package/src/qingflow_mcp/public_surface.py +0 -8
- package/src/qingflow_mcp/response_trim.py +2 -11
- package/src/qingflow_mcp/server.py +0 -2
- package/src/qingflow_mcp/server_app_builder.py +10 -34
- package/src/qingflow_mcp/server_app_user.py +0 -4
- package/src/qingflow_mcp/solution/compiler/view_compiler.py +1 -2
- package/src/qingflow_mcp/solution/executor.py +8 -96
- package/src/qingflow_mcp/solution/spec_models.py +0 -1
- package/src/qingflow_mcp/tools/ai_builder_tools.py +398 -218
- package/src/qingflow_mcp/tools/base.py +42 -10
- package/src/qingflow_mcp/tools/record_tools.py +46 -11
- package/src/qingflow_mcp/tools/solution_tools.py +0 -2
- package/src/qingflow_mcp/tools/view_tools.py +6 -0
- package/skills/qingflow-cli/reference/data/README.md +0 -32
- package/skills/qingflow-cli/reference/examples/views/views_apply_v1alpha1.example.json +0 -14
- package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_SPECIALIZED_WORKFLOW.md +0 -50
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/rich_text.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/views/manifest.json +0 -42
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/complete.json +0 -1245
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/button-config.json +0 -53
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/data-permission.json +0 -60
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/display.json +0 -73
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/operation-permission.json +0 -48
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/query-condition.json +0 -47
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/summary.json +0 -319
- package/src/qingflow_mcp/builder_facade/views_extract.py +0 -592
- package/src/qingflow_mcp/builder_facade/views_schema.py +0 -400
- package/src/qingflow_mcp/cli/commands/data.py +0 -134
- package/src/qingflow_mcp/tools/csv_export.py +0 -168
- package/src/qingflow_mcp/tools/data_tools.py +0 -284
- package/src/qingflow_mcp/tools/view_writer.py +0 -44
|
@@ -15,8 +15,6 @@ 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
|
-
|
|
20
18
|
from ..backend_client import BackendRequestContext
|
|
21
19
|
from ..config import get_flow_edit_maintenance_threshold
|
|
22
20
|
from ..errors import QingflowApiError, backend_code_int, backend_code_value_int, is_auth_like_error
|
|
@@ -28,7 +26,7 @@ from ..solution.compiler.chart_compiler import qingbi_workspace_visible_auth
|
|
|
28
26
|
from ..solution.compiler.form_compiler import build_question, default_form_payload, default_member_auth
|
|
29
27
|
from ..solution.compiler.icon_utils import encode_workspace_icon_with_defaults, workspace_icon_config
|
|
30
28
|
from ..solution.compiler.view_compiler import VIEW_TYPE_MAP
|
|
31
|
-
from ..solution.executor import _build_viewgraph_questions, _compact_dict,
|
|
29
|
+
from ..solution.executor import _build_viewgraph_questions, _compact_dict, extract_field_map
|
|
32
30
|
from ..solution.spec_models import FieldType, FormLayoutRowSpec, FormLayoutSectionSpec, ViewSpec
|
|
33
31
|
from ..tools.app_tools import AppTools
|
|
34
32
|
from ..tools.custom_button_tools import CustomButtonTools
|
|
@@ -43,8 +41,6 @@ from ..tools.workflow_tools import WorkflowTools
|
|
|
43
41
|
from . import flow_validation
|
|
44
42
|
from . import workflow_spec as workflow_spec_api
|
|
45
43
|
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
|
|
48
44
|
from .models import (
|
|
49
45
|
AppChartsReadResponse,
|
|
50
46
|
AppFieldsReadResponse,
|
|
@@ -163,14 +159,9 @@ FIELD_TYPE_TO_QUESTION_TYPE: dict[str, int] = {
|
|
|
163
159
|
FieldType.relation.value: 25,
|
|
164
160
|
}
|
|
165
161
|
|
|
166
|
-
# code_block / q_linker 输出目标字段类型。后端写链无类型白名单(FormSvcImpl 原样
|
|
167
|
-
# 落库),运行时 DataSvcImpl.getAnswerValueByString:attachment 按拆分 URL 写入、
|
|
168
|
-
# subtable 整表组装(均有专门分支),rich_text/data_relation 走 Question.canImport
|
|
169
|
-
# 兜底按字符串写入(不报错)——四者为存量合法配置,与 validator 侧白名单同口径。
|
|
170
162
|
INTEGRATION_OUTPUT_TARGET_FIELD_TYPES: tuple[str, ...] = (
|
|
171
163
|
FieldType.text.value,
|
|
172
164
|
FieldType.long_text.value,
|
|
173
|
-
"rich_text",
|
|
174
165
|
FieldType.number.value,
|
|
175
166
|
FieldType.amount.value,
|
|
176
167
|
FieldType.date.value,
|
|
@@ -178,9 +169,6 @@ INTEGRATION_OUTPUT_TARGET_FIELD_TYPES: tuple[str, ...] = (
|
|
|
178
169
|
FieldType.single_select.value,
|
|
179
170
|
FieldType.multi_select.value,
|
|
180
171
|
FieldType.boolean.value,
|
|
181
|
-
FieldType.attachment.value,
|
|
182
|
-
"data_relation",
|
|
183
|
-
FieldType.subtable.value,
|
|
184
172
|
)
|
|
185
173
|
|
|
186
174
|
MATCH_TYPE_ACCURACY = 1
|
|
@@ -302,7 +290,6 @@ class AiBuilderFacade:
|
|
|
302
290
|
self.buttons = buttons
|
|
303
291
|
self.packages = packages
|
|
304
292
|
self.views = views
|
|
305
|
-
self._view_writer = ViewWriter(views.sessions, views.backend)
|
|
306
293
|
self.workflows = workflows
|
|
307
294
|
self.portals = portals
|
|
308
295
|
self.charts = charts
|
|
@@ -3085,27 +3072,14 @@ class AiBuilderFacade:
|
|
|
3085
3072
|
member_ids=visibility.external_selectors.member_ids,
|
|
3086
3073
|
member_emails=visibility.external_selectors.member_emails,
|
|
3087
3074
|
)
|
|
3088
|
-
|
|
3089
|
-
|
|
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 [
|
|
3075
|
+
self._raise_visibility_resolution_issues(
|
|
3076
|
+
[
|
|
3096
3077
|
*member_resolution["issues"],
|
|
3097
3078
|
*role_resolution["issues"],
|
|
3098
3079
|
*department_resolution["issues"],
|
|
3099
3080
|
*external_member_resolution["issues"],
|
|
3100
3081
|
]
|
|
3101
|
-
|
|
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)
|
|
3082
|
+
)
|
|
3109
3083
|
|
|
3110
3084
|
external_depart_entries: list[dict[str, Any]] = []
|
|
3111
3085
|
for dept_id in visibility.external_selectors.dept_ids:
|
|
@@ -3246,7 +3220,7 @@ class AiBuilderFacade:
|
|
|
3246
3220
|
}
|
|
3247
3221
|
)
|
|
3248
3222
|
condition_matrix, condition_issues = _build_flow_condition_matrix(
|
|
3249
|
-
|
|
3223
|
+
current_fields_by_name=field_name_to_field,
|
|
3250
3224
|
node=normalized_node,
|
|
3251
3225
|
)
|
|
3252
3226
|
issues.extend({**issue, "node_id": node.get("id")} for issue in condition_issues)
|
|
@@ -3933,7 +3907,7 @@ class AiBuilderFacade:
|
|
|
3933
3907
|
"reason_path": f"patch_buttons[{index}].button_id",
|
|
3934
3908
|
"button_id": selector_id,
|
|
3935
3909
|
"message": "button_id does not exist in the current app draft",
|
|
3936
|
-
"next_action": "call
|
|
3910
|
+
"next_action": "call app_get and use custom_buttons[].button_id",
|
|
3937
3911
|
}
|
|
3938
3912
|
issues.append(issue)
|
|
3939
3913
|
results.append({"index": index, "status": "failed", **issue})
|
|
@@ -4237,7 +4211,7 @@ class AiBuilderFacade:
|
|
|
4237
4211
|
"reason_path": f"upsert_buttons[{index}].button_id",
|
|
4238
4212
|
"button_id": button_id,
|
|
4239
4213
|
"message": "button_id does not exist in the current app draft",
|
|
4240
|
-
"next_action": "call
|
|
4214
|
+
"next_action": "call app_get and use custom_buttons[].button_id",
|
|
4241
4215
|
}
|
|
4242
4216
|
)
|
|
4243
4217
|
continue
|
|
@@ -4308,7 +4282,7 @@ class AiBuilderFacade:
|
|
|
4308
4282
|
"reason_path": f"remove_buttons[{index}].button_id",
|
|
4309
4283
|
"button_id": selector_id,
|
|
4310
4284
|
"message": "button_id does not exist in the current app draft",
|
|
4311
|
-
"next_action": "call
|
|
4285
|
+
"next_action": "call app_get and use custom_buttons[].button_id",
|
|
4312
4286
|
}
|
|
4313
4287
|
)
|
|
4314
4288
|
continue
|
|
@@ -7181,14 +7155,10 @@ class AiBuilderFacade:
|
|
|
7181
7155
|
fields = cast(list[dict[str, Any]], state["parsed"]["fields"])
|
|
7182
7156
|
return _chart_field_names_by_id_from_public_fields(app_key=app_key, fields=fields), None
|
|
7183
7157
|
|
|
7184
|
-
def app_get_buttons(self, *, profile: str, app_key: str
|
|
7158
|
+
def app_get_buttons(self, *, profile: str, app_key: str) -> JSONObject:
|
|
7185
7159
|
self.apps._require_app_key(app_key)
|
|
7186
7160
|
warnings: list[dict[str, Any]] = []
|
|
7187
7161
|
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
|
|
7192
7162
|
try:
|
|
7193
7163
|
raw = self.buttons.custom_button_list(profile=profile, app_key=app_key, being_draft=True)
|
|
7194
7164
|
except (QingflowApiError, RuntimeError) as error:
|
|
@@ -7196,18 +7166,11 @@ class AiBuilderFacade:
|
|
|
7196
7166
|
return _failed_from_api_error(
|
|
7197
7167
|
"BUTTONS_READ_FAILED",
|
|
7198
7168
|
api_error,
|
|
7199
|
-
normalized_args=
|
|
7169
|
+
normalized_args={"app_key": app_key},
|
|
7200
7170
|
details={"app_key": app_key},
|
|
7201
7171
|
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
7202
7172
|
)
|
|
7203
7173
|
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
|
-
]
|
|
7211
7174
|
view_configs: list[dict[str, Any]] = []
|
|
7212
7175
|
view_config_read_errors: list[dict[str, Any]] = []
|
|
7213
7176
|
try:
|
|
@@ -7227,7 +7190,7 @@ class AiBuilderFacade:
|
|
|
7227
7190
|
if view_config_read_errors:
|
|
7228
7191
|
verification["view_configs_loaded"] = False
|
|
7229
7192
|
warnings.append(_warning("BUTTON_VIEW_CONFIGS_PARTIAL", "some view button binding configs could not be read back"))
|
|
7230
|
-
|
|
7193
|
+
return {
|
|
7231
7194
|
"status": "success",
|
|
7232
7195
|
"error_code": None,
|
|
7233
7196
|
"recoverable": False,
|
|
@@ -7242,11 +7205,6 @@ class AiBuilderFacade:
|
|
|
7242
7205
|
"verification": verification,
|
|
7243
7206
|
"details": {"view_config_read_errors": view_config_read_errors} if view_config_read_errors else {},
|
|
7244
7207
|
}
|
|
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
|
|
7250
7208
|
|
|
7251
7209
|
def app_get_associated_resources(self, *, profile: str, app_key: str) -> JSONObject:
|
|
7252
7210
|
self.apps._require_app_key(app_key)
|
|
@@ -8472,7 +8430,11 @@ class AiBuilderFacade:
|
|
|
8472
8430
|
try:
|
|
8473
8431
|
schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
8474
8432
|
parsed_schema = _parse_schema(schema)
|
|
8475
|
-
|
|
8433
|
+
current_fields_by_name = {
|
|
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
|
+
}
|
|
8476
8438
|
existing_views, _views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=False)
|
|
8477
8439
|
except (QingflowApiError, RuntimeError) as error:
|
|
8478
8440
|
api_error = _coerce_api_error(error)
|
|
@@ -8563,7 +8525,7 @@ class AiBuilderFacade:
|
|
|
8563
8525
|
view_binding = _custom_button_view_binding_to_view_button_patch(binding=binding, button_id=button_id)
|
|
8564
8526
|
dto, binding_issues = _serialize_view_button_binding(
|
|
8565
8527
|
binding=view_binding,
|
|
8566
|
-
|
|
8528
|
+
current_fields_by_name=current_fields_by_name,
|
|
8567
8529
|
valid_custom_button_ids=valid_custom_button_ids,
|
|
8568
8530
|
allow_unverified_custom_button_id=allow_unverified_numeric_button_ids,
|
|
8569
8531
|
)
|
|
@@ -9493,37 +9455,49 @@ class AiBuilderFacade:
|
|
|
9493
9455
|
return fields_result
|
|
9494
9456
|
current_fields = fields_result.get("fields", [])
|
|
9495
9457
|
field_names = {str(field.get("name") or "") for field in current_fields}
|
|
9496
|
-
|
|
9458
|
+
current_fields_by_name = {
|
|
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
|
+
}
|
|
9497
9463
|
upsert_views = [public_view_upsert_payload(view) for view in request.upsert_views]
|
|
9498
9464
|
patch_views = [public_view_partial_payload(patch) for patch in request.patch_views]
|
|
9499
9465
|
if request.preset is not None:
|
|
9500
9466
|
upsert_views = _build_views_preset(request.preset, list(field_names))
|
|
9501
9467
|
blocking_issues: list[dict[str, Any]] = []
|
|
9502
9468
|
for patch in upsert_views:
|
|
9503
|
-
|
|
9504
|
-
|
|
9505
|
-
|
|
9506
|
-
|
|
9469
|
+
raw_columns = [str(name or "").strip() for name in (patch.get("columns") or []) if str(name or "").strip()]
|
|
9470
|
+
columns = _filter_known_system_view_columns(raw_columns)
|
|
9471
|
+
if patch.get("type") in {"table", "card"} and raw_columns and not columns:
|
|
9472
|
+
blocking_issues.append(
|
|
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]
|
|
9507
9481
|
if missing_columns:
|
|
9508
9482
|
blocking_issues.append({"error_code": "UNKNOWN_VIEW_FIELD", "view_name": patch.get("name"), "missing_fields": missing_columns})
|
|
9509
|
-
|
|
9510
|
-
if
|
|
9511
|
-
blocking_issues.append({"error_code": "UNKNOWN_VIEW_FIELD", "view_name": patch.get("name"), "missing_fields": [
|
|
9512
|
-
|
|
9513
|
-
|
|
9514
|
-
|
|
9483
|
+
group_by = patch.get("group_by")
|
|
9484
|
+
if group_by and group_by not in field_names:
|
|
9485
|
+
blocking_issues.append({"error_code": "UNKNOWN_VIEW_FIELD", "view_name": patch.get("name"), "missing_fields": [group_by]})
|
|
9486
|
+
start_field = str(patch.get("start_field") or "").strip()
|
|
9487
|
+
end_field = str(patch.get("end_field") or "").strip()
|
|
9488
|
+
title_field = str(patch.get("title_field") or "").strip()
|
|
9515
9489
|
if patch.get("type") == "gantt":
|
|
9516
9490
|
missing_required = []
|
|
9517
|
-
if not
|
|
9518
|
-
missing_required.append("
|
|
9519
|
-
if not
|
|
9520
|
-
missing_required.append("
|
|
9491
|
+
if not start_field:
|
|
9492
|
+
missing_required.append("start_field")
|
|
9493
|
+
if not end_field:
|
|
9494
|
+
missing_required.append("end_field")
|
|
9521
9495
|
if missing_required:
|
|
9522
9496
|
blocking_issues.append({"error_code": "INVALID_GANTT_CONFIG", "view_name": patch.get("name"), "missing_fields": missing_required})
|
|
9523
|
-
missing_gantt_fields = [
|
|
9497
|
+
missing_gantt_fields = [name for name in (start_field, end_field, title_field) if name and name not in field_names]
|
|
9524
9498
|
if missing_gantt_fields:
|
|
9525
9499
|
blocking_issues.append({"error_code": "UNKNOWN_VIEW_FIELD", "view_name": patch.get("name"), "missing_fields": missing_gantt_fields})
|
|
9526
|
-
translated_filters, filter_issues = _build_view_filter_groups(
|
|
9500
|
+
translated_filters, filter_issues = _build_view_filter_groups(current_fields_by_name=current_fields_by_name, filters=patch.get("filters") or [])
|
|
9527
9501
|
if filter_issues:
|
|
9528
9502
|
blocking_issues.extend(
|
|
9529
9503
|
{
|
|
@@ -9531,12 +9505,11 @@ class AiBuilderFacade:
|
|
|
9531
9505
|
"view_name": patch.get("name"),
|
|
9532
9506
|
}
|
|
9533
9507
|
for issue in filter_issues
|
|
9534
|
-
if issue.get("error_code")
|
|
9535
9508
|
)
|
|
9536
9509
|
if translated_filters:
|
|
9537
9510
|
patch["filters"] = [dict(rule) for rule in (patch.get("filters") or [])]
|
|
9538
9511
|
_, _, query_condition_issues = _build_view_query_conditions_payload(
|
|
9539
|
-
|
|
9512
|
+
current_fields_by_name=current_fields_by_name,
|
|
9540
9513
|
query_conditions=patch.get("query_conditions"),
|
|
9541
9514
|
)
|
|
9542
9515
|
if query_condition_issues:
|
|
@@ -11823,26 +11796,6 @@ class AiBuilderFacade:
|
|
|
11823
11796
|
result["retry_payload"] = self._view_action_buttons_retry_payload(profile=profile, app_key=app_key, intents=retry_intents) if retry_intents else None
|
|
11824
11797
|
return result
|
|
11825
11798
|
|
|
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
|
-
|
|
11846
11799
|
def app_views_apply(
|
|
11847
11800
|
self,
|
|
11848
11801
|
*,
|
|
@@ -11852,91 +11805,15 @@ class AiBuilderFacade:
|
|
|
11852
11805
|
patch_views: list[ViewPartialPatch] | None = None,
|
|
11853
11806
|
remove_views: list[str],
|
|
11854
11807
|
publish: bool = True,
|
|
11855
|
-
schema_version: str | None = None,
|
|
11856
11808
|
) -> JSONObject:
|
|
11857
11809
|
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]
|
|
11865
11810
|
normalized_args = {
|
|
11866
11811
|
"app_key": app_key,
|
|
11867
|
-
"
|
|
11812
|
+
"upsert_views": [public_view_upsert_payload(patch) for patch in upsert_views],
|
|
11868
11813
|
"patch_views": [public_view_partial_payload(patch) for patch in patch_views],
|
|
11869
11814
|
"remove_views": list(remove_views),
|
|
11870
11815
|
"publish": publish,
|
|
11871
|
-
"schema_version": schema_version,
|
|
11872
11816
|
}
|
|
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
|
-
)
|
|
11940
11817
|
apply_started_at = time.perf_counter()
|
|
11941
11818
|
permission_ms = 0
|
|
11942
11819
|
view_state_read_ms = 0
|
|
@@ -11951,8 +11828,6 @@ class AiBuilderFacade:
|
|
|
11951
11828
|
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
11952
11829
|
|
|
11953
11830
|
def finalize(response: JSONObject) -> JSONObject:
|
|
11954
|
-
if schema_context is not None:
|
|
11955
|
-
response["schemaVersion"] = schema_context.schema_version
|
|
11956
11831
|
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
11957
11832
|
|
|
11958
11833
|
def attach_duration(response: JSONObject) -> JSONObject:
|
|
@@ -12169,25 +12044,7 @@ class AiBuilderFacade:
|
|
|
12169
12044
|
return finish(create_permission_outcome.block)
|
|
12170
12045
|
permission_outcomes.append(create_permission_outcome)
|
|
12171
12046
|
parsed_schema = _parse_schema(schema)
|
|
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)
|
|
12047
|
+
field_names = {field["name"] for field in parsed_schema["fields"]}
|
|
12191
12048
|
if patch_views:
|
|
12192
12049
|
expanded_views, patch_issues, patch_results = self._expand_view_partial_patches(
|
|
12193
12050
|
profile=profile,
|
|
@@ -12207,18 +12064,14 @@ class AiBuilderFacade:
|
|
|
12207
12064
|
suggested_next_call={"tool_name": "view_get", "arguments": {"profile": profile, "view_key": patch_issues[0].get("view_key") or "VIEW_KEY"}},
|
|
12208
12065
|
)
|
|
12209
12066
|
)
|
|
12210
|
-
|
|
12211
|
-
|
|
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
|
|
12067
|
+
upsert_views = [*upsert_views, *expanded_views]
|
|
12068
|
+
normalized_args["upsert_views"] = [public_view_upsert_payload(patch) for patch in upsert_views]
|
|
12221
12069
|
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
|
+
}
|
|
12222
12075
|
requires_custom_button_validation = any(
|
|
12223
12076
|
any(binding.button_type == PublicViewButtonType.custom for binding in (patch.buttons or []))
|
|
12224
12077
|
for patch in upsert_views
|
|
@@ -12314,8 +12167,8 @@ class AiBuilderFacade:
|
|
|
12314
12167
|
associated_resource_inventory_ms += _duration_ms(associated_resource_inventory_started_at)
|
|
12315
12168
|
removed: list[str] = []
|
|
12316
12169
|
removed_keys: set[str] = set()
|
|
12317
|
-
|
|
12318
|
-
|
|
12170
|
+
view_results: list[dict[str, Any]] = []
|
|
12171
|
+
failed_views: list[dict[str, Any]] = []
|
|
12319
12172
|
action_button_intents: list[dict[str, Any]] = list(patch_action_button_intents)
|
|
12320
12173
|
|
|
12321
12174
|
def record_action_button_intent(*, patch: ViewUpsertPatch, view_key: str | None, index: int) -> None:
|
|
@@ -12333,17 +12186,10 @@ class AiBuilderFacade:
|
|
|
12333
12186
|
)
|
|
12334
12187
|
|
|
12335
12188
|
view_mutation_started_at = time.perf_counter()
|
|
12336
|
-
|
|
12337
|
-
selector_text = str(
|
|
12189
|
+
for selector in remove_views:
|
|
12190
|
+
selector_text = str(selector or "").strip()
|
|
12338
12191
|
if not selector_text:
|
|
12339
|
-
|
|
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
|
-
))
|
|
12192
|
+
continue
|
|
12347
12193
|
key_match = existing_by_key.get(selector_text)
|
|
12348
12194
|
matches = [key_match] if isinstance(key_match, dict) else existing_by_name.get(selector_text, [])
|
|
12349
12195
|
if len(matches) > 1:
|
|
@@ -12370,40 +12216,44 @@ class AiBuilderFacade:
|
|
|
12370
12216
|
"error_code": "VIEW_NOT_FOUND",
|
|
12371
12217
|
"message": "remove_views item did not match an existing view name or view_key",
|
|
12372
12218
|
}
|
|
12373
|
-
|
|
12374
|
-
|
|
12375
|
-
|
|
12219
|
+
failed_views.append(failed_view)
|
|
12220
|
+
view_results.append(deepcopy(failed_view))
|
|
12221
|
+
continue
|
|
12222
|
+
if len(matches) == 1:
|
|
12376
12223
|
key = _extract_view_key(matches[0])
|
|
12377
12224
|
removed_name = _extract_view_name(matches[0]) or selector_text
|
|
12378
12225
|
try:
|
|
12379
|
-
|
|
12226
|
+
self.views.view_delete(profile=profile, viewgraph_key=key)
|
|
12227
|
+
delete_readback = self._verify_view_deleted_by_key(profile=profile, view_key=key)
|
|
12380
12228
|
removed.append(removed_name)
|
|
12381
12229
|
if key:
|
|
12382
12230
|
removed_keys.add(key)
|
|
12383
12231
|
existing_by_key.pop(key, None)
|
|
12384
12232
|
existing_by_name.pop(removed_name, None)
|
|
12385
|
-
|
|
12386
|
-
|
|
12387
|
-
|
|
12388
|
-
|
|
12389
|
-
|
|
12390
|
-
|
|
12391
|
-
|
|
12392
|
-
|
|
12393
|
-
|
|
12394
|
-
|
|
12395
|
-
|
|
12396
|
-
|
|
12397
|
-
|
|
12398
|
-
|
|
12399
|
-
|
|
12400
|
-
|
|
12401
|
-
|
|
12402
|
-
|
|
12403
|
-
|
|
12404
|
-
|
|
12405
|
-
|
|
12406
|
-
|
|
12233
|
+
view_results.append(
|
|
12234
|
+
{
|
|
12235
|
+
"name": removed_name,
|
|
12236
|
+
"view_key": key,
|
|
12237
|
+
"type": None,
|
|
12238
|
+
"status": delete_readback.get("status") or "readback_pending",
|
|
12239
|
+
"operation": "delete",
|
|
12240
|
+
"delete_executed": True,
|
|
12241
|
+
"readback_status": delete_readback.get("readback_status"),
|
|
12242
|
+
"safe_to_retry_delete": False,
|
|
12243
|
+
**(
|
|
12244
|
+
{
|
|
12245
|
+
"error_code": delete_readback.get("error_code"),
|
|
12246
|
+
"message": delete_readback.get("message"),
|
|
12247
|
+
"request_id": delete_readback.get("request_id"),
|
|
12248
|
+
"backend_code": delete_readback.get("backend_code"),
|
|
12249
|
+
"http_status": delete_readback.get("http_status"),
|
|
12250
|
+
"transport_error": delete_readback.get("transport_error"),
|
|
12251
|
+
}
|
|
12252
|
+
if delete_readback.get("readback_status") != "deleted"
|
|
12253
|
+
else {}
|
|
12254
|
+
),
|
|
12255
|
+
}
|
|
12256
|
+
)
|
|
12407
12257
|
except (QingflowApiError, RuntimeError) as error:
|
|
12408
12258
|
api_error = _coerce_api_error(error)
|
|
12409
12259
|
failed_view = {
|
|
@@ -12420,8 +12270,8 @@ class AiBuilderFacade:
|
|
|
12420
12270
|
"backend_code": api_error.backend_code,
|
|
12421
12271
|
"http_status": None if api_error.http_status == 404 else api_error.http_status,
|
|
12422
12272
|
}
|
|
12423
|
-
|
|
12424
|
-
|
|
12273
|
+
failed_views.append(failed_view)
|
|
12274
|
+
view_results.append(deepcopy(failed_view))
|
|
12425
12275
|
created: list[str] = []
|
|
12426
12276
|
updated: list[str] = []
|
|
12427
12277
|
existing_view_list = [
|
|
@@ -12431,129 +12281,95 @@ class AiBuilderFacade:
|
|
|
12431
12281
|
and _extract_view_name(view) not in removed
|
|
12432
12282
|
and _extract_view_key(view) not in removed_keys
|
|
12433
12283
|
]
|
|
12434
|
-
|
|
12435
|
-
|
|
12436
|
-
|
|
12284
|
+
for ordinal, patch in enumerate(upsert_views, start=1):
|
|
12285
|
+
apply_columns = _resolve_view_visible_field_names(patch)
|
|
12286
|
+
ignored_system_columns = [
|
|
12287
|
+
name
|
|
12288
|
+
for name in [str(value or "").strip() for value in patch.columns]
|
|
12289
|
+
if name in _KNOWN_SYSTEM_VIEW_COLUMNS
|
|
12290
|
+
]
|
|
12437
12291
|
if patch.type in {PublicViewType.table, PublicViewType.card} and patch.columns and not apply_columns:
|
|
12438
12292
|
return _failed(
|
|
12439
12293
|
"VALIDATION_ERROR",
|
|
12440
12294
|
"view columns must include at least one real app field; system columns cannot be applied directly",
|
|
12441
12295
|
normalized_args=normalized_args,
|
|
12442
|
-
details={
|
|
12296
|
+
details={
|
|
12297
|
+
"app_key": app_key,
|
|
12298
|
+
"view_name": patch.name,
|
|
12299
|
+
"ignored_system_columns": ignored_system_columns,
|
|
12300
|
+
},
|
|
12443
12301
|
)
|
|
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
|
-
]
|
|
12302
|
+
missing_columns = [name for name in apply_columns if name not in field_names]
|
|
12449
12303
|
if missing_columns:
|
|
12450
|
-
|
|
12451
|
-
|
|
12452
|
-
|
|
12453
|
-
|
|
12454
|
-
|
|
12455
|
-
|
|
12304
|
+
return _failed(
|
|
12305
|
+
"UNKNOWN_VIEW_FIELD",
|
|
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}},
|
|
12456
12316
|
)
|
|
12457
|
-
|
|
12458
|
-
(
|
|
12459
|
-
|
|
12460
|
-
|
|
12461
|
-
|
|
12462
|
-
|
|
12463
|
-
|
|
12464
|
-
|
|
12465
|
-
|
|
12466
|
-
|
|
12467
|
-
|
|
12468
|
-
|
|
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)
|
|
12510
|
-
translated_filters: list[list[dict[str, Any]]] | None
|
|
12511
|
-
filter_issues: list[dict[str, Any]]
|
|
12512
|
-
if patch.preserve_filters:
|
|
12513
|
-
translated_filters = None
|
|
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
|
|
12317
|
+
if patch.group_by and patch.group_by not in field_names:
|
|
12318
|
+
return _failed(
|
|
12319
|
+
"UNKNOWN_VIEW_FIELD",
|
|
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}},
|
|
12520
12329
|
)
|
|
12521
|
-
|
|
12522
|
-
|
|
12523
|
-
if filter_issues:
|
|
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]
|
|
12330
|
+
for gantt_field_name in (patch.start_field, patch.end_field, patch.title_field):
|
|
12331
|
+
if gantt_field_name and gantt_field_name not in field_names:
|
|
12537
12332
|
return _failed(
|
|
12538
|
-
|
|
12539
|
-
"
|
|
12333
|
+
"UNKNOWN_VIEW_FIELD",
|
|
12334
|
+
f"gantt configuration references unknown field '{gantt_field_name}'",
|
|
12540
12335
|
normalized_args=normalized_args,
|
|
12541
12336
|
details={
|
|
12542
12337
|
"app_key": app_key,
|
|
12543
12338
|
"view_name": patch.name,
|
|
12544
|
-
|
|
12339
|
+
"missing_fields": [gantt_field_name],
|
|
12545
12340
|
},
|
|
12546
|
-
missing_fields=
|
|
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]},
|
|
12341
|
+
missing_fields=[gantt_field_name],
|
|
12548
12342
|
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
12549
12343
|
)
|
|
12344
|
+
translated_filters: list[list[dict[str, Any]]] | None
|
|
12345
|
+
filter_issues: list[dict[str, Any]]
|
|
12346
|
+
if patch.preserve_filters:
|
|
12347
|
+
translated_filters = None
|
|
12348
|
+
filter_issues = []
|
|
12349
|
+
else:
|
|
12350
|
+
translated_filters, filter_issues = _build_view_filter_groups(current_fields_by_name=current_fields_by_name, filters=patch.filters)
|
|
12351
|
+
if filter_issues:
|
|
12352
|
+
first_issue = filter_issues[0]
|
|
12353
|
+
return _failed(
|
|
12354
|
+
str(first_issue.get("error_code") or "UNKNOWN_VIEW_FIELD"),
|
|
12355
|
+
"view filters reference invalid fields or values",
|
|
12356
|
+
normalized_args=normalized_args,
|
|
12357
|
+
details={
|
|
12358
|
+
"app_key": app_key,
|
|
12359
|
+
"view_name": patch.name,
|
|
12360
|
+
**first_issue,
|
|
12361
|
+
},
|
|
12362
|
+
missing_fields=list(first_issue.get("missing_fields") or []),
|
|
12363
|
+
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]},
|
|
12364
|
+
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
12365
|
+
)
|
|
12550
12366
|
if patch.preserve_query_conditions:
|
|
12551
12367
|
query_condition_payload = None
|
|
12552
12368
|
expected_query_conditions = None
|
|
12553
12369
|
query_condition_issues = []
|
|
12554
12370
|
else:
|
|
12555
12371
|
query_condition_payload, expected_query_conditions, query_condition_issues = _build_view_query_conditions_payload(
|
|
12556
|
-
|
|
12372
|
+
current_fields_by_name=current_fields_by_name,
|
|
12557
12373
|
query_conditions=patch.query_conditions,
|
|
12558
12374
|
)
|
|
12559
12375
|
if query_condition_issues:
|
|
@@ -12610,7 +12426,7 @@ class AiBuilderFacade:
|
|
|
12610
12426
|
expected_button_summary = None
|
|
12611
12427
|
elif patch.buttons is not None:
|
|
12612
12428
|
explicit_button_dtos, button_issues = _build_view_button_dtos(
|
|
12613
|
-
|
|
12429
|
+
current_fields_by_name=current_fields_by_name,
|
|
12614
12430
|
bindings=patch.buttons,
|
|
12615
12431
|
valid_custom_button_ids=valid_custom_button_ids,
|
|
12616
12432
|
)
|
|
@@ -12627,10 +12443,7 @@ class AiBuilderFacade:
|
|
|
12627
12443
|
},
|
|
12628
12444
|
missing_fields=list(first_issue.get("missing_fields") or []),
|
|
12629
12445
|
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]},
|
|
12630
|
-
suggested_next_call={
|
|
12631
|
-
"tool_name": "app_get_buttons",
|
|
12632
|
-
"arguments": {"profile": profile, "app_key": app_key},
|
|
12633
|
-
},
|
|
12446
|
+
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
12634
12447
|
)
|
|
12635
12448
|
expected_button_summary = _normalize_expected_view_buttons_for_compare(
|
|
12636
12449
|
explicit_button_dtos or [],
|
|
@@ -12671,10 +12484,6 @@ class AiBuilderFacade:
|
|
|
12671
12484
|
existing_key = _extract_view_key(matched_existing_view)
|
|
12672
12485
|
created_key: str | None = None
|
|
12673
12486
|
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
|
|
12678
12487
|
operation_phase = "view_update" if existing_key else "view_create"
|
|
12679
12488
|
query_condition_payload_for_apply = deepcopy(query_condition_payload)
|
|
12680
12489
|
expected_query_conditions_for_verify = deepcopy(expected_query_conditions)
|
|
@@ -12708,17 +12517,16 @@ class AiBuilderFacade:
|
|
|
12708
12517
|
views=self.views,
|
|
12709
12518
|
profile=profile,
|
|
12710
12519
|
source_viewgraph_key=existing_key,
|
|
12711
|
-
hierarchy_filter_groups=hierarchy_root_filter_groups,
|
|
12712
12520
|
schema=schema,
|
|
12713
12521
|
patch=patch,
|
|
12714
12522
|
view_filters=translated_filters,
|
|
12715
|
-
|
|
12523
|
+
current_fields_by_name=current_fields_by_name,
|
|
12716
12524
|
auth_override=view_auth_override,
|
|
12717
12525
|
explicit_button_dtos=explicit_button_dtos,
|
|
12718
12526
|
query_condition_payload=query_condition_payload_for_apply,
|
|
12719
12527
|
associated_resources_payload=associated_resources_payload_for_apply,
|
|
12720
12528
|
)
|
|
12721
|
-
self.
|
|
12529
|
+
self.views.view_update(profile=profile, viewgraph_key=existing_key, payload=payload)
|
|
12722
12530
|
system_view_sync: dict[str, Any] | None = None
|
|
12723
12531
|
if system_view_list_type is not None and patch.type.value == "table":
|
|
12724
12532
|
operation_phase = "default_view_apply_config_sync"
|
|
@@ -12729,9 +12537,7 @@ class AiBuilderFacade:
|
|
|
12729
12537
|
payload=payload,
|
|
12730
12538
|
list_type=system_view_list_type,
|
|
12731
12539
|
schema=schema,
|
|
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
|
-
],
|
|
12540
|
+
visible_field_names=apply_columns,
|
|
12735
12541
|
)
|
|
12736
12542
|
if not bool(system_view_sync.get("verified")):
|
|
12737
12543
|
failure_entry = {
|
|
@@ -12756,23 +12562,24 @@ class AiBuilderFacade:
|
|
|
12756
12562
|
"apply_columns": apply_columns,
|
|
12757
12563
|
},
|
|
12758
12564
|
}
|
|
12759
|
-
|
|
12760
|
-
|
|
12565
|
+
failed_views.append(failure_entry)
|
|
12566
|
+
view_results.append(failure_entry)
|
|
12761
12567
|
continue
|
|
12762
12568
|
updated.append(patch.name)
|
|
12763
|
-
|
|
12764
|
-
|
|
12765
|
-
|
|
12766
|
-
|
|
12767
|
-
|
|
12768
|
-
|
|
12769
|
-
|
|
12770
|
-
|
|
12771
|
-
|
|
12772
|
-
|
|
12773
|
-
|
|
12774
|
-
|
|
12775
|
-
|
|
12569
|
+
view_results.append(
|
|
12570
|
+
{
|
|
12571
|
+
"name": patch.name,
|
|
12572
|
+
"view_key": existing_key,
|
|
12573
|
+
"type": patch.type.value,
|
|
12574
|
+
"status": "updated",
|
|
12575
|
+
"expected_filters": deepcopy(translated_filters),
|
|
12576
|
+
"expected_buttons": deepcopy(expected_button_summary),
|
|
12577
|
+
"expected_query_conditions": deepcopy(expected_query_conditions_for_verify),
|
|
12578
|
+
"expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
|
|
12579
|
+
"system_view_sync": system_view_sync,
|
|
12580
|
+
"apply_columns": deepcopy(apply_columns),
|
|
12581
|
+
}
|
|
12582
|
+
)
|
|
12776
12583
|
record_action_button_intent(patch=patch, view_key=existing_key, index=ordinal - 1)
|
|
12777
12584
|
else:
|
|
12778
12585
|
template_key = _pick_view_template_key(existing_view_list, desired_type=patch.type.value)
|
|
@@ -12784,45 +12591,50 @@ class AiBuilderFacade:
|
|
|
12784
12591
|
views=self.views,
|
|
12785
12592
|
profile=profile,
|
|
12786
12593
|
source_viewgraph_key=created_key,
|
|
12787
|
-
hierarchy_filter_groups=hierarchy_root_filter_groups,
|
|
12788
12594
|
schema=schema,
|
|
12789
12595
|
patch=patch,
|
|
12790
12596
|
view_filters=translated_filters,
|
|
12791
|
-
|
|
12597
|
+
current_fields_by_name=current_fields_by_name,
|
|
12792
12598
|
auth_override=view_auth_override,
|
|
12793
12599
|
explicit_button_dtos=explicit_button_dtos,
|
|
12794
12600
|
query_condition_payload=query_condition_payload_for_apply,
|
|
12795
12601
|
associated_resources_payload=associated_resources_payload_for_apply,
|
|
12796
12602
|
)
|
|
12797
|
-
self.
|
|
12603
|
+
self.views.view_update(profile=profile, viewgraph_key=created_key, payload=payload)
|
|
12798
12604
|
else:
|
|
12799
|
-
payload =
|
|
12605
|
+
payload = _build_view_create_payload(
|
|
12800
12606
|
app_key=app_key,
|
|
12801
12607
|
base_info=base,
|
|
12802
12608
|
schema=schema,
|
|
12803
12609
|
patch=patch,
|
|
12804
12610
|
ordinal=ordinal,
|
|
12805
12611
|
view_filters=translated_filters,
|
|
12806
|
-
|
|
12612
|
+
current_fields_by_name=current_fields_by_name,
|
|
12807
12613
|
auth_override=view_auth_override,
|
|
12808
12614
|
explicit_button_dtos=explicit_button_dtos,
|
|
12809
12615
|
query_condition_payload=query_condition_payload_for_apply,
|
|
12810
12616
|
associated_resources_payload=associated_resources_payload_for_apply,
|
|
12811
|
-
hierarchy_filter_groups=hierarchy_root_filter_groups,
|
|
12812
12617
|
)
|
|
12813
|
-
|
|
12618
|
+
create_result = self.views.view_create(profile=profile, payload=payload)
|
|
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
|
|
12814
12625
|
created.append(patch.name)
|
|
12815
|
-
|
|
12816
|
-
|
|
12817
|
-
|
|
12818
|
-
|
|
12819
|
-
|
|
12820
|
-
|
|
12821
|
-
|
|
12822
|
-
|
|
12823
|
-
|
|
12824
|
-
|
|
12825
|
-
|
|
12626
|
+
view_results.append(
|
|
12627
|
+
{
|
|
12628
|
+
"name": patch.name,
|
|
12629
|
+
"view_key": created_key,
|
|
12630
|
+
"type": patch.type.value,
|
|
12631
|
+
"status": "created",
|
|
12632
|
+
"expected_filters": deepcopy(translated_filters),
|
|
12633
|
+
"expected_buttons": deepcopy(expected_button_summary),
|
|
12634
|
+
"expected_query_conditions": deepcopy(expected_query_conditions_for_verify),
|
|
12635
|
+
"expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
|
|
12636
|
+
}
|
|
12637
|
+
)
|
|
12826
12638
|
record_action_button_intent(patch=patch, view_key=created_key, index=ordinal - 1)
|
|
12827
12639
|
except (QingflowApiError, RuntimeError) as error:
|
|
12828
12640
|
api_error = _coerce_api_error(error)
|
|
@@ -12849,20 +12661,17 @@ class AiBuilderFacade:
|
|
|
12849
12661
|
patch=patch,
|
|
12850
12662
|
ordinal=ordinal,
|
|
12851
12663
|
view_filters=translated_filters,
|
|
12852
|
-
|
|
12853
|
-
fields_by_que_id=fields_by_que_id,
|
|
12664
|
+
current_fields_by_name=current_fields_by_name,
|
|
12854
12665
|
auth_override=view_auth_override,
|
|
12855
12666
|
explicit_button_dtos=fallback_button_dtos,
|
|
12856
12667
|
query_condition_payload=query_condition_payload_for_apply,
|
|
12857
12668
|
associated_resources_payload=associated_resources_payload_for_apply,
|
|
12858
12669
|
)
|
|
12859
|
-
self.
|
|
12670
|
+
self.views.view_update(profile=profile, viewgraph_key=target_key, payload=fallback_payload)
|
|
12860
12671
|
system_view_sync: dict[str, Any] | None = None
|
|
12861
12672
|
fallback_system_view_list_type = (
|
|
12862
12673
|
_resolve_system_view_list_type(view_key=target_key, view_name=patch.name)
|
|
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"}
|
|
12674
|
+
if patch.type.value == "table" and target_key
|
|
12866
12675
|
else None
|
|
12867
12676
|
)
|
|
12868
12677
|
if fallback_system_view_list_type is not None:
|
|
@@ -12874,9 +12683,7 @@ class AiBuilderFacade:
|
|
|
12874
12683
|
payload=fallback_payload,
|
|
12875
12684
|
list_type=fallback_system_view_list_type,
|
|
12876
12685
|
schema=schema,
|
|
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
|
-
],
|
|
12686
|
+
visible_field_names=apply_columns,
|
|
12880
12687
|
)
|
|
12881
12688
|
if not bool(system_view_sync.get("verified")):
|
|
12882
12689
|
failure_entry = {
|
|
@@ -12901,42 +12708,44 @@ class AiBuilderFacade:
|
|
|
12901
12708
|
"apply_columns": apply_columns,
|
|
12902
12709
|
},
|
|
12903
12710
|
}
|
|
12904
|
-
|
|
12905
|
-
|
|
12711
|
+
failed_views.append(failure_entry)
|
|
12712
|
+
view_results.append(failure_entry)
|
|
12906
12713
|
continue
|
|
12907
12714
|
if existing_key:
|
|
12908
12715
|
updated.append(patch.name)
|
|
12909
|
-
|
|
12910
|
-
|
|
12911
|
-
|
|
12912
|
-
|
|
12913
|
-
|
|
12914
|
-
|
|
12915
|
-
|
|
12916
|
-
|
|
12917
|
-
|
|
12918
|
-
|
|
12919
|
-
|
|
12920
|
-
|
|
12921
|
-
|
|
12922
|
-
|
|
12716
|
+
view_results.append(
|
|
12717
|
+
{
|
|
12718
|
+
"name": patch.name,
|
|
12719
|
+
"view_key": existing_key,
|
|
12720
|
+
"type": patch.type.value,
|
|
12721
|
+
"status": "updated",
|
|
12722
|
+
"fallback_applied": True,
|
|
12723
|
+
"expected_filters": deepcopy(translated_filters),
|
|
12724
|
+
"expected_buttons": deepcopy(expected_button_summary),
|
|
12725
|
+
"expected_query_conditions": deepcopy(expected_query_conditions_for_verify),
|
|
12726
|
+
"expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
|
|
12727
|
+
"system_view_sync": system_view_sync,
|
|
12728
|
+
"apply_columns": deepcopy(apply_columns),
|
|
12729
|
+
}
|
|
12730
|
+
)
|
|
12923
12731
|
record_action_button_intent(patch=patch, view_key=existing_key, index=ordinal - 1)
|
|
12924
12732
|
else:
|
|
12925
12733
|
created.append(patch.name)
|
|
12926
|
-
|
|
12927
|
-
|
|
12928
|
-
|
|
12929
|
-
|
|
12930
|
-
|
|
12931
|
-
|
|
12932
|
-
|
|
12933
|
-
|
|
12934
|
-
|
|
12935
|
-
|
|
12936
|
-
|
|
12937
|
-
|
|
12938
|
-
|
|
12939
|
-
|
|
12734
|
+
view_results.append(
|
|
12735
|
+
{
|
|
12736
|
+
"name": patch.name,
|
|
12737
|
+
"view_key": created_key,
|
|
12738
|
+
"type": patch.type.value,
|
|
12739
|
+
"status": "created",
|
|
12740
|
+
"fallback_applied": True,
|
|
12741
|
+
"expected_filters": deepcopy(translated_filters),
|
|
12742
|
+
"expected_buttons": deepcopy(expected_button_summary),
|
|
12743
|
+
"expected_query_conditions": deepcopy(expected_query_conditions_for_verify),
|
|
12744
|
+
"expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
|
|
12745
|
+
"system_view_sync": system_view_sync,
|
|
12746
|
+
"apply_columns": deepcopy(apply_columns),
|
|
12747
|
+
}
|
|
12748
|
+
)
|
|
12940
12749
|
record_action_button_intent(patch=patch, view_key=created_key, index=ordinal - 1)
|
|
12941
12750
|
continue
|
|
12942
12751
|
fallback_payload = _build_minimal_view_payload(
|
|
@@ -12945,16 +12754,16 @@ class AiBuilderFacade:
|
|
|
12945
12754
|
patch=patch,
|
|
12946
12755
|
ordinal=ordinal,
|
|
12947
12756
|
view_filters=translated_filters,
|
|
12948
|
-
|
|
12949
|
-
fields_by_que_id=fields_by_que_id,
|
|
12757
|
+
current_fields_by_name=current_fields_by_name,
|
|
12950
12758
|
auth_override=view_auth_override,
|
|
12951
12759
|
explicit_button_dtos=explicit_button_dtos,
|
|
12952
12760
|
query_condition_payload=query_condition_payload_for_apply,
|
|
12953
12761
|
associated_resources_payload=associated_resources_payload_for_apply,
|
|
12954
12762
|
)
|
|
12955
|
-
self.
|
|
12763
|
+
self.views.view_create(profile=profile, payload=fallback_payload)
|
|
12956
12764
|
created.append(patch.name)
|
|
12957
|
-
|
|
12765
|
+
view_results.append(
|
|
12766
|
+
{
|
|
12958
12767
|
"name": patch.name,
|
|
12959
12768
|
"view_key": created_key,
|
|
12960
12769
|
"type": patch.type.value,
|
|
@@ -12964,6 +12773,7 @@ class AiBuilderFacade:
|
|
|
12964
12773
|
"expected_query_conditions": deepcopy(expected_query_conditions_for_verify),
|
|
12965
12774
|
"expected_associated_resources": deepcopy(expected_associated_resources_for_verify),
|
|
12966
12775
|
}
|
|
12776
|
+
)
|
|
12967
12777
|
record_action_button_intent(patch=patch, view_key=created_key, index=ordinal - 1)
|
|
12968
12778
|
continue
|
|
12969
12779
|
except (QingflowApiError, RuntimeError) as fallback_error:
|
|
@@ -12989,16 +12799,11 @@ class AiBuilderFacade:
|
|
|
12989
12799
|
"view_name": patch.name,
|
|
12990
12800
|
"view_type": patch.type.value,
|
|
12991
12801
|
"columns": patch.columns,
|
|
12992
|
-
"
|
|
12993
|
-
"filters": [
|
|
12994
|
-
|
|
12995
|
-
|
|
12996
|
-
|
|
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,
|
|
12802
|
+
"group_by": patch.group_by,
|
|
12803
|
+
"filters": [item.model_dump(mode="json") for item in patch.filters],
|
|
12804
|
+
"start_field": patch.start_field,
|
|
12805
|
+
"end_field": patch.end_field,
|
|
12806
|
+
"title_field": patch.title_field,
|
|
13002
12807
|
"operation": "sync_default_view" if operation_phase == "default_view_apply_config_sync" else ("update" if existing_key or created_key else "create"),
|
|
13003
12808
|
"list_type": system_view_list_type,
|
|
13004
12809
|
"transport_error": {
|
|
@@ -13008,69 +12813,24 @@ class AiBuilderFacade:
|
|
|
13008
12813
|
},
|
|
13009
12814
|
**_view_apply_failure_diagnostics(
|
|
13010
12815
|
patch=patch,
|
|
13011
|
-
|
|
12816
|
+
current_fields_by_name=current_fields_by_name,
|
|
13012
12817
|
backend_code=api_error.backend_code,
|
|
13013
12818
|
operation_phase=operation_phase,
|
|
13014
12819
|
),
|
|
13015
12820
|
},
|
|
13016
12821
|
}
|
|
13017
|
-
|
|
13018
|
-
|
|
12822
|
+
failed_views.append(failure_entry)
|
|
12823
|
+
view_results.append(failure_entry)
|
|
13019
12824
|
continue
|
|
13020
12825
|
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
|
|
13066
12826
|
successful_action_view_keys = {
|
|
13067
12827
|
str(item.get("view_key") or "").strip()
|
|
13068
|
-
for item in
|
|
12828
|
+
for item in view_results
|
|
13069
12829
|
if str(item.get("status") or "") in {"created", "updated"} and str(item.get("view_key") or "").strip()
|
|
13070
12830
|
}
|
|
13071
12831
|
successful_action_view_names = {
|
|
13072
12832
|
str(item.get("name") or "").strip()
|
|
13073
|
-
for item in
|
|
12833
|
+
for item in view_results
|
|
13074
12834
|
if str(item.get("status") or "") in {"created", "updated"} and str(item.get("name") or "").strip()
|
|
13075
12835
|
}
|
|
13076
12836
|
skipped_action_button_intents = [
|
|
@@ -13121,12 +12881,12 @@ class AiBuilderFacade:
|
|
|
13121
12881
|
action_buttons_failed = bool(action_button_intents) and not (action_buttons_verified and view_action_button_bindings_verified)
|
|
13122
12882
|
action_buttons_retry_payload = action_buttons_result.get("retry_payload") if isinstance(action_buttons_result.get("retry_payload"), dict) else None
|
|
13123
12883
|
needs_view_list_readback = bool(created or updated)
|
|
13124
|
-
|
|
12884
|
+
verified_view_result: list[dict[str, Any]] | None = []
|
|
13125
12885
|
verified_views_unavailable = False
|
|
13126
12886
|
if needs_view_list_readback:
|
|
13127
12887
|
try:
|
|
13128
12888
|
view_readback_started_at = time.perf_counter()
|
|
13129
|
-
|
|
12889
|
+
verified_view_result, verified_views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
13130
12890
|
except (QingflowApiError, RuntimeError) as error:
|
|
13131
12891
|
view_readback_ms += _duration_ms(view_readback_started_at)
|
|
13132
12892
|
api_error = _coerce_api_error(error)
|
|
@@ -13140,23 +12900,23 @@ class AiBuilderFacade:
|
|
|
13140
12900
|
view_readback_ms += _duration_ms(view_readback_started_at)
|
|
13141
12901
|
verified_names = {
|
|
13142
12902
|
_extract_view_name(item)
|
|
13143
|
-
for item in (
|
|
12903
|
+
for item in (verified_view_result or [])
|
|
13144
12904
|
if isinstance(item, dict) and _extract_view_name(item)
|
|
13145
12905
|
}
|
|
13146
12906
|
verified_by_key = {
|
|
13147
12907
|
_extract_view_key(item): item
|
|
13148
|
-
for item in (
|
|
12908
|
+
for item in (verified_view_result or [])
|
|
13149
12909
|
if isinstance(item, dict) and _extract_view_key(item)
|
|
13150
12910
|
}
|
|
13151
12911
|
verified_view_keys_by_name: dict[str, list[str]] = {}
|
|
13152
|
-
for item in (
|
|
12912
|
+
for item in (verified_view_result or []):
|
|
13153
12913
|
if not isinstance(item, dict):
|
|
13154
12914
|
continue
|
|
13155
12915
|
name = _extract_view_name(item)
|
|
13156
12916
|
key = _extract_view_key(item)
|
|
13157
12917
|
if name and key:
|
|
13158
12918
|
verified_view_keys_by_name.setdefault(name, []).append(key)
|
|
13159
|
-
|
|
12919
|
+
verification_by_view: list[dict[str, Any]] = []
|
|
13160
12920
|
filter_readback_pending = False
|
|
13161
12921
|
filter_mismatches: list[dict[str, Any]] = []
|
|
13162
12922
|
query_condition_readback_pending = False
|
|
@@ -13183,7 +12943,7 @@ class AiBuilderFacade:
|
|
|
13183
12943
|
return cached
|
|
13184
12944
|
|
|
13185
12945
|
view_verification_started_at = time.perf_counter()
|
|
13186
|
-
for item in
|
|
12946
|
+
for item in view_results:
|
|
13187
12947
|
status = str(item.get("status") or "")
|
|
13188
12948
|
name = str(item.get("name") or "")
|
|
13189
12949
|
item_view_key = str(item.get("view_key") or "").strip()
|
|
@@ -13226,7 +12986,7 @@ class AiBuilderFacade:
|
|
|
13226
12986
|
verification_entry["readback_ambiguous"] = True
|
|
13227
12987
|
verification_entry["matching_view_keys"] = matched_keys
|
|
13228
12988
|
filter_readback_pending = True
|
|
13229
|
-
|
|
12989
|
+
verification_by_view.append(verification_entry)
|
|
13230
12990
|
continue
|
|
13231
12991
|
try:
|
|
13232
12992
|
config_result, config_error = read_view_config_for_verification(verification_key)
|
|
@@ -13244,11 +13004,11 @@ class AiBuilderFacade:
|
|
|
13244
13004
|
filters_verified = filters_semantically_equivalent and actual_data_scope == expected_data_scope
|
|
13245
13005
|
public_expected_filters = _public_view_filter_groups_from_match_rules(
|
|
13246
13006
|
expected_filter_summary,
|
|
13247
|
-
|
|
13007
|
+
current_fields_by_name=current_fields_by_name,
|
|
13248
13008
|
)
|
|
13249
13009
|
public_actual_filters = _public_view_filter_groups_from_match_rules(
|
|
13250
13010
|
semantic_actual_filters,
|
|
13251
|
-
|
|
13011
|
+
current_fields_by_name=current_fields_by_name,
|
|
13252
13012
|
)
|
|
13253
13013
|
verification_entry["filters_verified"] = filters_verified
|
|
13254
13014
|
verification_entry["view_key"] = verification_key
|
|
@@ -13298,7 +13058,7 @@ class AiBuilderFacade:
|
|
|
13298
13058
|
verification_entry["readback_ambiguous"] = True
|
|
13299
13059
|
verification_entry["matching_view_keys"] = matched_keys
|
|
13300
13060
|
query_condition_readback_pending = True
|
|
13301
|
-
|
|
13061
|
+
verification_by_view.append(verification_entry)
|
|
13302
13062
|
continue
|
|
13303
13063
|
try:
|
|
13304
13064
|
config_result, config_error = read_view_config_for_verification(verification_key)
|
|
@@ -13347,7 +13107,7 @@ class AiBuilderFacade:
|
|
|
13347
13107
|
verification_entry["readback_ambiguous"] = True
|
|
13348
13108
|
verification_entry["matching_view_keys"] = matched_keys
|
|
13349
13109
|
associated_resource_readback_pending = True
|
|
13350
|
-
|
|
13110
|
+
verification_by_view.append(verification_entry)
|
|
13351
13111
|
continue
|
|
13352
13112
|
try:
|
|
13353
13113
|
config_result, config_error = read_view_config_for_verification(verification_key)
|
|
@@ -13402,7 +13162,7 @@ class AiBuilderFacade:
|
|
|
13402
13162
|
verification_entry["readback_ambiguous"] = True
|
|
13403
13163
|
verification_entry["matching_view_keys"] = matched_keys
|
|
13404
13164
|
button_readback_pending = True
|
|
13405
|
-
|
|
13165
|
+
verification_by_view.append(verification_entry)
|
|
13406
13166
|
continue
|
|
13407
13167
|
try:
|
|
13408
13168
|
config_result, config_error = read_view_config_for_verification(verification_key)
|
|
@@ -13450,45 +13210,51 @@ class AiBuilderFacade:
|
|
|
13450
13210
|
"category": api_error.category,
|
|
13451
13211
|
}
|
|
13452
13212
|
button_readback_pending = True
|
|
13453
|
-
|
|
13213
|
+
verification_by_view.append(verification_entry)
|
|
13454
13214
|
elif status == "removed":
|
|
13455
|
-
|
|
13456
|
-
|
|
13457
|
-
|
|
13458
|
-
|
|
13459
|
-
|
|
13460
|
-
|
|
13461
|
-
|
|
13462
|
-
|
|
13463
|
-
|
|
13464
|
-
|
|
13465
|
-
|
|
13215
|
+
verification_by_view.append(
|
|
13216
|
+
{
|
|
13217
|
+
"name": name,
|
|
13218
|
+
"view_key": item.get("view_key"),
|
|
13219
|
+
"type": item.get("type"),
|
|
13220
|
+
"status": "removed",
|
|
13221
|
+
"present_in_readback": False,
|
|
13222
|
+
"removed_verified": True,
|
|
13223
|
+
"delete_executed": bool(item.get("delete_executed")),
|
|
13224
|
+
"readback_status": item.get("readback_status") or "deleted",
|
|
13225
|
+
"safe_to_retry_delete": False,
|
|
13226
|
+
}
|
|
13227
|
+
)
|
|
13466
13228
|
elif status == "readback_pending" and item.get("operation") == "delete":
|
|
13467
13229
|
readback_status = str(item.get("readback_status") or "unavailable")
|
|
13468
|
-
|
|
13469
|
-
|
|
13470
|
-
|
|
13471
|
-
|
|
13472
|
-
|
|
13473
|
-
|
|
13474
|
-
|
|
13475
|
-
|
|
13476
|
-
|
|
13477
|
-
|
|
13478
|
-
|
|
13479
|
-
|
|
13230
|
+
verification_by_view.append(
|
|
13231
|
+
{
|
|
13232
|
+
"name": name,
|
|
13233
|
+
"view_key": item.get("view_key"),
|
|
13234
|
+
"type": item.get("type"),
|
|
13235
|
+
"status": "readback_pending",
|
|
13236
|
+
"present_in_readback": True if readback_status == "still_exists" else None,
|
|
13237
|
+
"removed_verified": False,
|
|
13238
|
+
"delete_executed": bool(item.get("delete_executed")),
|
|
13239
|
+
"readback_status": readback_status,
|
|
13240
|
+
"safe_to_retry_delete": False,
|
|
13241
|
+
"error_code": item.get("error_code"),
|
|
13242
|
+
}
|
|
13243
|
+
)
|
|
13480
13244
|
else:
|
|
13481
|
-
|
|
13482
|
-
|
|
13483
|
-
|
|
13484
|
-
|
|
13485
|
-
|
|
13486
|
-
|
|
13487
|
-
|
|
13245
|
+
verification_by_view.append(
|
|
13246
|
+
{
|
|
13247
|
+
"name": item.get("name"),
|
|
13248
|
+
"type": item.get("type"),
|
|
13249
|
+
"status": "failed",
|
|
13250
|
+
"present_in_readback": None,
|
|
13251
|
+
"error_code": item.get("error_code"),
|
|
13252
|
+
}
|
|
13253
|
+
)
|
|
13488
13254
|
view_verification_ms += _duration_ms(view_verification_started_at)
|
|
13489
13255
|
removed_delete_results = [
|
|
13490
13256
|
item
|
|
13491
|
-
for item in
|
|
13257
|
+
for item in view_results
|
|
13492
13258
|
if item.get("operation") == "delete" and bool(item.get("delete_executed"))
|
|
13493
13259
|
]
|
|
13494
13260
|
removed_verified = all(str(item.get("readback_status") or "") == "deleted" for item in removed_delete_results)
|
|
@@ -13503,19 +13269,19 @@ class AiBuilderFacade:
|
|
|
13503
13269
|
view_associated_resources_verified = verified and not associated_resource_readback_pending and not associated_resource_mismatches
|
|
13504
13270
|
view_buttons_verified = verified and not button_readback_pending and not button_mismatches and not custom_button_readback_pending
|
|
13505
13271
|
noop = not created and not updated and not removed and not action_button_write_executed
|
|
13506
|
-
if
|
|
13272
|
+
if failed_views:
|
|
13507
13273
|
successful_changes = bool(created or updated or removed or action_button_write_succeeded)
|
|
13508
|
-
first_failure =
|
|
13274
|
+
first_failure = failed_views[0]
|
|
13509
13275
|
response = {
|
|
13510
13276
|
"status": "partial_success" if successful_changes else "failed",
|
|
13511
13277
|
"error_code": "VIEW_APPLY_PARTIAL" if successful_changes else "VIEW_APPLY_FAILED",
|
|
13512
13278
|
"recoverable": True,
|
|
13513
|
-
"message": "view
|
|
13279
|
+
"message": "applied some view patches; at least one view failed" if successful_changes else "one or more view patches failed",
|
|
13514
13280
|
"normalized_args": normalized_args,
|
|
13515
13281
|
"missing_fields": [],
|
|
13516
13282
|
"allowed_values": {"view_types": [member.value for member in PublicViewType], "view.filter.operator": [member.value for member in ViewFilterOperator]},
|
|
13517
13283
|
"details": {
|
|
13518
|
-
"
|
|
13284
|
+
"per_view_results": view_results,
|
|
13519
13285
|
"filter_mismatches": filter_mismatches,
|
|
13520
13286
|
"query_condition_mismatches": query_condition_mismatches,
|
|
13521
13287
|
"associated_resource_mismatches": associated_resource_mismatches,
|
|
@@ -13533,8 +13299,7 @@ class AiBuilderFacade:
|
|
|
13533
13299
|
"http_status": first_failure.get("http_status"),
|
|
13534
13300
|
"noop": noop,
|
|
13535
13301
|
"warnings": (
|
|
13536
|
-
|
|
13537
|
-
+ (
|
|
13302
|
+
(
|
|
13538
13303
|
[_warning("VIEW_FILTERS_UNVERIFIED", "view definitions may exist, but saved filter behavior is not fully verified")]
|
|
13539
13304
|
if (filter_readback_pending or filter_mismatches)
|
|
13540
13305
|
else []
|
|
@@ -13574,14 +13339,13 @@ class AiBuilderFacade:
|
|
|
13574
13339
|
"action_buttons_verified": action_buttons_verified,
|
|
13575
13340
|
"view_button_bindings_verified": view_action_button_bindings_verified,
|
|
13576
13341
|
"views_read_unavailable": verified_views_unavailable,
|
|
13577
|
-
"
|
|
13342
|
+
"by_view": verification_by_view,
|
|
13578
13343
|
"custom_button_readback_pending": custom_button_readback_pending,
|
|
13579
13344
|
"custom_button_readback_pending_entries": deepcopy(custom_button_readback_pending_entries),
|
|
13580
13345
|
},
|
|
13581
13346
|
"app_key": app_key,
|
|
13582
13347
|
"app_name": app_name,
|
|
13583
|
-
"
|
|
13584
|
-
"views_diff": {"created": created, "updated": updated, "removed": removed, "failed": [failure_result]},
|
|
13348
|
+
"views_diff": {"created": created, "updated": updated, "removed": removed, "failed": failed_views},
|
|
13585
13349
|
"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,
|
|
13586
13350
|
"write_executed": bool(created or updated or removed or action_button_write_executed),
|
|
13587
13351
|
"write_succeeded": bool(created or updated or removed or action_button_write_succeeded),
|
|
@@ -13589,7 +13353,6 @@ class AiBuilderFacade:
|
|
|
13589
13353
|
}
|
|
13590
13354
|
return finish_with_publish(response)
|
|
13591
13355
|
warnings: list[dict[str, Any]] = []
|
|
13592
|
-
warnings.extend(stale_warnings)
|
|
13593
13356
|
if filter_readback_pending or filter_mismatches:
|
|
13594
13357
|
warnings.append(_warning("VIEW_FILTERS_UNVERIFIED", "view definitions were applied, but saved filter behavior is not fully verified"))
|
|
13595
13358
|
if query_condition_readback_pending or query_condition_mismatches:
|
|
@@ -13608,388 +13371,87 @@ class AiBuilderFacade:
|
|
|
13608
13371
|
)
|
|
13609
13372
|
)
|
|
13610
13373
|
if delete_readback_pending:
|
|
13611
|
-
warnings.append(
|
|
13612
|
-
_warning(
|
|
13613
|
-
"VIEW_DELETE_READBACK_PENDING",
|
|
13614
|
-
"view delete was sent, but deletion readback is not fully verified; do not blindly repeat delete",
|
|
13615
|
-
)
|
|
13616
|
-
)
|
|
13617
|
-
all_verified = (
|
|
13618
|
-
verified
|
|
13619
|
-
and view_filters_verified
|
|
13620
|
-
and view_query_conditions_verified
|
|
13621
|
-
and view_associated_resources_verified
|
|
13622
|
-
and view_buttons_verified
|
|
13623
|
-
and action_buttons_verified
|
|
13624
|
-
and view_action_button_bindings_verified
|
|
13625
|
-
)
|
|
13626
|
-
action_buttons_error_code = str(action_buttons_result.get("error_code") or "VIEW_ACTION_BUTTONS_APPLY_FAILED") if action_buttons_failed else None
|
|
13627
|
-
response = {
|
|
13628
|
-
"status": "success" if all_verified else "partial_success",
|
|
13629
|
-
"error_code": None if all_verified else (action_buttons_error_code if action_buttons_failed else "VIEW_BUTTON_READBACK_MISMATCH" if button_mismatches else "VIEW_ASSOCIATED_RESOURCE_READBACK_MISMATCH" if associated_resource_mismatches else "VIEW_QUERY_CONDITION_READBACK_MISMATCH" if query_condition_mismatches else "VIEW_FILTER_READBACK_MISMATCH" if filter_mismatches else "VIEWS_READBACK_PENDING"),
|
|
13630
|
-
"recoverable": not all_verified,
|
|
13631
|
-
"message": (
|
|
13632
|
-
"applied view patch"
|
|
13633
|
-
if all_verified
|
|
13634
|
-
else "applied view patch; inline action buttons did not fully verify"
|
|
13635
|
-
if action_buttons_failed
|
|
13636
|
-
else "applied view patch; buttons did not fully verify"
|
|
13637
|
-
if button_mismatches
|
|
13638
|
-
else "applied view patch; associated resources did not fully verify"
|
|
13639
|
-
if associated_resource_mismatches
|
|
13640
|
-
else "applied view patch; query conditions did not fully verify"
|
|
13641
|
-
if query_condition_mismatches
|
|
13642
|
-
else "applied view patch; filters did not fully verify"
|
|
13643
|
-
if filter_mismatches
|
|
13644
|
-
else "applied view patch; views readback pending"
|
|
13645
|
-
),
|
|
13646
|
-
"normalized_args": normalized_args,
|
|
13647
|
-
"missing_fields": [],
|
|
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
|
-
}
|
|
13374
|
+
warnings.append(
|
|
13375
|
+
_warning(
|
|
13376
|
+
"VIEW_DELETE_READBACK_PENDING",
|
|
13377
|
+
"view delete was sent, but deletion readback is not fully verified; do not blindly repeat delete",
|
|
13888
13378
|
)
|
|
13889
|
-
|
|
13890
|
-
|
|
13891
|
-
|
|
13892
|
-
|
|
13893
|
-
|
|
13894
|
-
|
|
13895
|
-
|
|
13896
|
-
|
|
13897
|
-
|
|
13898
|
-
|
|
13899
|
-
|
|
13900
|
-
|
|
13901
|
-
"
|
|
13902
|
-
"
|
|
13379
|
+
)
|
|
13380
|
+
all_verified = (
|
|
13381
|
+
verified
|
|
13382
|
+
and view_filters_verified
|
|
13383
|
+
and view_query_conditions_verified
|
|
13384
|
+
and view_associated_resources_verified
|
|
13385
|
+
and view_buttons_verified
|
|
13386
|
+
and action_buttons_verified
|
|
13387
|
+
and view_action_button_bindings_verified
|
|
13388
|
+
)
|
|
13389
|
+
action_buttons_error_code = str(action_buttons_result.get("error_code") or "VIEW_ACTION_BUTTONS_APPLY_FAILED") if action_buttons_failed else None
|
|
13390
|
+
response = {
|
|
13391
|
+
"status": "success" if all_verified else "partial_success",
|
|
13392
|
+
"error_code": None if all_verified else (action_buttons_error_code if action_buttons_failed else "VIEW_BUTTON_READBACK_MISMATCH" if button_mismatches else "VIEW_ASSOCIATED_RESOURCE_READBACK_MISMATCH" if associated_resource_mismatches else "VIEW_QUERY_CONDITION_READBACK_MISMATCH" if query_condition_mismatches else "VIEW_FILTER_READBACK_MISMATCH" if filter_mismatches else "VIEWS_READBACK_PENDING"),
|
|
13393
|
+
"recoverable": not all_verified,
|
|
13394
|
+
"message": (
|
|
13395
|
+
"applied view patch"
|
|
13396
|
+
if all_verified
|
|
13397
|
+
else "applied view patch; inline action buttons did not fully verify"
|
|
13398
|
+
if action_buttons_failed
|
|
13399
|
+
else "applied view patch; buttons did not fully verify"
|
|
13400
|
+
if button_mismatches
|
|
13401
|
+
else "applied view patch; associated resources did not fully verify"
|
|
13402
|
+
if associated_resource_mismatches
|
|
13403
|
+
else "applied view patch; query conditions did not fully verify"
|
|
13404
|
+
if query_condition_mismatches
|
|
13405
|
+
else "applied view patch; filters did not fully verify"
|
|
13406
|
+
if filter_mismatches
|
|
13407
|
+
else "applied view patch; views readback pending"
|
|
13408
|
+
),
|
|
13903
13409
|
"normalized_args": normalized_args,
|
|
13904
13410
|
"missing_fields": [],
|
|
13905
|
-
"allowed_values": {"view_types": [member.value for member in PublicViewType]},
|
|
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"]},
|
|
13906
13412
|
"details": {
|
|
13907
|
-
"
|
|
13908
|
-
"
|
|
13909
|
-
"
|
|
13910
|
-
"
|
|
13413
|
+
**({"filter_mismatches": filter_mismatches} if filter_mismatches else {}),
|
|
13414
|
+
**({"query_condition_mismatches": query_condition_mismatches} if query_condition_mismatches else {}),
|
|
13415
|
+
**({"associated_resource_mismatches": associated_resource_mismatches} if associated_resource_mismatches else {}),
|
|
13416
|
+
**({"button_mismatches": button_mismatches} if button_mismatches else {}),
|
|
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
|
+
),
|
|
13911
13423
|
},
|
|
13912
13424
|
"request_id": None,
|
|
13913
|
-
"suggested_next_call": None,
|
|
13914
|
-
"
|
|
13915
|
-
"
|
|
13916
|
-
"
|
|
13917
|
-
|
|
13918
|
-
|
|
13425
|
+
"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}}),
|
|
13426
|
+
"noop": noop,
|
|
13427
|
+
"warnings": warnings,
|
|
13428
|
+
"verification": {
|
|
13429
|
+
"views_verified": verified,
|
|
13430
|
+
"view_filters_verified": view_filters_verified,
|
|
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
|
+
},
|
|
13919
13446
|
"app_key": app_key,
|
|
13920
|
-
"
|
|
13921
|
-
|
|
13922
|
-
|
|
13923
|
-
|
|
13924
|
-
|
|
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,
|
|
13447
|
+
"app_name": app_name,
|
|
13448
|
+
"views_diff": {"created": created, "updated": updated, "removed": removed, "failed": []},
|
|
13449
|
+
"verified": all_verified,
|
|
13450
|
+
"write_executed": bool(created or updated or removed or action_button_write_executed),
|
|
13451
|
+
"write_succeeded": bool(created or updated or removed or action_button_write_succeeded),
|
|
13452
|
+
"safe_to_retry": not bool(created or updated or removed or action_button_write_executed),
|
|
13992
13453
|
}
|
|
13454
|
+
return finish_with_publish(response)
|
|
13993
13455
|
|
|
13994
13456
|
def app_publish_verify(
|
|
13995
13457
|
self,
|
|
@@ -15967,76 +15429,6 @@ class AiBuilderFacade:
|
|
|
15967
15429
|
result = legacy_normalized or normalized_views
|
|
15968
15430
|
return (result, False, None) if include_error else (result, False)
|
|
15969
15431
|
|
|
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
|
-
|
|
16040
15432
|
def _load_workflow_result(
|
|
16041
15433
|
self,
|
|
16042
15434
|
*,
|
|
@@ -17464,7 +16856,7 @@ def _resolve_custom_button_view_button_ref(
|
|
|
17464
16856
|
"reason_path": reason_path,
|
|
17465
16857
|
"button_ref": button_ref,
|
|
17466
16858
|
"message": "button_ref id does not exist in the current app draft or was removed in this apply",
|
|
17467
|
-
"next_action": "call
|
|
16859
|
+
"next_action": "call app_get and use custom_buttons[].button_id, or use a client_key from upsert_buttons",
|
|
17468
16860
|
}
|
|
17469
16861
|
ref_text = str(button_ref or "").strip()
|
|
17470
16862
|
if ref_text in client_key_map:
|
|
@@ -19834,44 +19226,40 @@ def _chart_apply_batching_context(
|
|
|
19834
19226
|
def _view_apply_failure_diagnostics(
|
|
19835
19227
|
*,
|
|
19836
19228
|
patch: ViewUpsertPatch,
|
|
19837
|
-
|
|
19229
|
+
current_fields_by_name: dict[str, dict[str, Any]],
|
|
19838
19230
|
backend_code: Any,
|
|
19839
19231
|
operation_phase: str,
|
|
19840
19232
|
) -> dict[str, Any]:
|
|
19841
|
-
references: list[tuple[
|
|
19233
|
+
references: list[tuple[str, str]] = []
|
|
19842
19234
|
|
|
19843
|
-
def add_reference(
|
|
19844
|
-
|
|
19845
|
-
if
|
|
19846
|
-
references.append((
|
|
19235
|
+
def add_reference(name: Any, source: str) -> None:
|
|
19236
|
+
text = str(name or "").strip()
|
|
19237
|
+
if text:
|
|
19238
|
+
references.append((text, source))
|
|
19847
19239
|
|
|
19848
19240
|
for column in patch.columns:
|
|
19849
19241
|
add_reference(column, "columns")
|
|
19850
|
-
add_reference(patch.
|
|
19851
|
-
add_reference(patch.
|
|
19852
|
-
add_reference(patch.
|
|
19853
|
-
add_reference(patch.
|
|
19854
|
-
for
|
|
19855
|
-
|
|
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")
|
|
19242
|
+
add_reference(patch.group_by, "group_by")
|
|
19243
|
+
add_reference(patch.start_field, "start_field")
|
|
19244
|
+
add_reference(patch.end_field, "end_field")
|
|
19245
|
+
add_reference(patch.title_field, "title_field")
|
|
19246
|
+
for rule in patch.filters:
|
|
19247
|
+
add_reference(rule.field_name, "filters")
|
|
19860
19248
|
|
|
19861
|
-
seen: set[
|
|
19249
|
+
seen: set[str] = set()
|
|
19862
19250
|
field_entries: list[dict[str, Any]] = []
|
|
19863
|
-
for
|
|
19864
|
-
key =
|
|
19251
|
+
for field_name, source in references:
|
|
19252
|
+
key = f"{field_name}\0{source}"
|
|
19865
19253
|
if key in seen:
|
|
19866
19254
|
continue
|
|
19867
19255
|
seen.add(key)
|
|
19868
|
-
field =
|
|
19256
|
+
field = current_fields_by_name.get(field_name) or {}
|
|
19869
19257
|
field_entries.append(
|
|
19870
19258
|
{
|
|
19871
|
-
"field_name":
|
|
19259
|
+
"field_name": field_name,
|
|
19872
19260
|
"source": source,
|
|
19873
19261
|
"field_id": field.get("field_id"),
|
|
19874
|
-
"que_id": field.get("que_id")
|
|
19262
|
+
"que_id": field.get("que_id"),
|
|
19875
19263
|
"field_type": field.get("type"),
|
|
19876
19264
|
"que_type": field.get("que_type"),
|
|
19877
19265
|
"exists_in_schema": bool(field),
|
|
@@ -19889,7 +19277,7 @@ def _view_apply_failure_diagnostics(
|
|
|
19889
19277
|
"recommended_minimal_retry": {
|
|
19890
19278
|
"name": patch.name,
|
|
19891
19279
|
"type": patch.type.value,
|
|
19892
|
-
"columns": [field_entries[0]["
|
|
19280
|
+
"columns": [field_entries[0]["field_name"]] if field_entries else [],
|
|
19893
19281
|
**({"filters": [rule.model_dump(mode="json", exclude_none=True) for rule in patch.filters]} if patch.filters else {}),
|
|
19894
19282
|
},
|
|
19895
19283
|
}
|
|
@@ -20968,8 +20356,6 @@ def _normalize_portal_view_type(view: dict[str, Any]) -> str:
|
|
|
20968
20356
|
"ganttView": "ganttView",
|
|
20969
20357
|
"hierarchy": "hierarchyView",
|
|
20970
20358
|
"hierarchyView": "hierarchyView",
|
|
20971
|
-
"calendar": "calendarView",
|
|
20972
|
-
"calendarView": "calendarView",
|
|
20973
20359
|
}
|
|
20974
20360
|
return mapping.get(raw, "tableView")
|
|
20975
20361
|
|
|
@@ -21800,10 +21186,8 @@ def _infer_field_type(question: dict[str, Any]) -> str:
|
|
|
21800
21186
|
|
|
21801
21187
|
|
|
21802
21188
|
def _parse_field(question: dict[str, Any], *, field_id_hint: str | None = None) -> dict[str, Any]:
|
|
21189
|
+
name = str(question.get("queTitle") or "").strip()
|
|
21803
21190
|
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 "")
|
|
21807
21191
|
que_type = _coerce_positive_int(question.get("queType"))
|
|
21808
21192
|
field_type = _infer_field_type(question)
|
|
21809
21193
|
field_id = field_id_hint or f"field_{que_id or _slugify(name, default='x')}"
|
|
@@ -21973,17 +21357,6 @@ def _parse_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
|
|
21973
21357
|
for question in inner_row or []:
|
|
21974
21358
|
if not isinstance(question, dict):
|
|
21975
21359
|
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
|
|
21987
21360
|
field = _parse_field(question)
|
|
21988
21361
|
fields_by_name.setdefault(field["name"], field)
|
|
21989
21362
|
labels.append(field["name"])
|
|
@@ -22009,16 +21382,8 @@ def _parse_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
|
|
22009
21382
|
for question in row:
|
|
22010
21383
|
if not isinstance(question, dict):
|
|
22011
21384
|
continue
|
|
22012
|
-
if _coerce_positive_int(question.get("queType")) == 24:
|
|
22013
|
-
# 同行混入段落容器:不注册段落本身(它的子字段在自己 row),跳过避免同名占位挡住真实字段
|
|
22014
|
-
continue
|
|
22015
21385
|
field = _parse_field(question)
|
|
22016
|
-
|
|
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
|
-
|
|
21386
|
+
fields_by_name.setdefault(field["name"], field)
|
|
22022
21387
|
labels.append(field["name"])
|
|
22023
21388
|
if labels:
|
|
22024
21389
|
root_rows.append(labels)
|
|
@@ -23243,23 +22608,6 @@ def _apply_field_mutation(field: dict[str, Any], mutation: Any) -> None:
|
|
|
23243
22608
|
field.pop("_question_rebuild_required", None)
|
|
23244
22609
|
|
|
23245
22610
|
|
|
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
|
-
|
|
23263
22611
|
def _resolve_field_selector_with_uniqueness(
|
|
23264
22612
|
*,
|
|
23265
22613
|
fields: list[dict[str, Any]],
|
|
@@ -23275,12 +22623,7 @@ def _resolve_field_selector_with_uniqueness(
|
|
|
23275
22623
|
name = str(selector.name or "").strip()
|
|
23276
22624
|
matched = [field for field in fields if str(field.get("name") or "").strip() == name]
|
|
23277
22625
|
if len(matched) > 1:
|
|
23278
|
-
|
|
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)
|
|
22626
|
+
raise ValueError(f"{location} matched multiple fields with the same name; use field_id or que_id")
|
|
23284
22627
|
else:
|
|
23285
22628
|
matched = []
|
|
23286
22629
|
if len(matched) != 1:
|
|
@@ -26723,11 +26066,7 @@ def _merge_view_summary_with_config(
|
|
|
26723
26066
|
return summary
|
|
26724
26067
|
if isinstance(config.get("auth"), dict):
|
|
26725
26068
|
summary["visibility_summary"] = _visibility_summary(_public_visibility_from_member_auth(config.get("auth")))
|
|
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
|
-
]
|
|
26069
|
+
legacy_columns = [str(value) for value in (summary.get("columns") or []) if str(value or "").strip()]
|
|
26731
26070
|
question_entries = _extract_view_question_entries(config.get("viewgraphQuestions"))
|
|
26732
26071
|
canonical_question_entries = _extract_view_question_entries(question_list)
|
|
26733
26072
|
question_entries_by_id = {
|
|
@@ -26873,7 +26212,7 @@ def _merge_view_summary_with_config(
|
|
|
26873
26212
|
)
|
|
26874
26213
|
summary["filters"] = _public_view_filter_groups_from_match_rules(
|
|
26875
26214
|
config.get("viewgraphLimit"),
|
|
26876
|
-
|
|
26215
|
+
current_fields_by_name=field_lookup,
|
|
26877
26216
|
)
|
|
26878
26217
|
config_enriched = True
|
|
26879
26218
|
if any(key in config for key in ("asosChartVisible", "asosChartConfig", "asosChartIdList", "limitType")):
|
|
@@ -27116,16 +26455,12 @@ def _normalize_print_tpls(value: Any) -> list[dict[str, Any]]:
|
|
|
27116
26455
|
items: list[dict[str, Any]] = []
|
|
27117
26456
|
for item in value:
|
|
27118
26457
|
if isinstance(item, dict):
|
|
27119
|
-
tpl_id = item.get("
|
|
27120
|
-
if tpl_id is None:
|
|
27121
|
-
tpl_id = item.get("printTplId")
|
|
26458
|
+
tpl_id = item.get("printTplId")
|
|
27122
26459
|
if tpl_id is None:
|
|
27123
26460
|
tpl_id = item.get("templateId")
|
|
27124
26461
|
if tpl_id is None:
|
|
27125
26462
|
tpl_id = item.get("id")
|
|
27126
|
-
tpl_name = item.get("
|
|
27127
|
-
if tpl_name is None:
|
|
27128
|
-
tpl_name = item.get("printTplName")
|
|
26463
|
+
tpl_name = item.get("printTplName")
|
|
27129
26464
|
if tpl_name is None:
|
|
27130
26465
|
tpl_name = item.get("templateName")
|
|
27131
26466
|
if tpl_name is None:
|
|
@@ -27569,7 +26904,7 @@ def _build_grouped_view_button_config(button_config_dtos: list[dict[str, Any]])
|
|
|
27569
26904
|
|
|
27570
26905
|
def _build_view_button_dtos(
|
|
27571
26906
|
*,
|
|
27572
|
-
|
|
26907
|
+
current_fields_by_name: dict[str, dict[str, Any]],
|
|
27573
26908
|
bindings: list[ViewButtonBindingPatch],
|
|
27574
26909
|
valid_custom_button_ids: set[int],
|
|
27575
26910
|
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
@@ -27578,7 +26913,7 @@ def _build_view_button_dtos(
|
|
|
27578
26913
|
for binding in bindings:
|
|
27579
26914
|
dto, binding_issues = _serialize_view_button_binding(
|
|
27580
26915
|
binding=binding,
|
|
27581
|
-
|
|
26916
|
+
current_fields_by_name=current_fields_by_name,
|
|
27582
26917
|
valid_custom_button_ids=valid_custom_button_ids,
|
|
27583
26918
|
)
|
|
27584
26919
|
if binding_issues:
|
|
@@ -27591,7 +26926,7 @@ def _build_view_button_dtos(
|
|
|
27591
26926
|
def _serialize_view_button_binding(
|
|
27592
26927
|
*,
|
|
27593
26928
|
binding: ViewButtonBindingPatch,
|
|
27594
|
-
|
|
26929
|
+
current_fields_by_name: dict[str, dict[str, Any]],
|
|
27595
26930
|
valid_custom_button_ids: set[int],
|
|
27596
26931
|
allow_unverified_custom_button_id: bool = False,
|
|
27597
26932
|
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
@@ -27606,11 +26941,10 @@ def _serialize_view_button_binding(
|
|
|
27606
26941
|
"reason_path": "buttons[].button_id",
|
|
27607
26942
|
"missing_fields": [],
|
|
27608
26943
|
"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",
|
|
27610
26944
|
}
|
|
27611
26945
|
]
|
|
27612
26946
|
translated_limits, limit_issues = _build_view_button_limit_groups(
|
|
27613
|
-
|
|
26947
|
+
current_fields_by_name=current_fields_by_name,
|
|
27614
26948
|
groups=binding.button_limit,
|
|
27615
26949
|
)
|
|
27616
26950
|
if limit_issues:
|
|
@@ -27636,7 +26970,7 @@ def _serialize_view_button_binding(
|
|
|
27636
26970
|
|
|
27637
26971
|
def _build_view_button_limit_groups(
|
|
27638
26972
|
*,
|
|
27639
|
-
|
|
26973
|
+
current_fields_by_name: dict[str, dict[str, Any]],
|
|
27640
26974
|
groups: list[list[ViewFilterRulePatch]],
|
|
27641
26975
|
) -> tuple[list[list[dict[str, Any]]], list[dict[str, Any]]]:
|
|
27642
26976
|
translated_groups: list[list[dict[str, Any]]] = []
|
|
@@ -27650,14 +26984,14 @@ def _build_view_button_limit_groups(
|
|
|
27650
26984
|
raw_rule = raw_rule.model_dump(mode="json")
|
|
27651
26985
|
if not isinstance(raw_rule, dict):
|
|
27652
26986
|
continue
|
|
27653
|
-
|
|
27654
|
-
field =
|
|
26987
|
+
field_name = str(raw_rule.get("field_name") or "").strip()
|
|
26988
|
+
field = current_fields_by_name.get(field_name)
|
|
27655
26989
|
if field is None:
|
|
27656
26990
|
issues.append(
|
|
27657
26991
|
{
|
|
27658
26992
|
"error_code": "UNKNOWN_VIEW_FIELD",
|
|
27659
|
-
"missing_fields": [
|
|
27660
|
-
"reason_path": "buttons[].button_limit[].
|
|
26993
|
+
"missing_fields": [field_name] if field_name else [],
|
|
26994
|
+
"reason_path": "buttons[].button_limit[].field_name",
|
|
27661
26995
|
}
|
|
27662
26996
|
)
|
|
27663
26997
|
continue
|
|
@@ -27667,11 +27001,6 @@ def _build_view_button_limit_groups(
|
|
|
27667
27001
|
issue["reason_path"] = "buttons[].button_limit[].values"
|
|
27668
27002
|
issues.append(issue)
|
|
27669
27003
|
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)
|
|
27675
27004
|
translated_rules.append(translated_rule)
|
|
27676
27005
|
if translated_rules:
|
|
27677
27006
|
translated_groups.append(translated_rules)
|
|
@@ -27833,7 +27162,7 @@ def _custom_button_view_configs_from_view_summaries(views: list[dict[str, Any]])
|
|
|
27833
27162
|
placement = _public_view_button_placement(entry.get("config_type")) or entry.get("placement")
|
|
27834
27163
|
raw_button_limit = deepcopy(entry.get("button_limit") or [])
|
|
27835
27164
|
public_button_limit = (
|
|
27836
|
-
_public_view_filter_groups_from_match_rules(raw_button_limit,
|
|
27165
|
+
_public_view_filter_groups_from_match_rules(raw_button_limit, current_fields_by_name=field_lookup)
|
|
27837
27166
|
if raw_button_limit
|
|
27838
27167
|
else []
|
|
27839
27168
|
)
|
|
@@ -28031,11 +27360,6 @@ def _field_id_for_name(fields: list[dict[str, Any]], name: str) -> str:
|
|
|
28031
27360
|
for field in fields:
|
|
28032
27361
|
if field["name"] == name:
|
|
28033
27362
|
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 ''}")
|
|
28039
27363
|
raise KeyError(name)
|
|
28040
27364
|
|
|
28041
27365
|
|
|
@@ -28135,7 +27459,7 @@ def _build_public_workflow_spec(*, nodes: list[dict[str, Any]], transitions: lis
|
|
|
28135
27459
|
|
|
28136
27460
|
def _build_flow_condition_matrix(
|
|
28137
27461
|
*,
|
|
28138
|
-
|
|
27462
|
+
current_fields_by_name: dict[str, dict[str, Any]],
|
|
28139
27463
|
node: dict[str, Any],
|
|
28140
27464
|
) -> tuple[list[list[dict[str, Any]]], list[dict[str, Any]]]:
|
|
28141
27465
|
if str(node.get("type") or "") != "condition":
|
|
@@ -28153,7 +27477,7 @@ def _build_flow_condition_matrix(
|
|
|
28153
27477
|
if not isinstance(raw_rule, dict):
|
|
28154
27478
|
continue
|
|
28155
27479
|
field_name = str(raw_rule.get("field_name") or "").strip()
|
|
28156
|
-
field =
|
|
27480
|
+
field = current_fields_by_name.get(field_name)
|
|
28157
27481
|
if field is None:
|
|
28158
27482
|
issues.append(
|
|
28159
27483
|
{
|
|
@@ -28200,9 +27524,7 @@ def _translate_flow_condition_rule(*, field: dict[str, Any], rule: dict[str, Any
|
|
|
28200
27524
|
base["judgeType"] = JUDGE_LESS_OR_EQUAL
|
|
28201
27525
|
base["judgeValues"] = [_stringify_condition_value(values[0])]
|
|
28202
27526
|
elif operator == "is_empty":
|
|
28203
|
-
|
|
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
|
|
27527
|
+
base["judgeType"] = JUDGE_EQUAL
|
|
28206
27528
|
base["judgeValues"] = []
|
|
28207
27529
|
elif operator == "not_empty":
|
|
28208
27530
|
base["judgeType"] = JUDGE_UNEQUAL
|
|
@@ -28231,7 +27553,7 @@ def _empty_view_query_conditions_payload() -> dict[str, Any]:
|
|
|
28231
27553
|
|
|
28232
27554
|
def _build_view_query_conditions_payload(
|
|
28233
27555
|
*,
|
|
28234
|
-
|
|
27556
|
+
current_fields_by_name: dict[str, dict[str, Any]],
|
|
28235
27557
|
query_conditions: Any,
|
|
28236
27558
|
) -> tuple[dict[str, Any] | None, dict[str, Any] | None, list[dict[str, Any]]]:
|
|
28237
27559
|
if query_conditions is None:
|
|
@@ -28271,7 +27593,7 @@ def _build_view_query_conditions_payload(
|
|
|
28271
27593
|
compiled_row: list[int] = []
|
|
28272
27594
|
for column_index, selector in enumerate(row):
|
|
28273
27595
|
field, issue = _resolve_query_condition_field(
|
|
28274
|
-
|
|
27596
|
+
current_fields_by_name=current_fields_by_name,
|
|
28275
27597
|
selector=selector,
|
|
28276
27598
|
reason_path=f"query_conditions.rows[{row_index}][{column_index}]",
|
|
28277
27599
|
)
|
|
@@ -28289,16 +27611,14 @@ def _build_view_query_conditions_payload(
|
|
|
28289
27611
|
"queryConditionStatus": enabled,
|
|
28290
27612
|
"queryConditionExact": exact,
|
|
28291
27613
|
"hideBeforeQueryCondition": hide_before_query,
|
|
28292
|
-
|
|
28293
|
-
# 不能发裸 [[queId]](fastjson expect {, actual [ → 发现 #7)
|
|
28294
|
-
"queryCondition": [{"queIds": row} for row in rows] if enabled else [],
|
|
27614
|
+
"queryCondition": rows if enabled else [],
|
|
28295
27615
|
}
|
|
28296
27616
|
return payload, _normalize_view_query_conditions_for_compare(payload), []
|
|
28297
27617
|
|
|
28298
27618
|
|
|
28299
27619
|
def _resolve_query_condition_field(
|
|
28300
27620
|
*,
|
|
28301
|
-
|
|
27621
|
+
current_fields_by_name: dict[str, dict[str, Any]],
|
|
28302
27622
|
selector: Any,
|
|
28303
27623
|
reason_path: str,
|
|
28304
27624
|
) -> tuple[dict[str, Any], dict[str, Any] | None]:
|
|
@@ -28310,8 +27630,8 @@ def _resolve_query_condition_field(
|
|
|
28310
27630
|
"missing_fields": [],
|
|
28311
27631
|
"message": "query condition field selector cannot be empty",
|
|
28312
27632
|
}
|
|
28313
|
-
fields = [field for field in
|
|
28314
|
-
field =
|
|
27633
|
+
fields = [field for field in current_fields_by_name.values() if isinstance(field, dict)]
|
|
27634
|
+
field = current_fields_by_name.get(raw)
|
|
28315
27635
|
if field is None:
|
|
28316
27636
|
for candidate in fields:
|
|
28317
27637
|
if raw == str(candidate.get("field_id") or "").strip():
|
|
@@ -28322,7 +27642,7 @@ def _resolve_query_condition_field(
|
|
|
28322
27642
|
field = candidate
|
|
28323
27643
|
break
|
|
28324
27644
|
if field is None:
|
|
28325
|
-
subfield_parent = _find_subtable_subfield_parent(
|
|
27645
|
+
subfield_parent = _find_subtable_subfield_parent(current_fields_by_name=current_fields_by_name, selector=raw)
|
|
28326
27646
|
if subfield_parent is not None:
|
|
28327
27647
|
return {}, {
|
|
28328
27648
|
"error_code": "INVALID_QUERY_CONDITION_FIELD",
|
|
@@ -28370,10 +27690,10 @@ def _resolve_query_condition_field(
|
|
|
28370
27690
|
|
|
28371
27691
|
def _find_subtable_subfield_parent(
|
|
28372
27692
|
*,
|
|
28373
|
-
|
|
27693
|
+
current_fields_by_name: dict[str, dict[str, Any]],
|
|
28374
27694
|
selector: str,
|
|
28375
27695
|
) -> dict[str, Any] | None:
|
|
28376
|
-
for field in
|
|
27696
|
+
for field in current_fields_by_name.values():
|
|
28377
27697
|
if not isinstance(field, dict) or str(field.get("type") or "") != FieldType.subtable.value:
|
|
28378
27698
|
continue
|
|
28379
27699
|
for subfield in field.get("subfields") or []:
|
|
@@ -28434,9 +27754,6 @@ def _normalize_view_query_conditions_for_compare(config: Any) -> dict[str, Any]:
|
|
|
28434
27754
|
rows: list[list[int]] = []
|
|
28435
27755
|
if isinstance(raw_rows, list):
|
|
28436
27756
|
for raw_row in raw_rows:
|
|
28437
|
-
if isinstance(raw_row, dict):
|
|
28438
|
-
# ViewgraphQueryConditionConfig 形态({title?,queIds})
|
|
28439
|
-
raw_row = raw_row.get("queIds") or []
|
|
28440
27757
|
if not isinstance(raw_row, list):
|
|
28441
27758
|
continue
|
|
28442
27759
|
compiled_row = [
|
|
@@ -28499,18 +27816,11 @@ def _build_view_associated_resources_payload(
|
|
|
28499
27816
|
]
|
|
28500
27817
|
available_by_id = _associated_resource_index(available_resources)
|
|
28501
27818
|
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
|
-
]
|
|
28509
27819
|
payload = {
|
|
28510
27820
|
"asosChartVisible": True,
|
|
28511
27821
|
"asosChartConfig": {
|
|
28512
27822
|
"limitType": ASSOCIATED_RESOURCE_LIMIT_TYPE_ALL,
|
|
28513
|
-
"asosChartIdList":
|
|
27823
|
+
"asosChartIdList": [],
|
|
28514
27824
|
},
|
|
28515
27825
|
}
|
|
28516
27826
|
expected = _extract_view_associated_resources_config(payload, available_resources=available_resources)
|
|
@@ -28531,19 +27841,26 @@ def _build_view_associated_resources_payload(
|
|
|
28531
27841
|
]
|
|
28532
27842
|
item_ids.append(item_id)
|
|
28533
27843
|
missing_ids = [item_id for item_id in item_ids if item_id not in available_by_id]
|
|
28534
|
-
|
|
28535
|
-
|
|
28536
|
-
|
|
27844
|
+
if missing_ids:
|
|
27845
|
+
return None, None, [
|
|
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
|
+
]
|
|
28537
27856
|
payload = {
|
|
28538
27857
|
"asosChartVisible": True,
|
|
28539
27858
|
"asosChartConfig": {
|
|
28540
27859
|
"limitType": ASSOCIATED_RESOURCE_LIMIT_TYPE_SELECT,
|
|
28541
|
-
"asosChartIdList":
|
|
27860
|
+
"asosChartIdList": item_ids,
|
|
28542
27861
|
},
|
|
28543
27862
|
}
|
|
28544
27863
|
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
|
|
28547
27864
|
return payload, expected, []
|
|
28548
27865
|
|
|
28549
27866
|
|
|
@@ -29280,10 +28597,6 @@ _VIEW_PARTIAL_PATCH_KEY_ALIASES = {
|
|
|
29280
28597
|
"view_key": "view_key",
|
|
29281
28598
|
"viewKey": "view_key",
|
|
29282
28599
|
"viewgraphKey": "view_key",
|
|
29283
|
-
"pinned": "pinned",
|
|
29284
|
-
"fixed": "pinned",
|
|
29285
|
-
"is_pinned": "pinned",
|
|
29286
|
-
"isPinned": "pinned",
|
|
29287
28600
|
"type": "type",
|
|
29288
28601
|
"view_type": "type",
|
|
29289
28602
|
"viewType": "type",
|
|
@@ -29291,25 +28604,17 @@ _VIEW_PARTIAL_PATCH_KEY_ALIASES = {
|
|
|
29291
28604
|
"column_names": "columns",
|
|
29292
28605
|
"columnNames": "columns",
|
|
29293
28606
|
"fields": "columns",
|
|
29294
|
-
"
|
|
29295
|
-
"
|
|
29296
|
-
"group_by": "group_que_id",
|
|
29297
|
-
"groupBy": "group_que_id",
|
|
28607
|
+
"group_by": "group_by",
|
|
28608
|
+
"groupBy": "group_by",
|
|
29298
28609
|
"filters": "filters",
|
|
29299
28610
|
"filter_rules": "filters",
|
|
29300
28611
|
"filterRules": "filters",
|
|
29301
|
-
"
|
|
29302
|
-
"
|
|
29303
|
-
"
|
|
29304
|
-
"
|
|
29305
|
-
"
|
|
29306
|
-
"
|
|
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",
|
|
28612
|
+
"start_field": "start_field",
|
|
28613
|
+
"startField": "start_field",
|
|
28614
|
+
"end_field": "end_field",
|
|
28615
|
+
"endField": "end_field",
|
|
28616
|
+
"title_field": "title_field",
|
|
28617
|
+
"titleField": "title_field",
|
|
29313
28618
|
"buttons": "buttons",
|
|
29314
28619
|
"visibility": "visibility",
|
|
29315
28620
|
"query_conditions": "query_conditions",
|
|
@@ -29396,14 +28701,13 @@ def _normalize_view_partial_unset(raw_unset: Any, *, reason_path: str) -> tuple[
|
|
|
29396
28701
|
|
|
29397
28702
|
_VIEW_PARTIAL_SET_KEYS = {
|
|
29398
28703
|
"name",
|
|
29399
|
-
"pinned",
|
|
29400
28704
|
"type",
|
|
29401
28705
|
"columns",
|
|
29402
|
-
"
|
|
28706
|
+
"group_by",
|
|
29403
28707
|
"filters",
|
|
29404
|
-
"
|
|
29405
|
-
"
|
|
29406
|
-
"
|
|
28708
|
+
"start_field",
|
|
28709
|
+
"end_field",
|
|
28710
|
+
"title_field",
|
|
29407
28711
|
"buttons",
|
|
29408
28712
|
"visibility",
|
|
29409
28713
|
"query_conditions",
|
|
@@ -29429,108 +28733,6 @@ def _field_name_by_id(field_names_by_id: dict[int, str], field_id: Any) -> str |
|
|
|
29429
28733
|
return name or None
|
|
29430
28734
|
|
|
29431
28735
|
|
|
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
|
-
|
|
29534
28736
|
def _view_upsert_payload_from_existing_view(
|
|
29535
28737
|
*,
|
|
29536
28738
|
config: dict[str, Any],
|
|
@@ -29540,28 +28742,25 @@ def _view_upsert_payload_from_existing_view(
|
|
|
29540
28742
|
) -> dict[str, Any]:
|
|
29541
28743
|
view_type = _normalize_view_type_name(config.get("viewgraphType") or summary.get("type"))
|
|
29542
28744
|
configured_columns = [
|
|
29543
|
-
|
|
29544
|
-
for item in (config.get("viewgraphQueIds") or [])
|
|
29545
|
-
if
|
|
29546
|
-
and (field_names_by_id.get(que_id) or "") not in _KNOWN_SYSTEM_VIEW_COLUMNS
|
|
28745
|
+
name
|
|
28746
|
+
for name in (_field_name_by_id(field_names_by_id, item) for item in (config.get("viewgraphQueIds") or []))
|
|
28747
|
+
if name and name not in _KNOWN_SYSTEM_VIEW_COLUMNS
|
|
29547
28748
|
]
|
|
29548
28749
|
columns = [
|
|
29549
|
-
|
|
29550
|
-
for
|
|
29551
|
-
if (
|
|
29552
|
-
and (field_names_by_id.get(que_id) or "") not in _KNOWN_SYSTEM_VIEW_COLUMNS
|
|
28750
|
+
str(name or "").strip()
|
|
28751
|
+
for name in (summary.get("apply_columns") or summary.get("columns") or [])
|
|
28752
|
+
if str(name or "").strip() and str(name or "").strip() not in _KNOWN_SYSTEM_VIEW_COLUMNS
|
|
29553
28753
|
]
|
|
29554
28754
|
if configured_columns and (not columns or len(configured_columns) >= len(columns)):
|
|
29555
28755
|
columns = configured_columns
|
|
29556
28756
|
display_config = summary.get("display_config") if isinstance(summary.get("display_config"), dict) else {}
|
|
29557
|
-
|
|
29558
|
-
|
|
28757
|
+
group_by = str(summary.get("group_by") or display_config.get("group_by") or "").strip() or _field_name_by_id(field_names_by_id, config.get("groupQueId"))
|
|
28758
|
+
title_field = str(display_config.get("title_field") or "").strip() or _field_name_by_id(field_names_by_id, config.get("titleQue"))
|
|
29559
28759
|
gantt_config = config.get("viewgraphGanttConfigVO") if isinstance(config.get("viewgraphGanttConfigVO"), dict) else {}
|
|
29560
|
-
|
|
29561
|
-
|
|
29562
|
-
|
|
28760
|
+
start_field = _field_name_by_id(field_names_by_id, gantt_config.get("startTimeQueId")) if isinstance(gantt_config, dict) else None
|
|
28761
|
+
end_field = _field_name_by_id(field_names_by_id, gantt_config.get("endTimeQueId")) if isinstance(gantt_config, dict) else None
|
|
28762
|
+
gantt_title = _field_name_by_id(field_names_by_id, gantt_config.get("titleQueId")) if isinstance(gantt_config, dict) else None
|
|
29563
28763
|
query_conditions = _extract_view_query_conditions_config(config)
|
|
29564
|
-
query_conditions["rows"] = query_conditions.get("row_field_ids") or []
|
|
29565
28764
|
query_conditions.pop("row_field_ids", None)
|
|
29566
28765
|
associated_resources = _extract_view_associated_resources_config(config)
|
|
29567
28766
|
payload: dict[str, Any] = {
|
|
@@ -29569,6 +28768,7 @@ def _view_upsert_payload_from_existing_view(
|
|
|
29569
28768
|
"view_key": view_key,
|
|
29570
28769
|
"type": view_type or "table",
|
|
29571
28770
|
"columns": columns,
|
|
28771
|
+
"group_by": group_by,
|
|
29572
28772
|
"filters": [],
|
|
29573
28773
|
"query_conditions": query_conditions,
|
|
29574
28774
|
"associated_resources": {
|
|
@@ -29577,17 +28777,15 @@ def _view_upsert_payload_from_existing_view(
|
|
|
29577
28777
|
"associated_item_ids": list(associated_resources.get("configured_associated_item_ids") or []),
|
|
29578
28778
|
},
|
|
29579
28779
|
}
|
|
29580
|
-
if
|
|
29581
|
-
payload["
|
|
29582
|
-
if title_que_id is not None:
|
|
29583
|
-
payload["title_que_id"] = title_que_id
|
|
28780
|
+
if title_field:
|
|
28781
|
+
payload["title_field"] = title_field
|
|
29584
28782
|
if view_type == "gantt":
|
|
29585
|
-
if
|
|
29586
|
-
payload["
|
|
29587
|
-
if
|
|
29588
|
-
payload["
|
|
29589
|
-
if
|
|
29590
|
-
payload["
|
|
28783
|
+
if start_field:
|
|
28784
|
+
payload["start_field"] = start_field
|
|
28785
|
+
if end_field:
|
|
28786
|
+
payload["end_field"] = end_field
|
|
28787
|
+
if gantt_title:
|
|
28788
|
+
payload["title_field"] = gantt_title
|
|
29591
28789
|
return payload
|
|
29592
28790
|
|
|
29593
28791
|
|
|
@@ -29598,7 +28796,7 @@ def _first_present(mapping: dict[str, Any], *keys: str) -> Any:
|
|
|
29598
28796
|
return None
|
|
29599
28797
|
|
|
29600
28798
|
|
|
29601
|
-
def
|
|
28799
|
+
def _build_view_create_payload(
|
|
29602
28800
|
*,
|
|
29603
28801
|
app_key: str,
|
|
29604
28802
|
base_info: dict[str, Any],
|
|
@@ -29606,24 +28804,23 @@ def _build_view_creation_payload(
|
|
|
29606
28804
|
patch: ViewUpsertPatch,
|
|
29607
28805
|
ordinal: int,
|
|
29608
28806
|
view_filters: list[list[dict[str, Any]]] | None,
|
|
29609
|
-
|
|
28807
|
+
current_fields_by_name: dict[str, dict[str, Any]],
|
|
29610
28808
|
auth_override: dict[str, Any] | None = None,
|
|
29611
28809
|
explicit_button_dtos: list[dict[str, Any]] | None = None,
|
|
29612
28810
|
query_condition_payload: dict[str, Any] | None = None,
|
|
29613
28811
|
associated_resources_payload: dict[str, Any] | None = None,
|
|
29614
|
-
hierarchy_filter_groups: list[list[dict[str, Any]]] | None = None,
|
|
29615
28812
|
) -> JSONObject:
|
|
29616
28813
|
entity = _entity_spec_from_app(base_info=base_info, schema=schema, views=None)
|
|
29617
28814
|
parsed_schema = _parse_schema(schema)
|
|
29618
|
-
|
|
29619
|
-
field_ids = [
|
|
28815
|
+
visible_field_names = _resolve_view_visible_field_names(patch)
|
|
28816
|
+
field_ids = [_field_id_for_name(parsed_schema["fields"], name) for name in visible_field_names]
|
|
29620
28817
|
gantt_config = _build_public_gantt_payload(parsed_schema["fields"], extract_field_map(schema), patch)
|
|
29621
28818
|
view_spec = ViewSpec(
|
|
29622
28819
|
view_id=_slugify(patch.name, default=f"view_{uuid4().hex[:6]}"),
|
|
29623
28820
|
name=patch.name,
|
|
29624
28821
|
type=patch.type.value,
|
|
29625
28822
|
field_ids=field_ids,
|
|
29626
|
-
group_by_field_id=
|
|
28823
|
+
group_by_field_id=_field_id_for_name(_parse_schema(schema)["fields"], patch.group_by) if patch.group_by else None,
|
|
29627
28824
|
config=gantt_config,
|
|
29628
28825
|
)
|
|
29629
28826
|
from ..solution.spec_models import EntitySpec
|
|
@@ -29632,6 +28829,7 @@ def _build_view_creation_payload(
|
|
|
29632
28829
|
compiled = compile_views(EntitySpec.model_validate({**entity, "views": [view_spec.model_dump(mode="json")]}))[0]
|
|
29633
28830
|
field_map = extract_field_map(schema)
|
|
29634
28831
|
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]
|
|
29635
28833
|
payload["appKey"] = app_key
|
|
29636
28834
|
payload["ordinal"] = ordinal
|
|
29637
28835
|
payload["viewgraphQueIds"] = visible_que_ids
|
|
@@ -29643,21 +28841,17 @@ def _build_view_creation_payload(
|
|
|
29643
28841
|
payload["beingShowTitleQue"] = True
|
|
29644
28842
|
payload["titleQue"] = gantt_config.get("titleQueId") or (visible_que_ids[0] if visible_que_ids else None)
|
|
29645
28843
|
if patch.type.value == "board":
|
|
29646
|
-
payload["groupQueId"] = patch.
|
|
28844
|
+
payload["groupQueId"] = field_map.get(patch.group_by or "")
|
|
29647
28845
|
return _hydrate_view_backend_payload(
|
|
29648
28846
|
payload=payload,
|
|
29649
28847
|
view_type=patch.type.value,
|
|
29650
28848
|
visible_que_id_values=visible_que_ids,
|
|
29651
|
-
group_que_id=patch.
|
|
28849
|
+
group_que_id=field_map.get(patch.group_by or "") if patch.group_by else None,
|
|
29652
28850
|
view_filters=view_filters,
|
|
29653
28851
|
gantt_payload=gantt_config,
|
|
29654
28852
|
button_config_dtos=explicit_button_dtos,
|
|
29655
28853
|
query_condition_payload=query_condition_payload,
|
|
29656
28854
|
associated_resources_payload=associated_resources_payload,
|
|
29657
|
-
patch=patch,
|
|
29658
|
-
field_map=field_map,
|
|
29659
|
-
fields_by_que_id=fields_by_que_id,
|
|
29660
|
-
hierarchy_filter_groups=hierarchy_filter_groups,
|
|
29661
28855
|
)
|
|
29662
28856
|
|
|
29663
28857
|
|
|
@@ -29795,10 +28989,6 @@ def _normalize_view_type_name(value: Any) -> str:
|
|
|
29795
28989
|
return "gantt"
|
|
29796
28990
|
if "board" in normalized:
|
|
29797
28991
|
return "board"
|
|
29798
|
-
if "hierarchy" in normalized:
|
|
29799
|
-
return "hierarchy"
|
|
29800
|
-
if "calendar" in normalized:
|
|
29801
|
-
return "calendar"
|
|
29802
28992
|
if "card" in normalized:
|
|
29803
28993
|
return "card"
|
|
29804
28994
|
return "table"
|
|
@@ -29812,20 +29002,20 @@ def _build_view_update_payload(
|
|
|
29812
29002
|
schema: dict[str, Any],
|
|
29813
29003
|
patch: ViewUpsertPatch,
|
|
29814
29004
|
view_filters: list[list[dict[str, Any]]] | None,
|
|
29815
|
-
|
|
29005
|
+
current_fields_by_name: dict[str, dict[str, Any]],
|
|
29816
29006
|
auth_override: dict[str, Any] | None = None,
|
|
29817
29007
|
explicit_button_dtos: list[dict[str, Any]] | None = None,
|
|
29818
29008
|
query_condition_payload: dict[str, Any] | None = None,
|
|
29819
29009
|
associated_resources_payload: dict[str, Any] | None = None,
|
|
29820
|
-
hierarchy_filter_groups: list[list[dict[str, Any]]] | None = None,
|
|
29821
29010
|
) -> JSONObject:
|
|
29822
29011
|
config_response = views.view_get_config(profile=profile, viewgraph_key=source_viewgraph_key)
|
|
29823
29012
|
config = config_response.get("result") if isinstance(config_response.get("result"), dict) else {}
|
|
29824
29013
|
payload = deepcopy(config)
|
|
29825
29014
|
parsed_schema = _parse_schema(schema)
|
|
29826
29015
|
field_map = extract_field_map(schema)
|
|
29827
|
-
|
|
29828
|
-
|
|
29016
|
+
visible_field_names = _resolve_view_visible_field_names(patch)
|
|
29017
|
+
visible_que_ids = [_field_id_for_name(parsed_schema["fields"], name) for name in visible_field_names]
|
|
29018
|
+
visible_que_id_values = [field_map[name] for name in visible_field_names if name in field_map]
|
|
29829
29019
|
gantt_payload = _build_public_gantt_payload(parsed_schema["fields"], field_map, patch)
|
|
29830
29020
|
|
|
29831
29021
|
for key in (
|
|
@@ -29847,25 +29037,8 @@ def _build_view_update_payload(
|
|
|
29847
29037
|
payload["auth"] = deepcopy(auth_override if isinstance(auth_override, dict) else payload.get("auth") or default_member_auth())
|
|
29848
29038
|
if "viewName" in payload:
|
|
29849
29039
|
payload["viewName"] = patch.name
|
|
29850
|
-
|
|
29851
|
-
|
|
29852
|
-
que_id
|
|
29853
|
-
for que_id in (_coerce_any_int(raw) for raw in (config.get("viewgraphQueIds") or []))
|
|
29854
|
-
if que_id is not None
|
|
29855
|
-
)
|
|
29856
|
-
business_que_ids = iter(que_id for que_id in visible_que_id_values if que_id > 5)
|
|
29857
|
-
merged_que_ids: list[int] = []
|
|
29858
|
-
for que_id in existing_que_ids:
|
|
29859
|
-
if que_id <= 5:
|
|
29860
|
-
merged_que_ids.append(que_id)
|
|
29861
|
-
elif que_id in visible_que_id_values:
|
|
29862
|
-
merged_que_ids.append(next(business_que_ids))
|
|
29863
|
-
merged_que_ids.extend(business_que_ids)
|
|
29864
|
-
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)
|
|
29865
|
-
payload["viewgraphQueIds"] = merged_que_ids
|
|
29866
|
-
payload["viewgraphQuestions"] = _build_viewgraph_questions(
|
|
29867
|
-
schema, merged_que_ids, existing_by_que_id=_flatten_viewgraph_questions(config.get("viewgraphQuestions"))
|
|
29868
|
-
)
|
|
29040
|
+
payload["viewgraphQueIds"] = visible_que_id_values
|
|
29041
|
+
payload["viewgraphQuestions"] = _build_viewgraph_questions(schema, visible_que_id_values)
|
|
29869
29042
|
payload.setdefault("sortType", "defaultSort")
|
|
29870
29043
|
payload.setdefault("viewgraphSorts", [{"queId": 0, "beingSortAscend": True, "queType": 8}])
|
|
29871
29044
|
payload.setdefault("beingPinNavigate", True)
|
|
@@ -29895,79 +29068,25 @@ def _build_view_update_payload(
|
|
|
29895
29068
|
payload["viewgraphType"] = "boardView"
|
|
29896
29069
|
payload["beingShowTitleQue"] = True
|
|
29897
29070
|
payload["titleQue"] = visible_que_id_values[0] if visible_que_id_values else None
|
|
29898
|
-
payload["groupQueId"] = patch.
|
|
29071
|
+
payload["groupQueId"] = field_map.get(patch.group_by or "")
|
|
29899
29072
|
elif normalized_type == "gantt":
|
|
29900
29073
|
payload["viewgraphType"] = "ganttView"
|
|
29901
29074
|
payload["beingShowTitleQue"] = True
|
|
29902
29075
|
payload["titleQue"] = gantt_payload.get("titleQueId") or (visible_que_id_values[0] if visible_que_id_values else None)
|
|
29903
29076
|
payload.pop("groupQueId", None)
|
|
29904
|
-
elif normalized_type == "hierarchy":
|
|
29905
|
-
payload["viewgraphType"] = "hierarchyView"
|
|
29906
|
-
payload["beingShowTitleQue"] = True
|
|
29907
|
-
payload.pop("groupQueId", None)
|
|
29908
|
-
elif normalized_type == "calendar":
|
|
29909
|
-
payload["viewgraphType"] = "calendarView"
|
|
29910
|
-
payload["beingShowTitleQue"] = True
|
|
29911
|
-
payload.pop("groupQueId", None)
|
|
29912
29077
|
return _hydrate_view_backend_payload(
|
|
29913
29078
|
payload=payload,
|
|
29914
29079
|
view_type=normalized_type,
|
|
29915
29080
|
visible_que_id_values=visible_que_id_values,
|
|
29916
|
-
group_que_id=patch.
|
|
29081
|
+
group_que_id=field_map.get(patch.group_by or "") if patch.group_by else None,
|
|
29917
29082
|
view_filters=view_filters,
|
|
29918
29083
|
gantt_payload=gantt_payload,
|
|
29919
29084
|
button_config_dtos=button_config_dtos,
|
|
29920
29085
|
query_condition_payload=query_condition_payload,
|
|
29921
29086
|
associated_resources_payload=associated_resources_payload,
|
|
29922
|
-
patch=patch,
|
|
29923
|
-
field_map=field_map,
|
|
29924
|
-
fields_by_que_id=fields_by_que_id,
|
|
29925
|
-
hierarchy_filter_groups=hierarchy_filter_groups,
|
|
29926
|
-
schema=schema,
|
|
29927
29087
|
)
|
|
29928
29088
|
|
|
29929
29089
|
|
|
29930
|
-
# 系统列 queId→公开名(后端语义;读面展示与 viewgraphQueIds 透传时的名称解析)
|
|
29931
|
-
_SYSTEM_VIEW_QUE_NAMES = {
|
|
29932
|
-
0: "编号",
|
|
29933
|
-
1: "提交人",
|
|
29934
|
-
2: "提交时间",
|
|
29935
|
-
3: "更新时间",
|
|
29936
|
-
4: "流程标题",
|
|
29937
|
-
5: "当前流程状态",
|
|
29938
|
-
-17: "数据ID",
|
|
29939
|
-
-19: "删除时间",
|
|
29940
|
-
-20: "删除人",
|
|
29941
|
-
-21: "处理节点",
|
|
29942
|
-
-22: "抄送节点",
|
|
29943
|
-
-23: "处理时间",
|
|
29944
|
-
-24: "抄送时间",
|
|
29945
|
-
-25: "接收时间",
|
|
29946
|
-
}
|
|
29947
|
-
|
|
29948
|
-
_SYSTEM_VIEW_QUE_IDS = frozenset(
|
|
29949
|
-
{
|
|
29950
|
-
*_SYSTEM_VIEW_QUE_NAMES,
|
|
29951
|
-
-1,
|
|
29952
|
-
-2,
|
|
29953
|
-
-3,
|
|
29954
|
-
-4,
|
|
29955
|
-
-5,
|
|
29956
|
-
-6,
|
|
29957
|
-
-7,
|
|
29958
|
-
-8,
|
|
29959
|
-
-18,
|
|
29960
|
-
-30,
|
|
29961
|
-
-31,
|
|
29962
|
-
}
|
|
29963
|
-
)
|
|
29964
|
-
|
|
29965
|
-
|
|
29966
|
-
def _is_known_system_view_que_id(value: Any) -> bool:
|
|
29967
|
-
que_id = _coerce_any_int(value)
|
|
29968
|
-
return que_id in _SYSTEM_VIEW_QUE_IDS if que_id is not None else False
|
|
29969
|
-
|
|
29970
|
-
|
|
29971
29090
|
_KNOWN_SYSTEM_VIEW_COLUMNS = {
|
|
29972
29091
|
"编号",
|
|
29973
29092
|
"当前流程状态",
|
|
@@ -29991,16 +29110,16 @@ def _build_minimal_view_payload(
|
|
|
29991
29110
|
patch: ViewUpsertPatch,
|
|
29992
29111
|
ordinal: int,
|
|
29993
29112
|
view_filters: list[list[dict[str, Any]]] | None,
|
|
29994
|
-
|
|
29113
|
+
current_fields_by_name: dict[str, dict[str, Any]],
|
|
29995
29114
|
auth_override: dict[str, Any] | None = None,
|
|
29996
29115
|
explicit_button_dtos: list[dict[str, Any]] | None = None,
|
|
29997
29116
|
query_condition_payload: dict[str, Any] | None = None,
|
|
29998
29117
|
associated_resources_payload: dict[str, Any] | None = None,
|
|
29999
|
-
hierarchy_filter_groups: list[list[dict[str, Any]]] | None = None,
|
|
30000
29118
|
) -> JSONObject:
|
|
30001
29119
|
field_map = extract_field_map(schema)
|
|
30002
29120
|
parsed_schema = _parse_schema(schema)
|
|
30003
|
-
|
|
29121
|
+
visible_field_names = _resolve_view_visible_field_names(patch)
|
|
29122
|
+
visible_que_id_values = [field_map[name] for name in visible_field_names if name in field_map]
|
|
30004
29123
|
gantt_payload = _build_public_gantt_payload(parsed_schema["fields"], field_map, patch)
|
|
30005
29124
|
payload: JSONObject = {
|
|
30006
29125
|
"appKey": app_key,
|
|
@@ -30010,8 +29129,6 @@ def _build_minimal_view_payload(
|
|
|
30010
29129
|
"card": "cardView",
|
|
30011
29130
|
"board": "boardView",
|
|
30012
29131
|
"gantt": "ganttView",
|
|
30013
|
-
"hierarchy": "hierarchyView",
|
|
30014
|
-
"calendar": "calendarView",
|
|
30015
29132
|
}[patch.type.value],
|
|
30016
29133
|
"ordinal": ordinal,
|
|
30017
29134
|
"viewgraphQueIds": visible_que_id_values,
|
|
@@ -30022,21 +29139,17 @@ def _build_minimal_view_payload(
|
|
|
30022
29139
|
payload["beingShowTitleQue"] = True
|
|
30023
29140
|
payload["titleQue"] = gantt_payload.get("titleQueId") or (visible_que_id_values[0] if visible_que_id_values else None)
|
|
30024
29141
|
if patch.type.value == "board":
|
|
30025
|
-
payload["groupQueId"] = patch.
|
|
29142
|
+
payload["groupQueId"] = field_map.get(patch.group_by or "")
|
|
30026
29143
|
return _hydrate_view_backend_payload(
|
|
30027
29144
|
payload=payload,
|
|
30028
29145
|
view_type=patch.type.value,
|
|
30029
29146
|
visible_que_id_values=visible_que_id_values,
|
|
30030
|
-
group_que_id=patch.
|
|
29147
|
+
group_que_id=field_map.get(patch.group_by or "") if patch.group_by else None,
|
|
30031
29148
|
view_filters=view_filters,
|
|
30032
29149
|
gantt_payload=gantt_payload,
|
|
30033
29150
|
button_config_dtos=explicit_button_dtos,
|
|
30034
29151
|
query_condition_payload=query_condition_payload,
|
|
30035
29152
|
associated_resources_payload=associated_resources_payload,
|
|
30036
|
-
patch=patch,
|
|
30037
|
-
field_map=field_map,
|
|
30038
|
-
fields_by_que_id=fields_by_que_id,
|
|
30039
|
-
hierarchy_filter_groups=hierarchy_filter_groups,
|
|
30040
29153
|
)
|
|
30041
29154
|
|
|
30042
29155
|
|
|
@@ -30051,11 +29164,6 @@ def _hydrate_view_backend_payload(
|
|
|
30051
29164
|
button_config_dtos: list[dict[str, Any]] | None = None,
|
|
30052
29165
|
query_condition_payload: dict[str, Any] | None = None,
|
|
30053
29166
|
associated_resources_payload: dict[str, Any] | None = None,
|
|
30054
|
-
patch: ViewUpsertPatch | None = None,
|
|
30055
|
-
field_map: dict[str, int] | None = None,
|
|
30056
|
-
fields_by_que_id: dict[str, dict[str, Any]] | None = None,
|
|
30057
|
-
hierarchy_filter_groups: list[list[dict[str, Any]]] | None = None,
|
|
30058
|
-
schema: dict[str, Any] | None = None,
|
|
30059
29167
|
) -> JSONObject:
|
|
30060
29168
|
data = deepcopy(payload)
|
|
30061
29169
|
data.setdefault("beingPinNavigate", True)
|
|
@@ -30125,239 +29233,26 @@ def _hydrate_view_backend_payload(
|
|
|
30125
29233
|
data["titleQue"] = (gantt_payload or {}).get("titleQueId") or (visible_que_id_values[0] if visible_que_id_values else None)
|
|
30126
29234
|
data["groupQueId"] = None
|
|
30127
29235
|
data["viewgraphGanttConfigVO"] = deepcopy(gantt_payload) if gantt_payload else None
|
|
30128
|
-
elif view_type == "hierarchy":
|
|
30129
|
-
data["viewgraphType"] = "hierarchyView"
|
|
30130
|
-
data["beingShowTitleQue"] = True
|
|
30131
|
-
data["groupQueId"] = None
|
|
30132
|
-
data["viewgraphGanttConfigVO"] = None
|
|
30133
|
-
elif view_type == "calendar":
|
|
30134
|
-
data["viewgraphType"] = "calendarView"
|
|
30135
|
-
data["beingShowTitleQue"] = True
|
|
30136
|
-
data["groupQueId"] = None
|
|
30137
|
-
data["viewgraphGanttConfigVO"] = None
|
|
30138
29236
|
if view_filters is not None:
|
|
30139
29237
|
data["viewgraphLimit"] = deepcopy(view_filters)
|
|
30140
29238
|
data["viewgraphLimitType"] = 1
|
|
30141
29239
|
data["viewgraphLimitFormula"] = ""
|
|
30142
29240
|
data["dataPermissionType"] = "CUSTOM"
|
|
30143
29241
|
data["dataScope"] = "CUSTOM" if view_filters else "ALL"
|
|
30144
|
-
if patch is not None:
|
|
30145
|
-
_apply_view_config_groups(
|
|
30146
|
-
data,
|
|
30147
|
-
patch=patch,
|
|
30148
|
-
view_type=view_type,
|
|
30149
|
-
visible_que_id_values=visible_que_id_values,
|
|
30150
|
-
field_map=field_map or {},
|
|
30151
|
-
fields_by_que_id=fields_by_que_id or {},
|
|
30152
|
-
hierarchy_filter_groups=hierarchy_filter_groups,
|
|
30153
|
-
config=payload if isinstance(payload, dict) else None,
|
|
30154
|
-
id_to_name=_collect_field_id_to_name(schema) if isinstance(schema, dict) else None,
|
|
30155
|
-
)
|
|
30156
29242
|
_apply_view_query_conditions_payload(data, query_condition_payload)
|
|
30157
29243
|
_apply_view_associated_resources_payload(data, associated_resources_payload)
|
|
30158
29244
|
return data
|
|
30159
29245
|
|
|
30160
29246
|
|
|
30161
|
-
|
|
30162
|
-
|
|
30163
|
-
|
|
30164
|
-
|
|
30165
|
-
|
|
30166
|
-
|
|
30167
|
-
}
|
|
30168
|
-
|
|
30169
|
-
|
|
30170
|
-
def _apply_view_config_groups(
|
|
30171
|
-
data: JSONObject,
|
|
30172
|
-
*,
|
|
30173
|
-
patch: ViewUpsertPatch,
|
|
30174
|
-
view_type: str,
|
|
30175
|
-
visible_que_id_values: list[int],
|
|
30176
|
-
field_map: dict[str, int],
|
|
30177
|
-
fields_by_que_id: dict[str, dict[str, Any]],
|
|
30178
|
-
hierarchy_filter_groups: list[list[dict[str, Any]]] | None,
|
|
30179
|
-
config: dict[str, Any] | None = None,
|
|
30180
|
-
id_to_name: dict[int, str] | None = None,
|
|
30181
|
-
) -> None:
|
|
30182
|
-
"""编译 complete 档新字段组:data_permission/sort/card/hierarchy/display/record/share。"""
|
|
30183
|
-
if patch.data_permission is not None:
|
|
30184
|
-
data["dataPermissionType"] = "CONFIG" if patch.data_permission.type == "config" else "CUSTOM"
|
|
30185
|
-
data["dataScope"] = _DATA_PERMISSION_SCOPE_MAP.get(patch.data_permission.scope, "ALL")
|
|
30186
|
-
if patch.sort is not None:
|
|
30187
|
-
if patch.sort:
|
|
30188
|
-
sorts: list[dict[str, Any]] = []
|
|
30189
|
-
for rule in patch.sort:
|
|
30190
|
-
field = fields_by_que_id.get(rule.que_id) or {}
|
|
30191
|
-
que_type = _coerce_positive_int(field.get("que_type")) or FIELD_TYPE_TO_QUESTION_TYPE.get(
|
|
30192
|
-
str(field.get("type") or FieldType.text.value), 2
|
|
30193
|
-
)
|
|
30194
|
-
sorts.append(
|
|
30195
|
-
{
|
|
30196
|
-
"queId": rule.que_id,
|
|
30197
|
-
"beingSortAscend": bool(rule.ascending),
|
|
30198
|
-
"queType": que_type,
|
|
30199
|
-
}
|
|
30200
|
-
)
|
|
30201
|
-
data["sortType"] = "customSort"
|
|
30202
|
-
data["viewgraphSorts"] = sorts
|
|
30203
|
-
else:
|
|
30204
|
-
data["sortType"] = "defaultSort"
|
|
30205
|
-
data["viewgraphSorts"] = [{"queId": 0, "beingSortAscend": True, "queType": 8}]
|
|
30206
|
-
card = patch.card
|
|
30207
|
-
config = config or {}
|
|
30208
|
-
id_to_name = id_to_name or {}
|
|
30209
|
-
if card is not None:
|
|
30210
|
-
if card.title_que_id is not None:
|
|
30211
|
-
data["titleQue"] = card.title_que_id
|
|
30212
|
-
if card.cover_que_id is not None:
|
|
30213
|
-
data["frontCoverQueId"] = card.cover_que_id
|
|
30214
|
-
if card.clipping_mode is not None:
|
|
30215
|
-
data["clippingMode"] = card.clipping_mode
|
|
30216
|
-
if card.image_adaption is not None:
|
|
30217
|
-
data["beingImageAdaption"] = card.image_adaption
|
|
30218
|
-
if card.show_field_title is not None:
|
|
30219
|
-
data["beingShowQueTitle"] = card.show_field_title
|
|
30220
|
-
if card.show_title is not None:
|
|
30221
|
-
data["beingShowTitleQue"] = card.show_title
|
|
30222
|
-
if card.show_cover is not None:
|
|
30223
|
-
data["beingShowCover"] = card.show_cover
|
|
30224
|
-
if card.group_que_id is not None:
|
|
30225
|
-
data["groupQueId"] = card.group_que_id
|
|
30226
|
-
if card.group_color is not None:
|
|
30227
|
-
data["beingGroupColor"] = card.group_color
|
|
30228
|
-
if patch.hierarchy is not None and view_type == "hierarchy":
|
|
30229
|
-
data["viewgraphHierarchyConfigVO"] = {
|
|
30230
|
-
"titleQueId": patch.hierarchy.title_que_id,
|
|
30231
|
-
"childrenQueId": patch.hierarchy.children_que_id,
|
|
30232
|
-
"rootNodeFilter": deepcopy(hierarchy_filter_groups) if hierarchy_filter_groups is not None else [],
|
|
30233
|
-
}
|
|
30234
|
-
data["titleQue"] = patch.hierarchy.title_que_id
|
|
30235
|
-
if patch.calendar is not None and view_type == "calendar":
|
|
30236
|
-
data["viewgraphCalendarConfigVO"] = {
|
|
30237
|
-
"titleQueId": patch.calendar.title_que_id,
|
|
30238
|
-
"dateQueId": patch.calendar.date_que_id,
|
|
30239
|
-
"colorQueId": patch.calendar.color_que_id,
|
|
30240
|
-
}
|
|
30241
|
-
data["titleQue"] = patch.calendar.title_que_id
|
|
30242
|
-
display = patch.display
|
|
30243
|
-
if display is not None:
|
|
30244
|
-
if display.row_height is not None:
|
|
30245
|
-
data["defaultRowHigh"] = display.row_height
|
|
30246
|
-
if display.pin_navigate is not None:
|
|
30247
|
-
data["beingPinNavigate"] = display.pin_navigate
|
|
30248
|
-
if display.remark is not None:
|
|
30249
|
-
data["remark"] = display.remark
|
|
30250
|
-
record = patch.record
|
|
30251
|
-
if record is not None:
|
|
30252
|
-
if record.audit_visible is not None:
|
|
30253
|
-
data["beingAuditRecordVisible"] = record.audit_visible
|
|
30254
|
-
if record.data_log_visible is not None:
|
|
30255
|
-
data["beingDataLogVisible"] = record.data_log_visible
|
|
30256
|
-
if record.qrobot_record_visible is not None:
|
|
30257
|
-
data["beingQrobotRecordVisible"] = record.qrobot_record_visible
|
|
30258
|
-
if record.workflow_future_visible is not None:
|
|
30259
|
-
data["beingWorkflowNodeFutureListVisible"] = record.workflow_future_visible
|
|
30260
|
-
if record.comment is not None:
|
|
30261
|
-
data["beingCommentStatus"] = record.comment
|
|
30262
|
-
share = patch.share
|
|
30263
|
-
if share is not None:
|
|
30264
|
-
if share.need_pass is not None:
|
|
30265
|
-
data["beingNeedPass"] = share.need_pass
|
|
30266
|
-
data["needPass"] = share.need_pass
|
|
30267
|
-
if share.password is None:
|
|
30268
|
-
# 声明未带密码(提取语义:不回传敏感值)——显式清空 payload 里读面带来的旧密码;
|
|
30269
|
-
# 后端 isNotBlank(空)跳过校验且不更新密码列,原密码保留(Java AbstractViewgraphStrategicService:424)
|
|
30270
|
-
data["viewgraphPass"] = ""
|
|
30271
|
-
data.pop("pass", None)
|
|
30272
|
-
if share.password is not None:
|
|
30273
|
-
data["viewgraphPass"] = share.password
|
|
30274
|
-
if share.force_login is not None:
|
|
30275
|
-
data["beingForceLogin"] = share.force_login
|
|
30276
|
-
if share.print_enabled is not None:
|
|
30277
|
-
data["beingPrintStatus"] = share.print_enabled
|
|
30278
|
-
|
|
30279
|
-
|
|
30280
|
-
def _stale_view_reference_warning(
|
|
30281
|
-
*,
|
|
30282
|
-
scope: str,
|
|
30283
|
-
view_name: str,
|
|
30284
|
-
field_ids: list[Any] | None = None,
|
|
30285
|
-
values: list[Any] | None = None,
|
|
30286
|
-
option: bool = False,
|
|
30287
|
-
) -> dict[str, Any]:
|
|
30288
|
-
"""存量死引用(已删字段/已删选项)透传警告:后端保存视图不校验引用存在性并原样透传,
|
|
30289
|
-
CLI 同口径透传写回,仅以警告留痕,不再整视图拒绝(apply(get(S))≡S 重放兼容)。"""
|
|
30290
|
-
warning = _warning(
|
|
30291
|
-
"STALE_OPTION_REFERENCE" if option else "STALE_FIELD_REFERENCE",
|
|
30292
|
-
(
|
|
30293
|
-
f"view {scope} references option values not present in the current form; original values passed through (backend tolerates stale references)"
|
|
30294
|
-
if option
|
|
30295
|
-
else f"view {scope} references fields not present in the current form; ids passed through (backend tolerates stale references)"
|
|
30296
|
-
),
|
|
30297
|
-
scope=scope,
|
|
30298
|
-
view_name=view_name,
|
|
30299
|
-
)
|
|
30300
|
-
if field_ids:
|
|
30301
|
-
warning["field_ids"] = list(field_ids)
|
|
30302
|
-
if values:
|
|
30303
|
-
warning["values"] = list(values)
|
|
30304
|
-
return warning
|
|
30305
|
-
|
|
30306
|
-
|
|
30307
|
-
def _iter_view_config_group_field_names(patch: ViewUpsertPatch) -> list[tuple[str, list[int]]]:
|
|
30308
|
-
"""枚举 complete 档新字段组引用的 queId(用于 UNKNOWN_VIEW_FIELD 校验)。"""
|
|
30309
|
-
groups: list[tuple[str, list[int]]] = []
|
|
30310
|
-
if patch.sort:
|
|
30311
|
-
groups.append(("sort", [rule.que_id for rule in patch.sort]))
|
|
30312
|
-
if patch.card is not None:
|
|
30313
|
-
groups.append(
|
|
30314
|
-
(
|
|
30315
|
-
"card",
|
|
30316
|
-
[
|
|
30317
|
-
que
|
|
30318
|
-
for que in (patch.card.title_que_id, patch.card.cover_que_id, patch.card.group_que_id)
|
|
30319
|
-
if que
|
|
30320
|
-
],
|
|
30321
|
-
)
|
|
30322
|
-
)
|
|
30323
|
-
if patch.gantt is not None:
|
|
30324
|
-
gantt_ids = [
|
|
30325
|
-
que
|
|
30326
|
-
for que in (patch.gantt.title_que_id, patch.gantt.start_que_id, patch.gantt.end_que_id)
|
|
30327
|
-
if que
|
|
30328
|
-
]
|
|
30329
|
-
gantt_ids.extend(group.que_id for group in patch.gantt.groups or [])
|
|
30330
|
-
for dep in patch.gantt.dependencies or []:
|
|
30331
|
-
gantt_ids.extend([dep.dependency_que_id, dep.predecessor_que_id])
|
|
30332
|
-
if gantt_ids:
|
|
30333
|
-
groups.append(("gantt", gantt_ids))
|
|
30334
|
-
if patch.hierarchy is not None:
|
|
30335
|
-
groups.append(("hierarchy", [patch.hierarchy.title_que_id, patch.hierarchy.children_que_id]))
|
|
30336
|
-
if patch.calendar is not None:
|
|
30337
|
-
calendar_ids = [patch.calendar.title_que_id, patch.calendar.date_que_id]
|
|
30338
|
-
if patch.calendar.color_que_id is not None:
|
|
30339
|
-
calendar_ids.append(patch.calendar.color_que_id)
|
|
30340
|
-
groups.append(("calendar", calendar_ids))
|
|
30341
|
-
return groups
|
|
30342
|
-
|
|
30343
|
-
|
|
30344
|
-
|
|
30345
|
-
def _resolve_view_visible_que_ids(patch: ViewUpsertPatch) -> list[int]:
|
|
30346
|
-
"""收集视图引用的业务字段和已知系统字段 queId。"""
|
|
30347
|
-
ordered: list[int] = []
|
|
30348
|
-
extra_ids: list[int | None] = []
|
|
30349
|
-
if patch.hierarchy is not None:
|
|
30350
|
-
extra_ids.extend([patch.hierarchy.title_que_id, patch.hierarchy.children_que_id])
|
|
30351
|
-
if patch.calendar is not None:
|
|
30352
|
-
extra_ids.extend([patch.calendar.title_que_id, patch.calendar.date_que_id, patch.calendar.color_que_id])
|
|
30353
|
-
for value in [*patch.columns, patch.title_que_id, patch.start_que_id, patch.end_que_id, patch.group_que_id, *extra_ids]:
|
|
30354
|
-
que_id = _coerce_any_int(value)
|
|
30355
|
-
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:
|
|
30356
|
-
ordered.append(que_id)
|
|
29247
|
+
def _resolve_view_visible_field_names(patch: ViewUpsertPatch) -> list[str]:
|
|
29248
|
+
ordered: list[str] = []
|
|
29249
|
+
for value in [*patch.columns, patch.title_field, patch.start_field, patch.end_field, patch.group_by]:
|
|
29250
|
+
name = str(value or "").strip()
|
|
29251
|
+
if name and name not in _KNOWN_SYSTEM_VIEW_COLUMNS and name not in ordered:
|
|
29252
|
+
ordered.append(name)
|
|
30357
29253
|
return ordered
|
|
30358
29254
|
|
|
30359
29255
|
|
|
30360
|
-
|
|
30361
29256
|
def _build_public_gantt_payload(
|
|
30362
29257
|
fields: list[dict[str, Any]],
|
|
30363
29258
|
field_map: dict[str, int],
|
|
@@ -30365,35 +29260,22 @@ def _build_public_gantt_payload(
|
|
|
30365
29260
|
) -> dict[str, Any]:
|
|
30366
29261
|
if patch.type.value != "gantt":
|
|
30367
29262
|
return {}
|
|
30368
|
-
|
|
30369
|
-
|
|
30370
|
-
|
|
30371
|
-
or patch.title_que_id
|
|
30372
|
-
or (patch.columns[0] if patch.columns else None)
|
|
30373
|
-
)
|
|
30374
|
-
start_que_id = getattr(gantt_config, "start_que_id", None) or patch.start_que_id
|
|
30375
|
-
end_que_id = getattr(gantt_config, "end_que_id", None) or patch.end_que_id
|
|
30376
|
-
dependency = gantt_config.dependencies[0] if gantt_config is not None and gantt_config.dependencies else None
|
|
30377
|
-
auto_calibration = bool(getattr(gantt_config, "auto_calibration", False) or False)
|
|
29263
|
+
title_field_name = str((patch.title_field or (patch.columns[0] if patch.columns else "")) or "").strip()
|
|
29264
|
+
start_field_name = str(patch.start_field or "").strip()
|
|
29265
|
+
end_field_name = str(patch.end_field or "").strip()
|
|
30378
29266
|
return {
|
|
30379
|
-
"titleQueId":
|
|
30380
|
-
"startTimeQueId":
|
|
30381
|
-
"endTimeQueId":
|
|
30382
|
-
"defaultTimeDimension":
|
|
30383
|
-
"ganttGroupVOList": [
|
|
30384
|
-
{
|
|
30385
|
-
"groupQueId": group.que_id,
|
|
30386
|
-
"ordinal": group.ordinal if group.ordinal is not None else index,
|
|
30387
|
-
}
|
|
30388
|
-
for index, group in enumerate((gantt_config.groups if gantt_config is not None else []), start=1)
|
|
30389
|
-
],
|
|
29267
|
+
"titleQueId": field_map.get(title_field_name),
|
|
29268
|
+
"startTimeQueId": field_map.get(start_field_name),
|
|
29269
|
+
"endTimeQueId": field_map.get(end_field_name),
|
|
29270
|
+
"defaultTimeDimension": "week",
|
|
29271
|
+
"ganttGroupVOList": [],
|
|
30390
29272
|
"ganttDependencyVO": {
|
|
30391
|
-
"dependencyQueId":
|
|
30392
|
-
"predecessorTaskQueId":
|
|
30393
|
-
"
|
|
30394
|
-
"
|
|
30395
|
-
"endEndOptionId":
|
|
30396
|
-
"endStartOptionId":
|
|
29273
|
+
"dependencyQueId": None,
|
|
29274
|
+
"predecessorTaskQueId": None,
|
|
29275
|
+
"startEndOptionId": None,
|
|
29276
|
+
"startStartOptionId": None,
|
|
29277
|
+
"endEndOptionId": None,
|
|
29278
|
+
"endStartOptionId": None,
|
|
30397
29279
|
},
|
|
30398
29280
|
"ganttAutoCalibrationVO": {
|
|
30399
29281
|
"autoCalibrationRuleVO": {
|
|
@@ -30404,15 +29286,15 @@ def _build_public_gantt_payload(
|
|
|
30404
29286
|
"endStartFinish": True,
|
|
30405
29287
|
"endEndFinish": False,
|
|
30406
29288
|
},
|
|
30407
|
-
"beingAutoCalibration":
|
|
30408
|
-
"userAutoCalibration":
|
|
29289
|
+
"beingAutoCalibration": False,
|
|
29290
|
+
"userAutoCalibration": False,
|
|
30409
29291
|
},
|
|
30410
29292
|
}
|
|
30411
29293
|
|
|
30412
29294
|
|
|
30413
29295
|
def _build_view_filter_groups(
|
|
30414
29296
|
*,
|
|
30415
|
-
|
|
29297
|
+
current_fields_by_name: dict[str, dict[str, Any]],
|
|
30416
29298
|
filters: list[Any],
|
|
30417
29299
|
) -> tuple[list[list[dict[str, Any]]], list[dict[str, Any]]]:
|
|
30418
29300
|
translated_rules: list[dict[str, Any]] = []
|
|
@@ -30422,14 +29304,14 @@ def _build_view_filter_groups(
|
|
|
30422
29304
|
raw_rule = raw_rule.model_dump(mode="json")
|
|
30423
29305
|
if not isinstance(raw_rule, dict):
|
|
30424
29306
|
continue
|
|
30425
|
-
|
|
30426
|
-
field =
|
|
29307
|
+
field_name = str(raw_rule.get("field_name") or "").strip()
|
|
29308
|
+
field = current_fields_by_name.get(field_name)
|
|
30427
29309
|
if field is None:
|
|
30428
29310
|
issues.append(
|
|
30429
29311
|
{
|
|
30430
29312
|
"error_code": "UNKNOWN_VIEW_FIELD",
|
|
30431
|
-
"missing_fields": [
|
|
30432
|
-
"reason_path": "filters[].
|
|
29313
|
+
"missing_fields": [field_name] if field_name else [],
|
|
29314
|
+
"reason_path": "filters[].field_name",
|
|
30433
29315
|
}
|
|
30434
29316
|
)
|
|
30435
29317
|
continue
|
|
@@ -30437,9 +29319,6 @@ def _build_view_filter_groups(
|
|
|
30437
29319
|
if issue:
|
|
30438
29320
|
issues.append(issue)
|
|
30439
29321
|
continue
|
|
30440
|
-
stale_warning = translated_rule.pop("_stale_warning", None)
|
|
30441
|
-
if stale_warning is not None:
|
|
30442
|
-
issues.append(stale_warning)
|
|
30443
29322
|
translated_rules.append(translated_rule)
|
|
30444
29323
|
return ([translated_rules] if translated_rules else []), issues
|
|
30445
29324
|
|
|
@@ -30450,9 +29329,9 @@ def _translate_view_filter_rule(*, field: dict[str, Any], rule: dict[str, Any])
|
|
|
30450
29329
|
field_type = str(field.get("type") or FieldType.text.value)
|
|
30451
29330
|
que_id = _coerce_positive_int(field.get("que_id")) or 0
|
|
30452
29331
|
que_type = _coerce_positive_int(field.get("que_type")) or FIELD_TYPE_TO_QUESTION_TYPE.get(field_type, 2)
|
|
30453
|
-
judge_values, judge_value_details,
|
|
30454
|
-
if
|
|
30455
|
-
return {},
|
|
29332
|
+
judge_values, judge_value_details, issue = _resolve_view_filter_values(field=field, values=values)
|
|
29333
|
+
if issue:
|
|
29334
|
+
return {}, issue
|
|
30456
29335
|
payload: dict[str, Any] = {
|
|
30457
29336
|
"queId": que_id,
|
|
30458
29337
|
"queTitle": str(field.get("name") or ""),
|
|
@@ -30484,9 +29363,6 @@ def _translate_view_filter_rule(*, field: dict[str, Any], rule: dict[str, Any])
|
|
|
30484
29363
|
elif operator == "not_empty":
|
|
30485
29364
|
payload["judgeType"] = JUDGE_UNEQUAL
|
|
30486
29365
|
payload["judgeValues"] = []
|
|
30487
|
-
if value_issue is not None:
|
|
30488
|
-
# 死选项透传:规则照常写入,warning 随 payload 携带由上层收集(_build_view_filter_groups 弹出)
|
|
30489
|
-
payload["_stale_warning"] = value_issue
|
|
30490
29366
|
return payload, None
|
|
30491
29367
|
|
|
30492
29368
|
|
|
@@ -30517,23 +29393,21 @@ def _resolve_view_filter_values(
|
|
|
30517
29393
|
unresolved_values.append(_stringify_condition_value(value))
|
|
30518
29394
|
continue
|
|
30519
29395
|
resolved_details.append({"id": resolved.get("id"), "value": str(resolved.get("value") or resolved.get("id"))})
|
|
30520
|
-
if unresolved_values
|
|
30521
|
-
# 全部值都解析不出(选项已删):原值透传(后端对存量死选项引用原样保存),
|
|
30522
|
-
# 以 STALE_OPTION_REFERENCE 警告留痕,不再整视图失败
|
|
29396
|
+
if unresolved_values:
|
|
30523
29397
|
return (
|
|
30524
|
-
[
|
|
30525
|
-
|
|
29398
|
+
[],
|
|
29399
|
+
[],
|
|
30526
29400
|
{
|
|
30527
|
-
"
|
|
29401
|
+
"error_code": "UNKNOWN_VIEW_FILTER_VALUE",
|
|
30528
29402
|
"reason_path": "filters[].values",
|
|
30529
29403
|
"missing_fields": [],
|
|
29404
|
+
"allowed_values": {"filter_values": sorted(option_by_value.keys())},
|
|
30530
29405
|
"details": {
|
|
30531
29406
|
"field_name": str(field.get("name") or ""),
|
|
30532
29407
|
"unresolved_values": unresolved_values,
|
|
30533
29408
|
},
|
|
30534
29409
|
},
|
|
30535
29410
|
)
|
|
30536
|
-
# 部分值解析不出:剔除已删选项值继续写(兼容存量脏数据,不再整视图失败)
|
|
30537
29411
|
return (
|
|
30538
29412
|
[str(detail["id"]) for detail in resolved_details if detail.get("id") is not None],
|
|
30539
29413
|
[_coerce_view_filter_value_detail(detail) for detail in resolved_details],
|
|
@@ -30577,10 +29451,6 @@ def _build_view_filter_value_details(values: list[Any]) -> list[dict[str, Any]]:
|
|
|
30577
29451
|
details.append(_coerce_view_filter_value_detail({"id": item_id, "value": item_value if item_value is not None else str(item_id)}))
|
|
30578
29452
|
elif isinstance(value, int):
|
|
30579
29453
|
details.append(_coerce_view_filter_value_detail({"id": value, "value": str(value)}))
|
|
30580
|
-
elif isinstance(value, str):
|
|
30581
|
-
# text/number 字段:AnswerValue.id 是 Integer(选项 id),字符串值无 id;
|
|
30582
|
-
# 后端 formula getId()==null → 走 getValue() 分支构建 ""value"" 形式
|
|
30583
|
-
details.append(_coerce_view_filter_value_detail({"id": None, "value": value}))
|
|
30584
29454
|
return details
|
|
30585
29455
|
|
|
30586
29456
|
|
|
@@ -30747,11 +29617,11 @@ def _view_filter_groups_semantic_readback(expected: Any, actual: Any) -> tuple[b
|
|
|
30747
29617
|
def _public_view_filter_groups_from_match_rules(
|
|
30748
29618
|
groups: Any,
|
|
30749
29619
|
*,
|
|
30750
|
-
|
|
29620
|
+
current_fields_by_name: dict[str, dict[str, Any]],
|
|
30751
29621
|
) -> list[list[dict[str, Any]]]:
|
|
30752
29622
|
fields_by_que_id = {
|
|
30753
29623
|
_coerce_positive_int(field.get("que_id")): field
|
|
30754
|
-
for field in
|
|
29624
|
+
for field in current_fields_by_name.values()
|
|
30755
29625
|
if isinstance(field, dict) and _coerce_positive_int(field.get("que_id")) is not None
|
|
30756
29626
|
}
|
|
30757
29627
|
public_groups: list[list[dict[str, Any]]] = []
|
|
@@ -30762,7 +29632,7 @@ def _public_view_filter_groups_from_match_rules(
|
|
|
30762
29632
|
field = fields_by_que_id.get(que_id) or {}
|
|
30763
29633
|
values = _public_view_filter_rule_values(rule, field=field)
|
|
30764
29634
|
public_rule: dict[str, Any] = {
|
|
30765
|
-
"
|
|
29635
|
+
"field_name": str(field.get("name") or rule.get("queTitle") or que_id),
|
|
30766
29636
|
"operator": _public_view_filter_operator_from_judge_type(rule.get("judgeType"), values=values),
|
|
30767
29637
|
}
|
|
30768
29638
|
if public_rule["operator"] == "in":
|
|
@@ -30787,8 +29657,6 @@ def _public_view_filter_operator_from_judge_type(judge_type: Any, *, values: lis
|
|
|
30787
29657
|
if normalized in {JUDGE_INCLUDE, JUDGE_FUZZY_MATCH}:
|
|
30788
29658
|
return "contains"
|
|
30789
29659
|
if normalized in {JUDGE_EQUAL_ANY, JUDGE_INCLUDE_ANY}:
|
|
30790
|
-
# ANY 判型+空值 = "该字段未选任何值"的合法存量形态(judgeType 保真回写,
|
|
30791
|
-
# 转 is_empty 会丢判型——后端 9 与 0 是不同判定域,实测回读不同)
|
|
30792
29660
|
return "in"
|
|
30793
29661
|
if normalized == JUDGE_GREATER_OR_EQUAL:
|
|
30794
29662
|
return "gte"
|