@josephyan/qingflow-cli 0.2.0-beta.62 → 0.2.0-beta.64
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/qingflow_mcp/builder_facade/models.py +225 -0
- package/src/qingflow_mcp/builder_facade/service.py +1416 -35
- package/src/qingflow_mcp/cli/commands/builder.py +58 -1
- package/src/qingflow_mcp/server_app_builder.py +25 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +244 -2
- package/src/qingflow_mcp/tools/custom_button_tools.py +177 -0
- package/src/qingflow_mcp/tools/import_tools.py +217 -90
|
@@ -21,6 +21,7 @@ from ..solution.compiler.view_compiler import VIEW_TYPE_MAP
|
|
|
21
21
|
from ..solution.executor import _build_viewgraph_questions, _compact_dict, extract_field_map
|
|
22
22
|
from ..solution.spec_models import FieldType, FormLayoutRowSpec, FormLayoutSectionSpec, ViewSpec
|
|
23
23
|
from ..tools.app_tools import AppTools
|
|
24
|
+
from ..tools.custom_button_tools import CustomButtonTools
|
|
24
25
|
from ..tools.directory_tools import DirectoryTools
|
|
25
26
|
from ..tools.package_tools import PackageTools
|
|
26
27
|
from ..tools.portal_tools import PortalTools
|
|
@@ -39,6 +40,8 @@ from .models import (
|
|
|
39
40
|
AppViewsReadResponse,
|
|
40
41
|
ChartApplyRequest,
|
|
41
42
|
ChartUpsertPatch,
|
|
43
|
+
CustomButtonMatchRulePatch,
|
|
44
|
+
CustomButtonPatch,
|
|
42
45
|
FieldPatch,
|
|
43
46
|
FieldRemovePatch,
|
|
44
47
|
FieldSelector,
|
|
@@ -54,8 +57,12 @@ from .models import (
|
|
|
54
57
|
PublicFieldType,
|
|
55
58
|
PublicRelationMode,
|
|
56
59
|
PublicChartType,
|
|
60
|
+
PublicButtonTriggerAction,
|
|
57
61
|
PublicViewType,
|
|
62
|
+
PublicViewButtonConfigType,
|
|
63
|
+
PublicViewButtonType,
|
|
58
64
|
SchemaPlanRequest,
|
|
65
|
+
ViewButtonBindingPatch,
|
|
59
66
|
ViewUpsertPatch,
|
|
60
67
|
ViewFilterOperator,
|
|
61
68
|
ViewsPlanRequest,
|
|
@@ -142,6 +149,7 @@ class AiBuilderFacade:
|
|
|
142
149
|
self,
|
|
143
150
|
*,
|
|
144
151
|
apps: AppTools,
|
|
152
|
+
buttons: CustomButtonTools,
|
|
145
153
|
packages: PackageTools,
|
|
146
154
|
views: ViewTools,
|
|
147
155
|
workflows: WorkflowTools,
|
|
@@ -152,6 +160,7 @@ class AiBuilderFacade:
|
|
|
152
160
|
solutions: SolutionTools,
|
|
153
161
|
) -> None:
|
|
154
162
|
self.apps = apps
|
|
163
|
+
self.buttons = buttons
|
|
155
164
|
self.packages = packages
|
|
156
165
|
self.views = views
|
|
157
166
|
self.workflows = workflows
|
|
@@ -1291,6 +1300,355 @@ class AiBuilderFacade:
|
|
|
1291
1300
|
**match,
|
|
1292
1301
|
}
|
|
1293
1302
|
|
|
1303
|
+
def app_custom_button_list(self, *, profile: str, app_key: str) -> JSONObject:
|
|
1304
|
+
normalized_args = {"app_key": app_key}
|
|
1305
|
+
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
1306
|
+
permission_outcome = self._guard_app_permission(
|
|
1307
|
+
profile=profile,
|
|
1308
|
+
app_key=app_key,
|
|
1309
|
+
required_permission="edit_app",
|
|
1310
|
+
normalized_args=normalized_args,
|
|
1311
|
+
)
|
|
1312
|
+
if permission_outcome.block is not None:
|
|
1313
|
+
return permission_outcome.block
|
|
1314
|
+
permission_outcomes.append(permission_outcome)
|
|
1315
|
+
|
|
1316
|
+
def finalize(response: JSONObject) -> JSONObject:
|
|
1317
|
+
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
1318
|
+
|
|
1319
|
+
listing = self.buttons.custom_button_list(profile=profile, app_key=app_key, being_draft=True, include_raw=False)
|
|
1320
|
+
items = [
|
|
1321
|
+
_normalize_custom_button_summary(item)
|
|
1322
|
+
for item in (listing.get("items") or [])
|
|
1323
|
+
if isinstance(item, dict)
|
|
1324
|
+
]
|
|
1325
|
+
return finalize(
|
|
1326
|
+
{
|
|
1327
|
+
"status": "success",
|
|
1328
|
+
"error_code": None,
|
|
1329
|
+
"recoverable": False,
|
|
1330
|
+
"message": "read custom button summary",
|
|
1331
|
+
"normalized_args": normalized_args,
|
|
1332
|
+
"missing_fields": [],
|
|
1333
|
+
"allowed_values": {},
|
|
1334
|
+
"details": {},
|
|
1335
|
+
"request_id": None,
|
|
1336
|
+
"suggested_next_call": None,
|
|
1337
|
+
"noop": False,
|
|
1338
|
+
"warnings": [],
|
|
1339
|
+
"verification": {"custom_buttons_verified": True},
|
|
1340
|
+
"verified": True,
|
|
1341
|
+
"app_key": app_key,
|
|
1342
|
+
"count": len(items),
|
|
1343
|
+
"buttons": items,
|
|
1344
|
+
}
|
|
1345
|
+
)
|
|
1346
|
+
|
|
1347
|
+
def app_custom_button_get(self, *, profile: str, app_key: str, button_id: int) -> JSONObject:
|
|
1348
|
+
normalized_args = {"app_key": app_key, "button_id": button_id}
|
|
1349
|
+
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
1350
|
+
permission_outcome = self._guard_app_permission(
|
|
1351
|
+
profile=profile,
|
|
1352
|
+
app_key=app_key,
|
|
1353
|
+
required_permission="edit_app",
|
|
1354
|
+
normalized_args=normalized_args,
|
|
1355
|
+
)
|
|
1356
|
+
if permission_outcome.block is not None:
|
|
1357
|
+
return permission_outcome.block
|
|
1358
|
+
permission_outcomes.append(permission_outcome)
|
|
1359
|
+
|
|
1360
|
+
def finalize(response: JSONObject) -> JSONObject:
|
|
1361
|
+
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
1362
|
+
|
|
1363
|
+
detail = self.buttons.custom_button_get(
|
|
1364
|
+
profile=profile,
|
|
1365
|
+
app_key=app_key,
|
|
1366
|
+
button_id=button_id,
|
|
1367
|
+
being_draft=True,
|
|
1368
|
+
include_raw=False,
|
|
1369
|
+
)
|
|
1370
|
+
button = _normalize_custom_button_detail(detail.get("result") if isinstance(detail.get("result"), dict) else {})
|
|
1371
|
+
return finalize(
|
|
1372
|
+
{
|
|
1373
|
+
"status": "success",
|
|
1374
|
+
"error_code": None,
|
|
1375
|
+
"recoverable": False,
|
|
1376
|
+
"message": "read custom button detail",
|
|
1377
|
+
"normalized_args": normalized_args,
|
|
1378
|
+
"missing_fields": [],
|
|
1379
|
+
"allowed_values": {},
|
|
1380
|
+
"details": {},
|
|
1381
|
+
"request_id": None,
|
|
1382
|
+
"suggested_next_call": None,
|
|
1383
|
+
"noop": False,
|
|
1384
|
+
"warnings": [],
|
|
1385
|
+
"verification": {"custom_button_verified": True},
|
|
1386
|
+
"verified": True,
|
|
1387
|
+
"app_key": app_key,
|
|
1388
|
+
"button_id": button_id,
|
|
1389
|
+
"button": button,
|
|
1390
|
+
}
|
|
1391
|
+
)
|
|
1392
|
+
|
|
1393
|
+
def app_custom_button_create(self, *, profile: str, app_key: str, payload: CustomButtonPatch) -> JSONObject:
|
|
1394
|
+
normalized_args = {"app_key": app_key, "payload": payload.model_dump(mode="json")}
|
|
1395
|
+
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
1396
|
+
permission_outcome = self._guard_app_permission(
|
|
1397
|
+
profile=profile,
|
|
1398
|
+
app_key=app_key,
|
|
1399
|
+
required_permission="edit_app",
|
|
1400
|
+
normalized_args=normalized_args,
|
|
1401
|
+
)
|
|
1402
|
+
if permission_outcome.block is not None:
|
|
1403
|
+
return permission_outcome.block
|
|
1404
|
+
permission_outcomes.append(permission_outcome)
|
|
1405
|
+
|
|
1406
|
+
def finalize(response: JSONObject) -> JSONObject:
|
|
1407
|
+
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
1408
|
+
|
|
1409
|
+
edit_version_no, edit_context_error = self._ensure_app_edit_context(
|
|
1410
|
+
profile=profile,
|
|
1411
|
+
app_key=app_key,
|
|
1412
|
+
normalized_args=normalized_args,
|
|
1413
|
+
failure_code="CUSTOM_BUTTON_CREATE_FAILED",
|
|
1414
|
+
)
|
|
1415
|
+
if edit_context_error is not None:
|
|
1416
|
+
return finalize(edit_context_error)
|
|
1417
|
+
|
|
1418
|
+
create_result = self.buttons.custom_button_create(
|
|
1419
|
+
profile=profile,
|
|
1420
|
+
app_key=app_key,
|
|
1421
|
+
payload=_serialize_custom_button_payload(payload),
|
|
1422
|
+
)
|
|
1423
|
+
raw_result = create_result.get("result")
|
|
1424
|
+
button_id = _extract_custom_button_id(raw_result)
|
|
1425
|
+
if button_id is None:
|
|
1426
|
+
return finalize(
|
|
1427
|
+
_failed(
|
|
1428
|
+
"CUSTOM_BUTTON_CREATE_FAILED",
|
|
1429
|
+
"custom button create succeeded but no button_id was returned",
|
|
1430
|
+
normalized_args=normalized_args,
|
|
1431
|
+
details={"app_key": app_key, "result": deepcopy(raw_result), "edit_version_no": edit_version_no},
|
|
1432
|
+
suggested_next_call={"tool_name": "app_custom_button_list", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1433
|
+
)
|
|
1434
|
+
)
|
|
1435
|
+
try:
|
|
1436
|
+
readback = self.buttons.custom_button_get(
|
|
1437
|
+
profile=profile,
|
|
1438
|
+
app_key=app_key,
|
|
1439
|
+
button_id=button_id,
|
|
1440
|
+
being_draft=True,
|
|
1441
|
+
include_raw=False,
|
|
1442
|
+
)
|
|
1443
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1444
|
+
api_error = _coerce_api_error(error)
|
|
1445
|
+
response = {
|
|
1446
|
+
"status": "partial_success",
|
|
1447
|
+
"error_code": "CUSTOM_BUTTON_READBACK_PENDING",
|
|
1448
|
+
"recoverable": True,
|
|
1449
|
+
"message": "created custom button; detail readback unavailable",
|
|
1450
|
+
"normalized_args": normalized_args,
|
|
1451
|
+
"missing_fields": [],
|
|
1452
|
+
"allowed_values": {},
|
|
1453
|
+
"details": {
|
|
1454
|
+
"app_key": app_key,
|
|
1455
|
+
"button_id": button_id,
|
|
1456
|
+
"edit_version_no": edit_version_no,
|
|
1457
|
+
"transport_error": _transport_error_payload(api_error),
|
|
1458
|
+
},
|
|
1459
|
+
"request_id": api_error.request_id,
|
|
1460
|
+
"suggested_next_call": {
|
|
1461
|
+
"tool_name": "app_custom_button_get",
|
|
1462
|
+
"arguments": {"profile": profile, "app_key": app_key, "button_id": button_id},
|
|
1463
|
+
},
|
|
1464
|
+
"noop": False,
|
|
1465
|
+
"warnings": [_warning("CUSTOM_BUTTON_READBACK_PENDING", "custom button was created, but detail readback is not available")],
|
|
1466
|
+
"verification": {"custom_button_verified": False},
|
|
1467
|
+
"verified": False,
|
|
1468
|
+
"app_key": app_key,
|
|
1469
|
+
"button_id": button_id,
|
|
1470
|
+
}
|
|
1471
|
+
if _is_permission_restricted_api_error(api_error):
|
|
1472
|
+
response = _apply_permission_outcomes(
|
|
1473
|
+
response,
|
|
1474
|
+
_verification_read_outcome(
|
|
1475
|
+
resource="custom_button",
|
|
1476
|
+
target={"app_key": app_key, "button_id": button_id},
|
|
1477
|
+
transport_error=api_error,
|
|
1478
|
+
),
|
|
1479
|
+
)
|
|
1480
|
+
return finalize(response)
|
|
1481
|
+
button = _normalize_custom_button_detail(readback.get("result") if isinstance(readback.get("result"), dict) else {})
|
|
1482
|
+
return finalize(
|
|
1483
|
+
{
|
|
1484
|
+
"status": "success",
|
|
1485
|
+
"error_code": None,
|
|
1486
|
+
"recoverable": False,
|
|
1487
|
+
"message": "created custom button",
|
|
1488
|
+
"normalized_args": normalized_args,
|
|
1489
|
+
"missing_fields": [],
|
|
1490
|
+
"allowed_values": {},
|
|
1491
|
+
"details": {},
|
|
1492
|
+
"request_id": None,
|
|
1493
|
+
"suggested_next_call": None,
|
|
1494
|
+
"noop": False,
|
|
1495
|
+
"warnings": [],
|
|
1496
|
+
"verification": {"custom_button_verified": True},
|
|
1497
|
+
"verified": True,
|
|
1498
|
+
"app_key": app_key,
|
|
1499
|
+
"button_id": button_id,
|
|
1500
|
+
"edit_version_no": edit_version_no,
|
|
1501
|
+
"button": button,
|
|
1502
|
+
}
|
|
1503
|
+
)
|
|
1504
|
+
|
|
1505
|
+
def app_custom_button_update(
|
|
1506
|
+
self,
|
|
1507
|
+
*,
|
|
1508
|
+
profile: str,
|
|
1509
|
+
app_key: str,
|
|
1510
|
+
button_id: int,
|
|
1511
|
+
payload: CustomButtonPatch,
|
|
1512
|
+
) -> JSONObject:
|
|
1513
|
+
normalized_args = {"app_key": app_key, "button_id": button_id, "payload": payload.model_dump(mode="json")}
|
|
1514
|
+
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
1515
|
+
permission_outcome = self._guard_app_permission(
|
|
1516
|
+
profile=profile,
|
|
1517
|
+
app_key=app_key,
|
|
1518
|
+
required_permission="edit_app",
|
|
1519
|
+
normalized_args=normalized_args,
|
|
1520
|
+
)
|
|
1521
|
+
if permission_outcome.block is not None:
|
|
1522
|
+
return permission_outcome.block
|
|
1523
|
+
permission_outcomes.append(permission_outcome)
|
|
1524
|
+
|
|
1525
|
+
def finalize(response: JSONObject) -> JSONObject:
|
|
1526
|
+
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
1527
|
+
|
|
1528
|
+
edit_version_no, edit_context_error = self._ensure_app_edit_context(
|
|
1529
|
+
profile=profile,
|
|
1530
|
+
app_key=app_key,
|
|
1531
|
+
normalized_args=normalized_args,
|
|
1532
|
+
failure_code="CUSTOM_BUTTON_UPDATE_FAILED",
|
|
1533
|
+
)
|
|
1534
|
+
if edit_context_error is not None:
|
|
1535
|
+
return finalize(edit_context_error)
|
|
1536
|
+
|
|
1537
|
+
self.buttons.custom_button_update(
|
|
1538
|
+
profile=profile,
|
|
1539
|
+
app_key=app_key,
|
|
1540
|
+
button_id=button_id,
|
|
1541
|
+
payload=_serialize_custom_button_payload(payload),
|
|
1542
|
+
)
|
|
1543
|
+
try:
|
|
1544
|
+
readback = self.buttons.custom_button_get(
|
|
1545
|
+
profile=profile,
|
|
1546
|
+
app_key=app_key,
|
|
1547
|
+
button_id=button_id,
|
|
1548
|
+
being_draft=True,
|
|
1549
|
+
include_raw=False,
|
|
1550
|
+
)
|
|
1551
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1552
|
+
api_error = _coerce_api_error(error)
|
|
1553
|
+
response = {
|
|
1554
|
+
"status": "partial_success",
|
|
1555
|
+
"error_code": "CUSTOM_BUTTON_READBACK_PENDING",
|
|
1556
|
+
"recoverable": True,
|
|
1557
|
+
"message": "updated custom button; detail readback unavailable",
|
|
1558
|
+
"normalized_args": normalized_args,
|
|
1559
|
+
"missing_fields": [],
|
|
1560
|
+
"allowed_values": {},
|
|
1561
|
+
"details": {
|
|
1562
|
+
"app_key": app_key,
|
|
1563
|
+
"button_id": button_id,
|
|
1564
|
+
"edit_version_no": edit_version_no,
|
|
1565
|
+
"transport_error": _transport_error_payload(api_error),
|
|
1566
|
+
},
|
|
1567
|
+
"request_id": api_error.request_id,
|
|
1568
|
+
"suggested_next_call": {
|
|
1569
|
+
"tool_name": "app_custom_button_get",
|
|
1570
|
+
"arguments": {"profile": profile, "app_key": app_key, "button_id": button_id},
|
|
1571
|
+
},
|
|
1572
|
+
"noop": False,
|
|
1573
|
+
"warnings": [_warning("CUSTOM_BUTTON_READBACK_PENDING", "custom button was updated, but detail readback is not available")],
|
|
1574
|
+
"verification": {"custom_button_verified": False},
|
|
1575
|
+
"verified": False,
|
|
1576
|
+
"app_key": app_key,
|
|
1577
|
+
"button_id": button_id,
|
|
1578
|
+
}
|
|
1579
|
+
if _is_permission_restricted_api_error(api_error):
|
|
1580
|
+
response = _apply_permission_outcomes(
|
|
1581
|
+
response,
|
|
1582
|
+
_verification_read_outcome(
|
|
1583
|
+
resource="custom_button",
|
|
1584
|
+
target={"app_key": app_key, "button_id": button_id},
|
|
1585
|
+
transport_error=api_error,
|
|
1586
|
+
),
|
|
1587
|
+
)
|
|
1588
|
+
return finalize(response)
|
|
1589
|
+
button = _normalize_custom_button_detail(readback.get("result") if isinstance(readback.get("result"), dict) else {})
|
|
1590
|
+
return finalize(
|
|
1591
|
+
{
|
|
1592
|
+
"status": "success",
|
|
1593
|
+
"error_code": None,
|
|
1594
|
+
"recoverable": False,
|
|
1595
|
+
"message": "updated custom button",
|
|
1596
|
+
"normalized_args": normalized_args,
|
|
1597
|
+
"missing_fields": [],
|
|
1598
|
+
"allowed_values": {},
|
|
1599
|
+
"details": {},
|
|
1600
|
+
"request_id": None,
|
|
1601
|
+
"suggested_next_call": None,
|
|
1602
|
+
"noop": False,
|
|
1603
|
+
"warnings": [],
|
|
1604
|
+
"verification": {"custom_button_verified": True},
|
|
1605
|
+
"verified": True,
|
|
1606
|
+
"app_key": app_key,
|
|
1607
|
+
"button_id": button_id,
|
|
1608
|
+
"edit_version_no": edit_version_no,
|
|
1609
|
+
"button": button,
|
|
1610
|
+
}
|
|
1611
|
+
)
|
|
1612
|
+
|
|
1613
|
+
def app_custom_button_delete(self, *, profile: str, app_key: str, button_id: int) -> JSONObject:
|
|
1614
|
+
normalized_args = {"app_key": app_key, "button_id": button_id}
|
|
1615
|
+
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
1616
|
+
permission_outcome = self._guard_app_permission(
|
|
1617
|
+
profile=profile,
|
|
1618
|
+
app_key=app_key,
|
|
1619
|
+
required_permission="edit_app",
|
|
1620
|
+
normalized_args=normalized_args,
|
|
1621
|
+
)
|
|
1622
|
+
if permission_outcome.block is not None:
|
|
1623
|
+
return permission_outcome.block
|
|
1624
|
+
permission_outcomes.append(permission_outcome)
|
|
1625
|
+
|
|
1626
|
+
def finalize(response: JSONObject) -> JSONObject:
|
|
1627
|
+
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
1628
|
+
|
|
1629
|
+
self.buttons.custom_button_delete(profile=profile, app_key=app_key, button_id=button_id)
|
|
1630
|
+
return finalize(
|
|
1631
|
+
{
|
|
1632
|
+
"status": "success",
|
|
1633
|
+
"error_code": None,
|
|
1634
|
+
"recoverable": False,
|
|
1635
|
+
"message": "deleted custom button",
|
|
1636
|
+
"normalized_args": normalized_args,
|
|
1637
|
+
"missing_fields": [],
|
|
1638
|
+
"allowed_values": {},
|
|
1639
|
+
"details": {},
|
|
1640
|
+
"request_id": None,
|
|
1641
|
+
"suggested_next_call": None,
|
|
1642
|
+
"noop": False,
|
|
1643
|
+
"warnings": [],
|
|
1644
|
+
"verification": {"custom_button_deleted": True},
|
|
1645
|
+
"verified": True,
|
|
1646
|
+
"app_key": app_key,
|
|
1647
|
+
"button_id": button_id,
|
|
1648
|
+
"deleted": True,
|
|
1649
|
+
}
|
|
1650
|
+
)
|
|
1651
|
+
|
|
1294
1652
|
def _resolve_app_matches_in_visible_apps(
|
|
1295
1653
|
self,
|
|
1296
1654
|
*,
|
|
@@ -2231,7 +2589,17 @@ class AiBuilderFacade:
|
|
|
2231
2589
|
upsert_views = _build_views_preset(request.preset, list(field_names))
|
|
2232
2590
|
blocking_issues: list[dict[str, Any]] = []
|
|
2233
2591
|
for patch in upsert_views:
|
|
2234
|
-
|
|
2592
|
+
raw_columns = [str(name or "").strip() for name in (patch.get("columns") or []) if str(name or "").strip()]
|
|
2593
|
+
columns = _filter_known_system_view_columns(raw_columns)
|
|
2594
|
+
if patch.get("type") in {"table", "card"} and raw_columns and not columns:
|
|
2595
|
+
blocking_issues.append(
|
|
2596
|
+
{
|
|
2597
|
+
"error_code": "VALIDATION_ERROR",
|
|
2598
|
+
"view_name": patch.get("name"),
|
|
2599
|
+
"message": "view columns must include at least one real app field; system columns cannot be applied directly",
|
|
2600
|
+
"ignored_system_columns": [name for name in raw_columns if name in _KNOWN_SYSTEM_VIEW_COLUMNS],
|
|
2601
|
+
}
|
|
2602
|
+
)
|
|
2235
2603
|
missing_columns = [name for name in columns if name not in field_names]
|
|
2236
2604
|
if missing_columns:
|
|
2237
2605
|
blocking_issues.append({"error_code": "UNKNOWN_VIEW_FIELD", "view_name": patch.get("name"), "missing_fields": missing_columns})
|
|
@@ -3303,6 +3671,56 @@ class AiBuilderFacade:
|
|
|
3303
3671
|
for field in parsed_schema["fields"]
|
|
3304
3672
|
if isinstance(field, dict) and str(field.get("name") or "")
|
|
3305
3673
|
}
|
|
3674
|
+
requires_custom_button_validation = any(
|
|
3675
|
+
any(binding.button_type == PublicViewButtonType.custom for binding in (patch.buttons or []))
|
|
3676
|
+
for patch in upsert_views
|
|
3677
|
+
)
|
|
3678
|
+
valid_custom_button_ids: set[int] = set()
|
|
3679
|
+
custom_button_details_by_id: dict[int, dict[str, Any]] = {}
|
|
3680
|
+
if requires_custom_button_validation:
|
|
3681
|
+
try:
|
|
3682
|
+
button_listing = self.buttons.custom_button_list(
|
|
3683
|
+
profile=profile,
|
|
3684
|
+
app_key=app_key,
|
|
3685
|
+
being_draft=True,
|
|
3686
|
+
include_raw=False,
|
|
3687
|
+
)
|
|
3688
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
3689
|
+
api_error = _coerce_api_error(error)
|
|
3690
|
+
return finalize(
|
|
3691
|
+
_failed_from_api_error(
|
|
3692
|
+
"CUSTOM_BUTTON_LIST_FAILED",
|
|
3693
|
+
api_error,
|
|
3694
|
+
normalized_args=normalized_args,
|
|
3695
|
+
details=_with_state_read_blocked_details({"app_key": app_key}, resource="custom_button", error=api_error),
|
|
3696
|
+
suggested_next_call={"tool_name": "app_custom_button_list", "arguments": {"profile": profile, "app_key": app_key}},
|
|
3697
|
+
)
|
|
3698
|
+
)
|
|
3699
|
+
valid_custom_button_ids = {
|
|
3700
|
+
button_id
|
|
3701
|
+
for item in (button_listing.get("items") or [])
|
|
3702
|
+
if isinstance(item, dict) and (button_id := _coerce_positive_int(item.get("button_id"))) is not None
|
|
3703
|
+
}
|
|
3704
|
+
referenced_custom_button_ids = {
|
|
3705
|
+
binding.button_id
|
|
3706
|
+
for patch in upsert_views
|
|
3707
|
+
for binding in (patch.buttons or [])
|
|
3708
|
+
if binding.button_type == PublicViewButtonType.custom and binding.button_id in valid_custom_button_ids
|
|
3709
|
+
}
|
|
3710
|
+
for button_id in sorted(referenced_custom_button_ids):
|
|
3711
|
+
try:
|
|
3712
|
+
detail = self.buttons.custom_button_get(
|
|
3713
|
+
profile=profile,
|
|
3714
|
+
app_key=app_key,
|
|
3715
|
+
button_id=button_id,
|
|
3716
|
+
being_draft=True,
|
|
3717
|
+
include_raw=False,
|
|
3718
|
+
)
|
|
3719
|
+
except (QingflowApiError, RuntimeError):
|
|
3720
|
+
continue
|
|
3721
|
+
detail_result = detail.get("result")
|
|
3722
|
+
if isinstance(detail_result, dict):
|
|
3723
|
+
custom_button_details_by_id[button_id] = _normalize_custom_button_detail(detail_result)
|
|
3306
3724
|
removed: list[str] = []
|
|
3307
3725
|
view_results: list[dict[str, Any]] = []
|
|
3308
3726
|
for name in remove_views:
|
|
@@ -3339,7 +3757,24 @@ class AiBuilderFacade:
|
|
|
3339
3757
|
and _extract_view_name(view) not in remove_views
|
|
3340
3758
|
]
|
|
3341
3759
|
for ordinal, patch in enumerate(upsert_views, start=1):
|
|
3342
|
-
|
|
3760
|
+
apply_columns = _resolve_view_visible_field_names(patch)
|
|
3761
|
+
ignored_system_columns = [
|
|
3762
|
+
name
|
|
3763
|
+
for name in [str(value or "").strip() for value in patch.columns]
|
|
3764
|
+
if name in _KNOWN_SYSTEM_VIEW_COLUMNS
|
|
3765
|
+
]
|
|
3766
|
+
if patch.type in {PublicViewType.table, PublicViewType.card} and patch.columns and not apply_columns:
|
|
3767
|
+
return _failed(
|
|
3768
|
+
"VALIDATION_ERROR",
|
|
3769
|
+
"view columns must include at least one real app field; system columns cannot be applied directly",
|
|
3770
|
+
normalized_args=normalized_args,
|
|
3771
|
+
details={
|
|
3772
|
+
"app_key": app_key,
|
|
3773
|
+
"view_name": patch.name,
|
|
3774
|
+
"ignored_system_columns": ignored_system_columns,
|
|
3775
|
+
},
|
|
3776
|
+
)
|
|
3777
|
+
missing_columns = [name for name in apply_columns if name not in field_names]
|
|
3343
3778
|
if missing_columns:
|
|
3344
3779
|
return _failed(
|
|
3345
3780
|
"UNKNOWN_VIEW_FIELD",
|
|
@@ -3349,6 +3784,7 @@ class AiBuilderFacade:
|
|
|
3349
3784
|
"app_key": app_key,
|
|
3350
3785
|
"view_name": patch.name,
|
|
3351
3786
|
"missing_fields": missing_columns,
|
|
3787
|
+
"ignored_system_columns": ignored_system_columns,
|
|
3352
3788
|
},
|
|
3353
3789
|
missing_fields=missing_columns,
|
|
3354
3790
|
suggested_next_call={"tool_name": "app_read_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
@@ -3396,6 +3832,33 @@ class AiBuilderFacade:
|
|
|
3396
3832
|
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]},
|
|
3397
3833
|
suggested_next_call={"tool_name": "app_read_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
3398
3834
|
)
|
|
3835
|
+
explicit_button_dtos: list[dict[str, Any]] | None = None
|
|
3836
|
+
expected_button_summary: list[dict[str, Any]] | None = None
|
|
3837
|
+
if patch.buttons is not None:
|
|
3838
|
+
explicit_button_dtos, button_issues = _build_view_button_dtos(
|
|
3839
|
+
current_fields_by_name=current_fields_by_name,
|
|
3840
|
+
bindings=patch.buttons,
|
|
3841
|
+
valid_custom_button_ids=valid_custom_button_ids,
|
|
3842
|
+
)
|
|
3843
|
+
if button_issues:
|
|
3844
|
+
first_issue = button_issues[0]
|
|
3845
|
+
return _failed(
|
|
3846
|
+
str(first_issue.get("error_code") or "INVALID_VIEW_BUTTON"),
|
|
3847
|
+
"view buttons reference invalid fields, values, or custom buttons",
|
|
3848
|
+
normalized_args=normalized_args,
|
|
3849
|
+
details={
|
|
3850
|
+
"app_key": app_key,
|
|
3851
|
+
"view_name": patch.name,
|
|
3852
|
+
**first_issue,
|
|
3853
|
+
},
|
|
3854
|
+
missing_fields=list(first_issue.get("missing_fields") or []),
|
|
3855
|
+
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]},
|
|
3856
|
+
suggested_next_call={"tool_name": "app_custom_button_list", "arguments": {"profile": profile, "app_key": app_key}},
|
|
3857
|
+
)
|
|
3858
|
+
expected_button_summary = _normalize_expected_view_buttons_for_compare(
|
|
3859
|
+
explicit_button_dtos or [],
|
|
3860
|
+
custom_button_details_by_id=custom_button_details_by_id,
|
|
3861
|
+
)
|
|
3399
3862
|
matched_existing_view: dict[str, Any] | None = None
|
|
3400
3863
|
existing_key: str | None = None
|
|
3401
3864
|
if patch.view_key:
|
|
@@ -3441,17 +3904,21 @@ class AiBuilderFacade:
|
|
|
3441
3904
|
schema=schema,
|
|
3442
3905
|
patch=patch,
|
|
3443
3906
|
view_filters=translated_filters,
|
|
3907
|
+
current_fields_by_name=current_fields_by_name,
|
|
3908
|
+
explicit_button_dtos=explicit_button_dtos,
|
|
3444
3909
|
)
|
|
3445
3910
|
self.views.view_update(profile=profile, viewgraph_key=existing_key, payload=payload)
|
|
3446
3911
|
system_view_sync: dict[str, Any] | None = None
|
|
3447
3912
|
if system_view_list_type is not None and patch.type.value == "table":
|
|
3448
3913
|
operation_phase = "default_view_apply_config_sync"
|
|
3449
|
-
system_view_sync = self.
|
|
3914
|
+
system_view_sync = self._sync_system_view_and_restore_buttons(
|
|
3450
3915
|
profile=profile,
|
|
3451
3916
|
app_key=app_key,
|
|
3917
|
+
viewgraph_key=existing_key,
|
|
3918
|
+
payload=payload,
|
|
3452
3919
|
list_type=system_view_list_type,
|
|
3453
3920
|
schema=schema,
|
|
3454
|
-
visible_field_names=
|
|
3921
|
+
visible_field_names=apply_columns,
|
|
3455
3922
|
)
|
|
3456
3923
|
if not bool(system_view_sync.get("verified")):
|
|
3457
3924
|
failure_entry = {
|
|
@@ -3473,6 +3940,7 @@ class AiBuilderFacade:
|
|
|
3473
3940
|
"list_type": system_view_list_type,
|
|
3474
3941
|
"expected_visible_order": system_view_sync.get("expected_visible_order"),
|
|
3475
3942
|
"actual_visible_order": system_view_sync.get("actual_visible_order"),
|
|
3943
|
+
"apply_columns": apply_columns,
|
|
3476
3944
|
},
|
|
3477
3945
|
}
|
|
3478
3946
|
failed_views.append(failure_entry)
|
|
@@ -3481,14 +3949,16 @@ class AiBuilderFacade:
|
|
|
3481
3949
|
updated.append(patch.name)
|
|
3482
3950
|
view_results.append(
|
|
3483
3951
|
{
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3952
|
+
"name": patch.name,
|
|
3953
|
+
"view_key": existing_key,
|
|
3954
|
+
"type": patch.type.value,
|
|
3955
|
+
"status": "updated",
|
|
3956
|
+
"expected_filters": deepcopy(translated_filters),
|
|
3957
|
+
"expected_buttons": deepcopy(expected_button_summary),
|
|
3958
|
+
"system_view_sync": system_view_sync,
|
|
3959
|
+
"apply_columns": deepcopy(apply_columns),
|
|
3960
|
+
}
|
|
3961
|
+
)
|
|
3492
3962
|
else:
|
|
3493
3963
|
template_key = _pick_view_template_key(existing_view_list, desired_type=patch.type.value)
|
|
3494
3964
|
should_copy_template = patch.type.value == "table" and template_key and not translated_filters
|
|
@@ -3502,6 +3972,8 @@ class AiBuilderFacade:
|
|
|
3502
3972
|
schema=schema,
|
|
3503
3973
|
patch=patch,
|
|
3504
3974
|
view_filters=translated_filters,
|
|
3975
|
+
current_fields_by_name=current_fields_by_name,
|
|
3976
|
+
explicit_button_dtos=explicit_button_dtos,
|
|
3505
3977
|
)
|
|
3506
3978
|
self.views.view_update(profile=profile, viewgraph_key=created_key, payload=payload)
|
|
3507
3979
|
else:
|
|
@@ -3512,6 +3984,8 @@ class AiBuilderFacade:
|
|
|
3512
3984
|
patch=patch,
|
|
3513
3985
|
ordinal=ordinal,
|
|
3514
3986
|
view_filters=translated_filters,
|
|
3987
|
+
current_fields_by_name=current_fields_by_name,
|
|
3988
|
+
explicit_button_dtos=explicit_button_dtos,
|
|
3515
3989
|
)
|
|
3516
3990
|
create_result = self.views.view_create(profile=profile, payload=payload)
|
|
3517
3991
|
raw_created = create_result.get("result")
|
|
@@ -3522,14 +3996,15 @@ class AiBuilderFacade:
|
|
|
3522
3996
|
created_key = raw_created.strip() or None
|
|
3523
3997
|
created.append(patch.name)
|
|
3524
3998
|
view_results.append(
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3999
|
+
{
|
|
4000
|
+
"name": patch.name,
|
|
4001
|
+
"view_key": created_key,
|
|
4002
|
+
"type": patch.type.value,
|
|
4003
|
+
"status": "created",
|
|
4004
|
+
"expected_filters": deepcopy(translated_filters),
|
|
4005
|
+
"expected_buttons": deepcopy(expected_button_summary),
|
|
4006
|
+
}
|
|
4007
|
+
)
|
|
3533
4008
|
except (QingflowApiError, RuntimeError) as error:
|
|
3534
4009
|
api_error = _coerce_api_error(error)
|
|
3535
4010
|
should_retry_minimal = operation_phase != "default_view_apply_config_sync" and (
|
|
@@ -3540,14 +4015,68 @@ class AiBuilderFacade:
|
|
|
3540
4015
|
try:
|
|
3541
4016
|
if existing_key or created_key:
|
|
3542
4017
|
target_key = created_key or existing_key or ""
|
|
4018
|
+
fallback_button_dtos = explicit_button_dtos
|
|
4019
|
+
if fallback_button_dtos is None:
|
|
4020
|
+
fallback_config_response = self.views.view_get_config(profile=profile, viewgraph_key=target_key)
|
|
4021
|
+
fallback_config = (
|
|
4022
|
+
fallback_config_response.get("result")
|
|
4023
|
+
if isinstance(fallback_config_response.get("result"), dict)
|
|
4024
|
+
else {}
|
|
4025
|
+
)
|
|
4026
|
+
fallback_button_dtos = _extract_existing_view_button_dtos(fallback_config)
|
|
3543
4027
|
fallback_payload = _build_minimal_view_payload(
|
|
3544
4028
|
app_key=app_key,
|
|
3545
4029
|
schema=schema,
|
|
3546
4030
|
patch=patch,
|
|
3547
4031
|
ordinal=ordinal,
|
|
3548
4032
|
view_filters=translated_filters,
|
|
4033
|
+
current_fields_by_name=current_fields_by_name,
|
|
4034
|
+
explicit_button_dtos=fallback_button_dtos,
|
|
3549
4035
|
)
|
|
3550
4036
|
self.views.view_update(profile=profile, viewgraph_key=target_key, payload=fallback_payload)
|
|
4037
|
+
system_view_sync: dict[str, Any] | None = None
|
|
4038
|
+
fallback_system_view_list_type = (
|
|
4039
|
+
_resolve_system_view_list_type(view_key=target_key, view_name=patch.name)
|
|
4040
|
+
if patch.type.value == "table" and target_key
|
|
4041
|
+
else None
|
|
4042
|
+
)
|
|
4043
|
+
if fallback_system_view_list_type is not None:
|
|
4044
|
+
operation_phase = "default_view_apply_config_sync"
|
|
4045
|
+
system_view_sync = self._sync_system_view_and_restore_buttons(
|
|
4046
|
+
profile=profile,
|
|
4047
|
+
app_key=app_key,
|
|
4048
|
+
viewgraph_key=target_key,
|
|
4049
|
+
payload=fallback_payload,
|
|
4050
|
+
list_type=fallback_system_view_list_type,
|
|
4051
|
+
schema=schema,
|
|
4052
|
+
visible_field_names=apply_columns,
|
|
4053
|
+
)
|
|
4054
|
+
if not bool(system_view_sync.get("verified")):
|
|
4055
|
+
failure_entry = {
|
|
4056
|
+
"name": patch.name,
|
|
4057
|
+
"view_key": target_key,
|
|
4058
|
+
"type": patch.type.value,
|
|
4059
|
+
"status": "failed",
|
|
4060
|
+
"error_code": "SYSTEM_VIEW_ORDER_SYNC_FAILED",
|
|
4061
|
+
"message": "default view column order did not verify through app apply/baseInfo readback",
|
|
4062
|
+
"request_id": None,
|
|
4063
|
+
"backend_code": None,
|
|
4064
|
+
"http_status": None,
|
|
4065
|
+
"operation": "sync_default_view",
|
|
4066
|
+
"details": {
|
|
4067
|
+
"app_key": app_key,
|
|
4068
|
+
"view_name": patch.name,
|
|
4069
|
+
"view_key": target_key,
|
|
4070
|
+
"view_type": patch.type.value,
|
|
4071
|
+
"list_type": fallback_system_view_list_type,
|
|
4072
|
+
"expected_visible_order": system_view_sync.get("expected_visible_order"),
|
|
4073
|
+
"actual_visible_order": system_view_sync.get("actual_visible_order"),
|
|
4074
|
+
"apply_columns": apply_columns,
|
|
4075
|
+
},
|
|
4076
|
+
}
|
|
4077
|
+
failed_views.append(failure_entry)
|
|
4078
|
+
view_results.append(failure_entry)
|
|
4079
|
+
continue
|
|
3551
4080
|
if existing_key:
|
|
3552
4081
|
updated.append(patch.name)
|
|
3553
4082
|
view_results.append(
|
|
@@ -3558,6 +4087,9 @@ class AiBuilderFacade:
|
|
|
3558
4087
|
"status": "updated",
|
|
3559
4088
|
"fallback_applied": True,
|
|
3560
4089
|
"expected_filters": deepcopy(translated_filters),
|
|
4090
|
+
"expected_buttons": deepcopy(expected_button_summary),
|
|
4091
|
+
"system_view_sync": system_view_sync,
|
|
4092
|
+
"apply_columns": deepcopy(apply_columns),
|
|
3561
4093
|
}
|
|
3562
4094
|
)
|
|
3563
4095
|
else:
|
|
@@ -3570,6 +4102,9 @@ class AiBuilderFacade:
|
|
|
3570
4102
|
"status": "created",
|
|
3571
4103
|
"fallback_applied": True,
|
|
3572
4104
|
"expected_filters": deepcopy(translated_filters),
|
|
4105
|
+
"expected_buttons": deepcopy(expected_button_summary),
|
|
4106
|
+
"system_view_sync": system_view_sync,
|
|
4107
|
+
"apply_columns": deepcopy(apply_columns),
|
|
3573
4108
|
}
|
|
3574
4109
|
)
|
|
3575
4110
|
continue
|
|
@@ -3579,6 +4114,8 @@ class AiBuilderFacade:
|
|
|
3579
4114
|
patch=patch,
|
|
3580
4115
|
ordinal=ordinal,
|
|
3581
4116
|
view_filters=translated_filters,
|
|
4117
|
+
current_fields_by_name=current_fields_by_name,
|
|
4118
|
+
explicit_button_dtos=explicit_button_dtos,
|
|
3582
4119
|
)
|
|
3583
4120
|
self.views.view_create(profile=profile, payload=fallback_payload)
|
|
3584
4121
|
created.append(patch.name)
|
|
@@ -3665,6 +4202,10 @@ class AiBuilderFacade:
|
|
|
3665
4202
|
verification_by_view: list[dict[str, Any]] = []
|
|
3666
4203
|
filter_readback_pending = False
|
|
3667
4204
|
filter_mismatches: list[dict[str, Any]] = []
|
|
4205
|
+
button_readback_pending = False
|
|
4206
|
+
button_mismatches: list[dict[str, Any]] = []
|
|
4207
|
+
custom_button_readback_pending = False
|
|
4208
|
+
custom_button_readback_pending_entries: list[dict[str, Any]] = []
|
|
3668
4209
|
for item in view_results:
|
|
3669
4210
|
status = str(item.get("status") or "")
|
|
3670
4211
|
name = str(item.get("name") or "")
|
|
@@ -3688,6 +4229,7 @@ class AiBuilderFacade:
|
|
|
3688
4229
|
if isinstance(system_view_sync, dict):
|
|
3689
4230
|
verification_entry["system_view_sync"] = deepcopy(system_view_sync)
|
|
3690
4231
|
expected_filters = item.get("expected_filters") or []
|
|
4232
|
+
expected_buttons = item.get("expected_buttons") if isinstance(item.get("expected_buttons"), list) else None
|
|
3691
4233
|
if expected_filters:
|
|
3692
4234
|
if verified_views_unavailable or not present_in_readback:
|
|
3693
4235
|
verification_entry["filters_verified"] = None
|
|
@@ -3743,6 +4285,70 @@ class AiBuilderFacade:
|
|
|
3743
4285
|
"category": api_error.category,
|
|
3744
4286
|
}
|
|
3745
4287
|
filter_readback_pending = True
|
|
4288
|
+
if expected_buttons is not None:
|
|
4289
|
+
if verified_views_unavailable or not present_in_readback:
|
|
4290
|
+
verification_entry["buttons_verified"] = None
|
|
4291
|
+
verification_entry["button_readback_pending"] = True
|
|
4292
|
+
button_readback_pending = True
|
|
4293
|
+
else:
|
|
4294
|
+
verification_key = item_view_key
|
|
4295
|
+
if not verification_key:
|
|
4296
|
+
matched_keys = verified_view_keys_by_name.get(name) or []
|
|
4297
|
+
if len(matched_keys) == 1:
|
|
4298
|
+
verification_key = matched_keys[0]
|
|
4299
|
+
else:
|
|
4300
|
+
verification_entry["buttons_verified"] = None
|
|
4301
|
+
verification_entry["button_readback_pending"] = True
|
|
4302
|
+
verification_entry["readback_ambiguous"] = True
|
|
4303
|
+
verification_entry["matching_view_keys"] = matched_keys
|
|
4304
|
+
button_readback_pending = True
|
|
4305
|
+
verification_by_view.append(verification_entry)
|
|
4306
|
+
continue
|
|
4307
|
+
try:
|
|
4308
|
+
config_response = self.views.view_get_config(profile=profile, viewgraph_key=verification_key)
|
|
4309
|
+
config_result = (config_response.get("result") or {}) if isinstance(config_response.get("result"), dict) else {}
|
|
4310
|
+
actual_buttons = _normalize_view_buttons_for_compare(config_result)
|
|
4311
|
+
button_comparison = _compare_view_button_summaries(
|
|
4312
|
+
expected=expected_buttons,
|
|
4313
|
+
actual=actual_buttons,
|
|
4314
|
+
)
|
|
4315
|
+
buttons_verified = bool(button_comparison.get("verified"))
|
|
4316
|
+
verification_entry["buttons_verified"] = buttons_verified
|
|
4317
|
+
verification_entry["view_key"] = verification_key
|
|
4318
|
+
verification_entry["expected_buttons"] = deepcopy(expected_buttons)
|
|
4319
|
+
verification_entry["actual_buttons"] = actual_buttons
|
|
4320
|
+
if button_comparison.get("custom_button_readback_pending"):
|
|
4321
|
+
verification_entry["custom_button_readback_pending"] = True
|
|
4322
|
+
verification_entry["pending_custom_buttons"] = deepcopy(button_comparison.get("pending_custom_buttons") or [])
|
|
4323
|
+
custom_button_readback_pending = True
|
|
4324
|
+
custom_button_readback_pending_entries.append(
|
|
4325
|
+
{
|
|
4326
|
+
"name": name,
|
|
4327
|
+
"type": item.get("type"),
|
|
4328
|
+
"view_key": verification_key,
|
|
4329
|
+
"pending_custom_buttons": deepcopy(button_comparison.get("pending_custom_buttons") or []),
|
|
4330
|
+
}
|
|
4331
|
+
)
|
|
4332
|
+
elif not buttons_verified:
|
|
4333
|
+
button_mismatches.append(
|
|
4334
|
+
{
|
|
4335
|
+
"name": name,
|
|
4336
|
+
"type": item.get("type"),
|
|
4337
|
+
"expected_buttons": deepcopy(expected_buttons),
|
|
4338
|
+
"actual_buttons": actual_buttons,
|
|
4339
|
+
}
|
|
4340
|
+
)
|
|
4341
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
4342
|
+
api_error = _coerce_api_error(error)
|
|
4343
|
+
verification_entry["buttons_verified"] = None
|
|
4344
|
+
verification_entry["button_readback_pending"] = True
|
|
4345
|
+
verification_entry["request_id"] = api_error.request_id
|
|
4346
|
+
verification_entry["transport_error"] = {
|
|
4347
|
+
"http_status": api_error.http_status,
|
|
4348
|
+
"backend_code": api_error.backend_code,
|
|
4349
|
+
"category": api_error.category,
|
|
4350
|
+
}
|
|
4351
|
+
button_readback_pending = True
|
|
3746
4352
|
verification_by_view.append(verification_entry)
|
|
3747
4353
|
elif status == "removed":
|
|
3748
4354
|
verification_by_view.append(
|
|
@@ -3769,6 +4375,7 @@ class AiBuilderFacade:
|
|
|
3769
4375
|
and all(name not in verified_names for name in removed)
|
|
3770
4376
|
)
|
|
3771
4377
|
view_filters_verified = verified and not filter_readback_pending and not filter_mismatches
|
|
4378
|
+
view_buttons_verified = verified and not button_readback_pending and not button_mismatches
|
|
3772
4379
|
noop = not created and not updated and not removed
|
|
3773
4380
|
if failed_views:
|
|
3774
4381
|
successful_changes = bool(created or updated or removed)
|
|
@@ -3781,34 +4388,73 @@ class AiBuilderFacade:
|
|
|
3781
4388
|
"normalized_args": normalized_args,
|
|
3782
4389
|
"missing_fields": [],
|
|
3783
4390
|
"allowed_values": {"view_types": [member.value for member in PublicViewType], "view.filter.operator": [member.value for member in ViewFilterOperator]},
|
|
3784
|
-
"details": {
|
|
4391
|
+
"details": {
|
|
4392
|
+
"per_view_results": view_results,
|
|
4393
|
+
"filter_mismatches": filter_mismatches,
|
|
4394
|
+
"button_mismatches": button_mismatches,
|
|
4395
|
+
**(
|
|
4396
|
+
{"custom_button_readback_pending": deepcopy(custom_button_readback_pending_entries)}
|
|
4397
|
+
if custom_button_readback_pending_entries
|
|
4398
|
+
else {}
|
|
4399
|
+
),
|
|
4400
|
+
},
|
|
3785
4401
|
"request_id": first_failure.get("request_id"),
|
|
3786
4402
|
"suggested_next_call": {"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
3787
4403
|
"backend_code": first_failure.get("backend_code"),
|
|
3788
4404
|
"http_status": first_failure.get("http_status"),
|
|
3789
4405
|
"noop": noop,
|
|
3790
|
-
"warnings":
|
|
4406
|
+
"warnings": (
|
|
4407
|
+
(
|
|
4408
|
+
[_warning("VIEW_FILTERS_UNVERIFIED", "view definitions may exist, but saved filter behavior is not fully verified")]
|
|
4409
|
+
if (filter_readback_pending or filter_mismatches)
|
|
4410
|
+
else []
|
|
4411
|
+
)
|
|
4412
|
+
+ (
|
|
4413
|
+
[_warning("VIEW_BUTTONS_UNVERIFIED", "view definitions may exist, but saved button behavior is not fully verified")]
|
|
4414
|
+
if (button_readback_pending or button_mismatches)
|
|
4415
|
+
else []
|
|
4416
|
+
)
|
|
4417
|
+
+ (
|
|
4418
|
+
[_warning("VIEW_CUSTOM_BUTTON_READBACK_PENDING", "system buttons verified, but draft custom button bindings are not fully visible through view readback yet")]
|
|
4419
|
+
if custom_button_readback_pending
|
|
4420
|
+
else []
|
|
4421
|
+
)
|
|
4422
|
+
),
|
|
3791
4423
|
"verification": {
|
|
3792
4424
|
"views_verified": verified,
|
|
3793
4425
|
"view_filters_verified": view_filters_verified,
|
|
4426
|
+
"view_buttons_verified": view_buttons_verified,
|
|
3794
4427
|
"views_read_unavailable": verified_views_unavailable,
|
|
3795
4428
|
"by_view": verification_by_view,
|
|
4429
|
+
"custom_button_readback_pending": custom_button_readback_pending,
|
|
4430
|
+
"custom_button_readback_pending_entries": deepcopy(custom_button_readback_pending_entries),
|
|
3796
4431
|
},
|
|
3797
4432
|
"app_key": app_key,
|
|
3798
4433
|
"views_diff": {"created": created, "updated": updated, "removed": removed, "failed": failed_views},
|
|
3799
|
-
"verified": verified and view_filters_verified,
|
|
4434
|
+
"verified": verified and view_filters_verified and view_buttons_verified,
|
|
3800
4435
|
}
|
|
3801
4436
|
return finalize(self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response))
|
|
3802
4437
|
warnings: list[dict[str, Any]] = []
|
|
3803
4438
|
if filter_readback_pending or filter_mismatches:
|
|
3804
4439
|
warnings.append(_warning("VIEW_FILTERS_UNVERIFIED", "view definitions were applied, but saved filter behavior is not fully verified"))
|
|
4440
|
+
if button_readback_pending or button_mismatches:
|
|
4441
|
+
warnings.append(_warning("VIEW_BUTTONS_UNVERIFIED", "view definitions were applied, but saved button behavior is not fully verified"))
|
|
4442
|
+
if custom_button_readback_pending:
|
|
4443
|
+
warnings.append(
|
|
4444
|
+
_warning(
|
|
4445
|
+
"VIEW_CUSTOM_BUTTON_READBACK_PENDING",
|
|
4446
|
+
"system buttons verified, but draft custom button bindings are not fully visible through view readback yet",
|
|
4447
|
+
)
|
|
4448
|
+
)
|
|
3805
4449
|
response = {
|
|
3806
|
-
"status": "success" if verified and view_filters_verified else "partial_success",
|
|
3807
|
-
"error_code": None if verified and view_filters_verified else ("VIEW_FILTER_READBACK_MISMATCH" if filter_mismatches else "VIEWS_READBACK_PENDING"),
|
|
3808
|
-
"recoverable": not (verified and view_filters_verified),
|
|
4450
|
+
"status": "success" if verified and view_filters_verified and view_buttons_verified else "partial_success",
|
|
4451
|
+
"error_code": None if verified and view_filters_verified and view_buttons_verified else ("VIEW_BUTTON_READBACK_MISMATCH" if button_mismatches else "VIEW_FILTER_READBACK_MISMATCH" if filter_mismatches else "VIEWS_READBACK_PENDING"),
|
|
4452
|
+
"recoverable": not (verified and view_filters_verified and view_buttons_verified),
|
|
3809
4453
|
"message": (
|
|
3810
4454
|
"applied view patch"
|
|
3811
|
-
if verified and view_filters_verified
|
|
4455
|
+
if verified and view_filters_verified and view_buttons_verified
|
|
4456
|
+
else "applied view patch; buttons did not fully verify"
|
|
4457
|
+
if button_mismatches
|
|
3812
4458
|
else "applied view patch; filters did not fully verify"
|
|
3813
4459
|
if filter_mismatches
|
|
3814
4460
|
else "applied view patch; views readback pending"
|
|
@@ -3816,21 +4462,33 @@ class AiBuilderFacade:
|
|
|
3816
4462
|
"normalized_args": normalized_args,
|
|
3817
4463
|
"missing_fields": [],
|
|
3818
4464
|
"allowed_values": {"view_types": [member.value for member in PublicViewType], "view.filter.operator": [member.value for member in ViewFilterOperator]},
|
|
3819
|
-
"details": {
|
|
4465
|
+
"details": {
|
|
4466
|
+
**({"filter_mismatches": filter_mismatches} if filter_mismatches else {}),
|
|
4467
|
+
**({"button_mismatches": button_mismatches} if button_mismatches else {}),
|
|
4468
|
+
**(
|
|
4469
|
+
{"custom_button_readback_pending": deepcopy(custom_button_readback_pending_entries)}
|
|
4470
|
+
if custom_button_readback_pending_entries
|
|
4471
|
+
else {}
|
|
4472
|
+
),
|
|
4473
|
+
},
|
|
3820
4474
|
"request_id": None,
|
|
3821
|
-
"suggested_next_call": None if verified and view_filters_verified else {"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
4475
|
+
"suggested_next_call": None if verified and view_filters_verified and view_buttons_verified else {"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
3822
4476
|
"noop": noop,
|
|
3823
4477
|
"warnings": warnings,
|
|
3824
4478
|
"verification": {
|
|
3825
4479
|
"views_verified": verified,
|
|
3826
4480
|
"view_filters_verified": view_filters_verified,
|
|
4481
|
+
"view_buttons_verified": view_buttons_verified,
|
|
3827
4482
|
"views_read_unavailable": verified_views_unavailable,
|
|
3828
4483
|
"filter_readback_pending": filter_readback_pending,
|
|
4484
|
+
"button_readback_pending": button_readback_pending,
|
|
4485
|
+
"custom_button_readback_pending": custom_button_readback_pending,
|
|
4486
|
+
"custom_button_readback_pending_entries": deepcopy(custom_button_readback_pending_entries),
|
|
3829
4487
|
"by_view": verification_by_view,
|
|
3830
4488
|
},
|
|
3831
4489
|
"app_key": app_key,
|
|
3832
4490
|
"views_diff": {"created": created, "updated": updated, "removed": removed, "failed": []},
|
|
3833
|
-
"verified": verified and view_filters_verified,
|
|
4491
|
+
"verified": verified and view_filters_verified and view_buttons_verified,
|
|
3834
4492
|
}
|
|
3835
4493
|
return finalize(self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response))
|
|
3836
4494
|
|
|
@@ -4572,6 +5230,28 @@ class AiBuilderFacade:
|
|
|
4572
5230
|
version_result = {}
|
|
4573
5231
|
return _coerce_positive_int(version_result.get("editVersionNo") or version_result.get("versionNo")) or int(current_schema.get("editVersionNo") or 1)
|
|
4574
5232
|
|
|
5233
|
+
def _ensure_app_edit_context(
|
|
5234
|
+
self,
|
|
5235
|
+
*,
|
|
5236
|
+
profile: str,
|
|
5237
|
+
app_key: str,
|
|
5238
|
+
normalized_args: dict[str, Any],
|
|
5239
|
+
failure_code: str,
|
|
5240
|
+
) -> tuple[int | None, JSONObject | None]:
|
|
5241
|
+
try:
|
|
5242
|
+
version_result = self.apps.app_get_edit_version_no(profile=profile, app_key=app_key).get("result") or {}
|
|
5243
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
5244
|
+
api_error = _coerce_api_error(error)
|
|
5245
|
+
return None, _failed_from_api_error(
|
|
5246
|
+
failure_code,
|
|
5247
|
+
api_error,
|
|
5248
|
+
normalized_args=normalized_args,
|
|
5249
|
+
details={"app_key": app_key, "phase": "prepare_edit_context"},
|
|
5250
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
5251
|
+
)
|
|
5252
|
+
edit_version_no = _coerce_positive_int(version_result.get("editVersionNo") or version_result.get("versionNo")) or 1
|
|
5253
|
+
return edit_version_no, None
|
|
5254
|
+
|
|
4575
5255
|
def _append_publish_result(self, *, profile: str, app_key: str, publish: bool, response: JSONObject) -> JSONObject:
|
|
4576
5256
|
verification = response.get("verification")
|
|
4577
5257
|
if not isinstance(verification, dict):
|
|
@@ -4651,6 +5331,29 @@ class AiBuilderFacade:
|
|
|
4651
5331
|
"verified": verified,
|
|
4652
5332
|
}
|
|
4653
5333
|
|
|
5334
|
+
def _sync_system_view_and_restore_buttons(
|
|
5335
|
+
self,
|
|
5336
|
+
*,
|
|
5337
|
+
profile: str,
|
|
5338
|
+
app_key: str,
|
|
5339
|
+
viewgraph_key: str,
|
|
5340
|
+
payload: dict[str, Any],
|
|
5341
|
+
list_type: int,
|
|
5342
|
+
schema: dict[str, Any],
|
|
5343
|
+
visible_field_names: list[str],
|
|
5344
|
+
) -> dict[str, Any]:
|
|
5345
|
+
sync_result = self._sync_system_view_apply_config(
|
|
5346
|
+
profile=profile,
|
|
5347
|
+
app_key=app_key,
|
|
5348
|
+
list_type=list_type,
|
|
5349
|
+
schema=schema,
|
|
5350
|
+
visible_field_names=visible_field_names,
|
|
5351
|
+
)
|
|
5352
|
+
if bool(sync_result.get("verified")) and "buttonConfigDTOList" in payload:
|
|
5353
|
+
self.views.view_update(profile=profile, viewgraph_key=viewgraph_key, payload=payload)
|
|
5354
|
+
sync_result = {**sync_result, "button_config_restored": True}
|
|
5355
|
+
return sync_result
|
|
5356
|
+
|
|
4654
5357
|
def _load_views_result(self, *, profile: str, app_key: str, tolerate_404: bool) -> tuple[Any, bool]:
|
|
4655
5358
|
try:
|
|
4656
5359
|
views = self.views.view_list_flat(profile=profile, app_key=app_key)
|
|
@@ -5002,6 +5705,158 @@ class AiBuilderFacade:
|
|
|
5002
5705
|
raise QingflowApiError(category="runtime", message="failed to attach app to package")
|
|
5003
5706
|
|
|
5004
5707
|
|
|
5708
|
+
def _extract_custom_button_id(value: Any) -> int | None:
|
|
5709
|
+
if isinstance(value, dict):
|
|
5710
|
+
for key in ("buttonId", "customButtonId", "id"):
|
|
5711
|
+
button_id = _coerce_positive_int(value.get(key))
|
|
5712
|
+
if button_id is not None:
|
|
5713
|
+
return button_id
|
|
5714
|
+
nested_result = value.get("result")
|
|
5715
|
+
if nested_result is not value:
|
|
5716
|
+
return _extract_custom_button_id(nested_result)
|
|
5717
|
+
return None
|
|
5718
|
+
return _coerce_positive_int(value)
|
|
5719
|
+
|
|
5720
|
+
|
|
5721
|
+
def _serialize_custom_button_payload(payload: CustomButtonPatch) -> dict[str, Any]:
|
|
5722
|
+
data = payload.model_dump(mode="json", exclude_none=True)
|
|
5723
|
+
serialized: dict[str, Any] = {
|
|
5724
|
+
"buttonText": data["button_text"],
|
|
5725
|
+
"backgroundColor": data["background_color"],
|
|
5726
|
+
"textColor": data["text_color"],
|
|
5727
|
+
"buttonIcon": data["button_icon"],
|
|
5728
|
+
"triggerAction": data["trigger_action"],
|
|
5729
|
+
}
|
|
5730
|
+
if str(data.get("trigger_link_url") or "").strip():
|
|
5731
|
+
serialized["triggerLinkUrl"] = data["trigger_link_url"]
|
|
5732
|
+
trigger_add_data_config = data.get("trigger_add_data_config")
|
|
5733
|
+
if isinstance(trigger_add_data_config, dict):
|
|
5734
|
+
serialized["triggerAddDataConfig"] = _serialize_custom_button_add_data_config(trigger_add_data_config)
|
|
5735
|
+
else:
|
|
5736
|
+
serialized["triggerAddDataConfig"] = _serialize_custom_button_add_data_config({})
|
|
5737
|
+
external_qrobot_config = data.get("external_qrobot_config")
|
|
5738
|
+
if isinstance(external_qrobot_config, dict):
|
|
5739
|
+
serialized["customButtonExternalQRobotRelationVO"] = _serialize_custom_button_external_qrobot_config(external_qrobot_config)
|
|
5740
|
+
trigger_wings_config = data.get("trigger_wings_config")
|
|
5741
|
+
if isinstance(trigger_wings_config, dict):
|
|
5742
|
+
serialized["triggerWingsConfig"] = _serialize_custom_button_wings_config(trigger_wings_config)
|
|
5743
|
+
return serialized
|
|
5744
|
+
|
|
5745
|
+
|
|
5746
|
+
def _serialize_custom_button_add_data_config(value: dict[str, Any]) -> dict[str, Any]:
|
|
5747
|
+
relation_rules = value.get("que_relation") or []
|
|
5748
|
+
return {
|
|
5749
|
+
"relatedAppKey": value.get("related_app_key"),
|
|
5750
|
+
"relatedAppName": value.get("related_app_name"),
|
|
5751
|
+
"queRelation": [_serialize_custom_button_match_rule(rule) for rule in relation_rules if isinstance(rule, dict)],
|
|
5752
|
+
}
|
|
5753
|
+
|
|
5754
|
+
|
|
5755
|
+
def _serialize_custom_button_external_qrobot_config(value: dict[str, Any]) -> dict[str, Any]:
|
|
5756
|
+
return {
|
|
5757
|
+
"externalQRobotConfigId": value.get("external_qrobot_config_id"),
|
|
5758
|
+
"triggeredText": value.get("triggered_text"),
|
|
5759
|
+
}
|
|
5760
|
+
|
|
5761
|
+
|
|
5762
|
+
def _serialize_custom_button_wings_config(value: dict[str, Any]) -> dict[str, Any]:
|
|
5763
|
+
return {
|
|
5764
|
+
"wingsAgentId": value.get("wings_agent_id"),
|
|
5765
|
+
"wingsAgentName": value.get("wings_agent_name"),
|
|
5766
|
+
"bindQueIdList": list(value.get("bind_que_id_list") or []),
|
|
5767
|
+
"bindFileQueIdList": list(value.get("bind_file_que_id_list") or []),
|
|
5768
|
+
"defaultPrompt": value.get("default_prompt"),
|
|
5769
|
+
"beingAutoSend": value.get("being_auto_send"),
|
|
5770
|
+
}
|
|
5771
|
+
|
|
5772
|
+
|
|
5773
|
+
def _serialize_custom_button_match_rule(value: dict[str, Any]) -> dict[str, Any]:
|
|
5774
|
+
serialized = {
|
|
5775
|
+
"queId": value.get("que_id"),
|
|
5776
|
+
"queTitle": value.get("que_title"),
|
|
5777
|
+
"queType": value.get("que_type"),
|
|
5778
|
+
"dateType": value.get("date_type"),
|
|
5779
|
+
"judgeType": value.get("judge_type"),
|
|
5780
|
+
"matchType": value.get("match_type"),
|
|
5781
|
+
"judgeValues": list(value.get("judge_values") or []),
|
|
5782
|
+
"judgeQueType": value.get("judge_que_type"),
|
|
5783
|
+
"judgeQueId": value.get("judge_que_id"),
|
|
5784
|
+
"pathValue": value.get("path_value"),
|
|
5785
|
+
"tableUpdateType": value.get("table_update_type"),
|
|
5786
|
+
"multiValue": value.get("multi_value"),
|
|
5787
|
+
"addRule": value.get("add_rule"),
|
|
5788
|
+
"fieldIdPrefix": value.get("field_id_prefix"),
|
|
5789
|
+
}
|
|
5790
|
+
judge_que_detail = value.get("judge_que_detail")
|
|
5791
|
+
if isinstance(judge_que_detail, dict):
|
|
5792
|
+
serialized["judgeQueDetail"] = {
|
|
5793
|
+
"queId": judge_que_detail.get("que_id"),
|
|
5794
|
+
"queTitle": judge_que_detail.get("que_title"),
|
|
5795
|
+
"queType": judge_que_detail.get("que_type"),
|
|
5796
|
+
}
|
|
5797
|
+
judge_value_details = value.get("judge_value_details")
|
|
5798
|
+
if isinstance(judge_value_details, list):
|
|
5799
|
+
serialized["judgeValueDetails"] = [
|
|
5800
|
+
{"id": item.get("id"), "value": item.get("value")}
|
|
5801
|
+
for item in judge_value_details
|
|
5802
|
+
if isinstance(item, dict)
|
|
5803
|
+
]
|
|
5804
|
+
filter_condition = value.get("filter_condition")
|
|
5805
|
+
if isinstance(filter_condition, list):
|
|
5806
|
+
serialized["filterCondition"] = [
|
|
5807
|
+
[_serialize_custom_button_match_rule(item) for item in group if isinstance(item, dict)]
|
|
5808
|
+
for group in filter_condition
|
|
5809
|
+
if isinstance(group, list)
|
|
5810
|
+
]
|
|
5811
|
+
return {key: deepcopy(item) for key, item in serialized.items() if item is not None}
|
|
5812
|
+
|
|
5813
|
+
|
|
5814
|
+
def _normalize_custom_button_summary(item: dict[str, Any]) -> dict[str, Any]:
|
|
5815
|
+
normalized = {
|
|
5816
|
+
"button_id": _coerce_positive_int(item.get("button_id") or item.get("buttonId") or item.get("id")),
|
|
5817
|
+
"button_text": str(item.get("button_text") or item.get("buttonText") or "").strip() or None,
|
|
5818
|
+
"button_icon": str(item.get("button_icon") or item.get("buttonIcon") or "").strip() or None,
|
|
5819
|
+
"background_color": str(item.get("background_color") or item.get("backgroundColor") or "").strip() or None,
|
|
5820
|
+
"text_color": str(item.get("text_color") or item.get("textColor") or "").strip() or None,
|
|
5821
|
+
"used_in_chart_count": _coerce_nonnegative_int(item.get("used_in_chart_count") or item.get("userInChartCount")),
|
|
5822
|
+
"being_effective_external_qrobot": bool(item.get("being_effective_external_qrobot") or item.get("beingEffectiveExternalQRobot")),
|
|
5823
|
+
}
|
|
5824
|
+
creator = item.get("creator_user_info") if isinstance(item.get("creator_user_info"), dict) else item.get("creatorUserInfo")
|
|
5825
|
+
if isinstance(creator, dict):
|
|
5826
|
+
normalized["creator_user_info"] = {
|
|
5827
|
+
"uid": creator.get("uid"),
|
|
5828
|
+
"name": creator.get("name"),
|
|
5829
|
+
"email": creator.get("email"),
|
|
5830
|
+
}
|
|
5831
|
+
return normalized
|
|
5832
|
+
|
|
5833
|
+
|
|
5834
|
+
def _normalize_custom_button_detail(item: dict[str, Any]) -> dict[str, Any]:
|
|
5835
|
+
normalized = _normalize_custom_button_summary(item)
|
|
5836
|
+
normalized.update(
|
|
5837
|
+
{
|
|
5838
|
+
"trigger_action": str(item.get("trigger_action") or item.get("triggerAction") or "").strip() or None,
|
|
5839
|
+
"trigger_link_url": str(item.get("trigger_link_url") or item.get("triggerLinkUrl") or "").strip() or None,
|
|
5840
|
+
}
|
|
5841
|
+
)
|
|
5842
|
+
trigger_add_data_config = item.get("trigger_add_data_config")
|
|
5843
|
+
if not isinstance(trigger_add_data_config, dict):
|
|
5844
|
+
trigger_add_data_config = item.get("triggerAddDataConfig")
|
|
5845
|
+
if isinstance(trigger_add_data_config, dict):
|
|
5846
|
+
normalized["trigger_add_data_config"] = deepcopy(trigger_add_data_config)
|
|
5847
|
+
external_qrobot_config = item.get("external_qrobot_config")
|
|
5848
|
+
if not isinstance(external_qrobot_config, dict):
|
|
5849
|
+
external_qrobot_config = item.get("customButtonExternalQRobotRelationVO")
|
|
5850
|
+
if isinstance(external_qrobot_config, dict):
|
|
5851
|
+
normalized["external_qrobot_config"] = deepcopy(external_qrobot_config)
|
|
5852
|
+
trigger_wings_config = item.get("trigger_wings_config")
|
|
5853
|
+
if not isinstance(trigger_wings_config, dict):
|
|
5854
|
+
trigger_wings_config = item.get("triggerWingsConfig")
|
|
5855
|
+
if isinstance(trigger_wings_config, dict):
|
|
5856
|
+
normalized["trigger_wings_config"] = deepcopy(trigger_wings_config)
|
|
5857
|
+
return normalized
|
|
5858
|
+
|
|
5859
|
+
|
|
5005
5860
|
def _failed(
|
|
5006
5861
|
error_code: str,
|
|
5007
5862
|
message: str,
|
|
@@ -7439,6 +8294,36 @@ def _merge_view_summary_with_config(base: dict[str, Any], *, config: dict[str, A
|
|
|
7439
8294
|
for entry in display_entries
|
|
7440
8295
|
if str(entry.get("name") or "").strip()
|
|
7441
8296
|
]
|
|
8297
|
+
apply_entries = [
|
|
8298
|
+
entry
|
|
8299
|
+
for entry in display_entries
|
|
8300
|
+
if _coerce_nonnegative_int(entry.get("field_id")) is not None
|
|
8301
|
+
and str(entry.get("name") or "").strip()
|
|
8302
|
+
and str(entry.get("name") or "").strip() not in _KNOWN_SYSTEM_VIEW_COLUMNS
|
|
8303
|
+
]
|
|
8304
|
+
apply_column_ids = [
|
|
8305
|
+
field_id
|
|
8306
|
+
for field_id in (_coerce_nonnegative_int(entry.get("field_id")) for entry in apply_entries)
|
|
8307
|
+
if field_id is not None
|
|
8308
|
+
]
|
|
8309
|
+
apply_columns = [
|
|
8310
|
+
str(entry.get("name") or "").strip()
|
|
8311
|
+
for entry in apply_entries
|
|
8312
|
+
if str(entry.get("name") or "").strip()
|
|
8313
|
+
]
|
|
8314
|
+
if not apply_columns and configured_column_ids:
|
|
8315
|
+
apply_columns = [
|
|
8316
|
+
str((question_entries_by_id.get(field_id) or {}).get("name") or "").strip()
|
|
8317
|
+
for field_id in configured_column_ids
|
|
8318
|
+
if str((question_entries_by_id.get(field_id) or {}).get("name") or "").strip()
|
|
8319
|
+
and str((question_entries_by_id.get(field_id) or {}).get("name") or "").strip() not in _KNOWN_SYSTEM_VIEW_COLUMNS
|
|
8320
|
+
]
|
|
8321
|
+
apply_column_ids = [
|
|
8322
|
+
field_id
|
|
8323
|
+
for field_id in configured_column_ids
|
|
8324
|
+
if str((question_entries_by_id.get(field_id) or {}).get("name") or "").strip()
|
|
8325
|
+
and str((question_entries_by_id.get(field_id) or {}).get("name") or "").strip() not in _KNOWN_SYSTEM_VIEW_COLUMNS
|
|
8326
|
+
]
|
|
7442
8327
|
if not display_columns and configured_columns:
|
|
7443
8328
|
display_columns = configured_columns
|
|
7444
8329
|
display_column_ids = list(configured_column_ids)
|
|
@@ -7454,6 +8339,10 @@ def _merge_view_summary_with_config(base: dict[str, Any], *, config: dict[str, A
|
|
|
7454
8339
|
summary["configured_columns"] = configured_columns
|
|
7455
8340
|
summary["configured_column_ids"] = configured_column_ids
|
|
7456
8341
|
config_enriched = True
|
|
8342
|
+
if apply_columns:
|
|
8343
|
+
summary["apply_columns"] = apply_columns
|
|
8344
|
+
summary["apply_column_ids"] = apply_column_ids
|
|
8345
|
+
config_enriched = True
|
|
7457
8346
|
if question_entries:
|
|
7458
8347
|
summary["column_details"] = display_entries or _sort_view_question_entries(question_entries)
|
|
7459
8348
|
config_enriched = True
|
|
@@ -7467,6 +8356,18 @@ def _merge_view_summary_with_config(base: dict[str, Any], *, config: dict[str, A
|
|
|
7467
8356
|
if not summary.get("group_by") and display_config.get("group_by"):
|
|
7468
8357
|
summary["group_by"] = display_config.get("group_by")
|
|
7469
8358
|
config_enriched = True
|
|
8359
|
+
button_entries, button_source = _extract_view_button_entries(config)
|
|
8360
|
+
if button_entries:
|
|
8361
|
+
summary["buttons"] = [_normalize_view_button_entry(entry) for entry in button_entries]
|
|
8362
|
+
summary["button_count"] = len(button_entries)
|
|
8363
|
+
if button_source:
|
|
8364
|
+
summary["button_read_source"] = button_source
|
|
8365
|
+
config_enriched = True
|
|
8366
|
+
elif button_source:
|
|
8367
|
+
summary["buttons"] = []
|
|
8368
|
+
summary["button_count"] = 0
|
|
8369
|
+
summary["button_read_source"] = button_source
|
|
8370
|
+
config_enriched = True
|
|
7470
8371
|
if config_enriched:
|
|
7471
8372
|
summary["read_source"] = "view_config"
|
|
7472
8373
|
return summary
|
|
@@ -7580,6 +8481,454 @@ def _extract_view_display_config(
|
|
|
7580
8481
|
return display_config
|
|
7581
8482
|
|
|
7582
8483
|
|
|
8484
|
+
def _normalize_view_button_type(value: Any) -> str | None:
|
|
8485
|
+
normalized = str(value or "").strip().upper()
|
|
8486
|
+
if normalized in {"SYSTEM", "CUSTOM"}:
|
|
8487
|
+
return normalized
|
|
8488
|
+
return None
|
|
8489
|
+
|
|
8490
|
+
|
|
8491
|
+
def _normalize_view_button_config_type(value: Any) -> str | None:
|
|
8492
|
+
normalized = str(value or "").strip().upper()
|
|
8493
|
+
if normalized in {"TOP", "DETAIL"}:
|
|
8494
|
+
return normalized
|
|
8495
|
+
return None
|
|
8496
|
+
|
|
8497
|
+
|
|
8498
|
+
def _normalize_print_tpls(value: Any) -> list[dict[str, Any]]:
|
|
8499
|
+
if not isinstance(value, list):
|
|
8500
|
+
return []
|
|
8501
|
+
items: list[dict[str, Any]] = []
|
|
8502
|
+
for item in value:
|
|
8503
|
+
if isinstance(item, dict):
|
|
8504
|
+
tpl_id = item.get("printTplId")
|
|
8505
|
+
if tpl_id is None:
|
|
8506
|
+
tpl_id = item.get("templateId")
|
|
8507
|
+
if tpl_id is None:
|
|
8508
|
+
tpl_id = item.get("id")
|
|
8509
|
+
tpl_name = item.get("printTplName")
|
|
8510
|
+
if tpl_name is None:
|
|
8511
|
+
tpl_name = item.get("templateName")
|
|
8512
|
+
if tpl_name is None:
|
|
8513
|
+
tpl_name = item.get("name")
|
|
8514
|
+
normalized: dict[str, Any] = {}
|
|
8515
|
+
if tpl_id is not None:
|
|
8516
|
+
normalized["id"] = str(tpl_id)
|
|
8517
|
+
if str(tpl_name or "").strip():
|
|
8518
|
+
normalized["name"] = str(tpl_name).strip()
|
|
8519
|
+
if normalized:
|
|
8520
|
+
items.append(normalized)
|
|
8521
|
+
continue
|
|
8522
|
+
scalar = str(item or "").strip()
|
|
8523
|
+
if scalar:
|
|
8524
|
+
items.append({"id": scalar})
|
|
8525
|
+
return items
|
|
8526
|
+
|
|
8527
|
+
|
|
8528
|
+
def _normalize_print_tpls_for_compare(value: Any) -> list[str]:
|
|
8529
|
+
normalized_items: list[str] = []
|
|
8530
|
+
for item in _normalize_print_tpls(value):
|
|
8531
|
+
item_id = str(item.get("id") or "").strip()
|
|
8532
|
+
item_name = str(item.get("name") or "").strip()
|
|
8533
|
+
normalized_items.append(item_id or item_name)
|
|
8534
|
+
return normalized_items
|
|
8535
|
+
|
|
8536
|
+
|
|
8537
|
+
def _serialize_print_tpl_ids(value: Any) -> list[str]:
|
|
8538
|
+
return [item for item in _normalize_print_tpls_for_compare(value) if item]
|
|
8539
|
+
|
|
8540
|
+
|
|
8541
|
+
def _extract_view_button_entries(config: dict[str, Any]) -> tuple[list[dict[str, Any]], str | None]:
|
|
8542
|
+
if not isinstance(config, dict):
|
|
8543
|
+
return [], None
|
|
8544
|
+
raw_vo = config.get("buttonConfigVO")
|
|
8545
|
+
if isinstance(raw_vo, list):
|
|
8546
|
+
return [deepcopy(item) for item in raw_vo if isinstance(item, dict)], "buttonConfigVO"
|
|
8547
|
+
raw_dtos = config.get("buttonConfigDTOList")
|
|
8548
|
+
if isinstance(raw_dtos, list):
|
|
8549
|
+
return [deepcopy(item) for item in raw_dtos if isinstance(item, dict)], "buttonConfigDTOList"
|
|
8550
|
+
grouped = config.get("buttonConfig")
|
|
8551
|
+
if not isinstance(grouped, dict):
|
|
8552
|
+
return [], None
|
|
8553
|
+
entries: list[dict[str, Any]] = []
|
|
8554
|
+
for item in grouped.get("topButtonList") or []:
|
|
8555
|
+
if not isinstance(item, dict):
|
|
8556
|
+
continue
|
|
8557
|
+
entry = deepcopy(item)
|
|
8558
|
+
entry.setdefault("configType", "TOP")
|
|
8559
|
+
entry.setdefault("beingMain", True)
|
|
8560
|
+
entries.append(entry)
|
|
8561
|
+
for item in grouped.get("mainButtonDetailList") or []:
|
|
8562
|
+
if not isinstance(item, dict):
|
|
8563
|
+
continue
|
|
8564
|
+
entry = deepcopy(item)
|
|
8565
|
+
entry.setdefault("configType", "DETAIL")
|
|
8566
|
+
entry["beingMain"] = True
|
|
8567
|
+
entries.append(entry)
|
|
8568
|
+
for item in grouped.get("moreButtonDetailList") or []:
|
|
8569
|
+
if not isinstance(item, dict):
|
|
8570
|
+
continue
|
|
8571
|
+
entry = deepcopy(item)
|
|
8572
|
+
entry.setdefault("configType", "DETAIL")
|
|
8573
|
+
entry["beingMain"] = False
|
|
8574
|
+
entries.append(entry)
|
|
8575
|
+
return entries, "buttonConfig"
|
|
8576
|
+
|
|
8577
|
+
|
|
8578
|
+
def _normalize_view_button_entry(entry: dict[str, Any]) -> dict[str, Any]:
|
|
8579
|
+
button_id = _coerce_positive_int(entry.get("buttonId") or entry.get("button_id") or entry.get("id"))
|
|
8580
|
+
button_text = str(entry.get("buttonText") or "").strip() or None
|
|
8581
|
+
default_button_text = str(entry.get("defaultButtonText") or "").strip() or None
|
|
8582
|
+
normalized: dict[str, Any] = {
|
|
8583
|
+
"button_type": _normalize_view_button_type(entry.get("buttonType") or entry.get("button_type")),
|
|
8584
|
+
"config_type": _normalize_view_button_config_type(entry.get("configType") or entry.get("config_type")),
|
|
8585
|
+
"button_id": button_id,
|
|
8586
|
+
"button_text": button_text or default_button_text,
|
|
8587
|
+
"being_main": bool(entry.get("beingMain", False)),
|
|
8588
|
+
"trigger_action": str(entry.get("triggerAction") or "").strip() or None,
|
|
8589
|
+
"print_tpls": _normalize_print_tpls(entry.get("printTpls")),
|
|
8590
|
+
"button_formula_type": _coerce_positive_int(entry.get("buttonFormulaType")) or 1,
|
|
8591
|
+
"button_limit": _normalize_view_filter_groups_for_compare(entry.get("buttonLimit")),
|
|
8592
|
+
}
|
|
8593
|
+
for public_key, source_key in (
|
|
8594
|
+
("default_button_text", "defaultButtonText"),
|
|
8595
|
+
("button_icon", "buttonIcon"),
|
|
8596
|
+
("background_color", "backgroundColor"),
|
|
8597
|
+
("text_color", "textColor"),
|
|
8598
|
+
("trigger_link_url", "triggerLinkUrl"),
|
|
8599
|
+
("button_formula", "buttonFormula"),
|
|
8600
|
+
):
|
|
8601
|
+
value = entry.get(source_key)
|
|
8602
|
+
if isinstance(value, str):
|
|
8603
|
+
value = value.strip() or None
|
|
8604
|
+
if value not in {None, ""}:
|
|
8605
|
+
normalized[public_key] = deepcopy(value)
|
|
8606
|
+
trigger_add_data_config = entry.get("triggerAddDataConfig")
|
|
8607
|
+
if isinstance(trigger_add_data_config, dict):
|
|
8608
|
+
normalized["trigger_add_data_config"] = deepcopy(trigger_add_data_config)
|
|
8609
|
+
trigger_wings_config = entry.get("triggerWingsConfig")
|
|
8610
|
+
if isinstance(trigger_wings_config, dict):
|
|
8611
|
+
normalized["trigger_wings_config"] = deepcopy(trigger_wings_config)
|
|
8612
|
+
return normalized
|
|
8613
|
+
|
|
8614
|
+
|
|
8615
|
+
def _normalize_view_buttons_for_compare(value: Any) -> list[dict[str, Any]]:
|
|
8616
|
+
if isinstance(value, dict):
|
|
8617
|
+
entries, _ = _extract_view_button_entries(value)
|
|
8618
|
+
elif isinstance(value, list):
|
|
8619
|
+
entries = [item for item in value if isinstance(item, dict)]
|
|
8620
|
+
else:
|
|
8621
|
+
entries = []
|
|
8622
|
+
normalized_entries: list[dict[str, Any]] = []
|
|
8623
|
+
for entry in entries:
|
|
8624
|
+
normalized = _normalize_view_button_entry(entry)
|
|
8625
|
+
normalized_entries.append(
|
|
8626
|
+
{
|
|
8627
|
+
"button_type": normalized.get("button_type"),
|
|
8628
|
+
"config_type": normalized.get("config_type"),
|
|
8629
|
+
"button_id": normalized.get("button_id") if normalized.get("button_type") == "CUSTOM" else None,
|
|
8630
|
+
"button_text": normalized.get("button_text"),
|
|
8631
|
+
"button_icon": normalized.get("button_icon"),
|
|
8632
|
+
"background_color": normalized.get("background_color"),
|
|
8633
|
+
"text_color": normalized.get("text_color"),
|
|
8634
|
+
"trigger_action": normalized.get("trigger_action"),
|
|
8635
|
+
"trigger_link_url": normalized.get("trigger_link_url"),
|
|
8636
|
+
"being_main": bool(normalized.get("being_main", False)),
|
|
8637
|
+
"print_tpls": _normalize_print_tpls_for_compare(normalized.get("print_tpls")),
|
|
8638
|
+
"button_formula": str(normalized.get("button_formula") or ""),
|
|
8639
|
+
"button_formula_type": _coerce_positive_int(normalized.get("button_formula_type")) or 1,
|
|
8640
|
+
"button_limit": deepcopy(normalized.get("button_limit") or []),
|
|
8641
|
+
}
|
|
8642
|
+
)
|
|
8643
|
+
return normalized_entries
|
|
8644
|
+
|
|
8645
|
+
|
|
8646
|
+
_SYSTEM_VIEW_BUTTON_ID_BY_ACTION: dict[tuple[str, str], int] = {
|
|
8647
|
+
("TOP", "set"): 1,
|
|
8648
|
+
("TOP", "switchView"): 2,
|
|
8649
|
+
("TOP", "setRowHeight"): 3,
|
|
8650
|
+
("TOP", "search"): 6,
|
|
8651
|
+
("DETAIL", "share"): 7,
|
|
8652
|
+
("DETAIL", "edit"): 8,
|
|
8653
|
+
}
|
|
8654
|
+
|
|
8655
|
+
_SYSTEM_VIEW_BUTTON_ID_BY_TEXT: dict[tuple[str, str], int] = {
|
|
8656
|
+
("TOP", "字段管理"): 1,
|
|
8657
|
+
("TOP", "视图类型"): 2,
|
|
8658
|
+
("TOP", "行高"): 3,
|
|
8659
|
+
("TOP", "搜索"): 6,
|
|
8660
|
+
("DETAIL", "分享"): 7,
|
|
8661
|
+
("DETAIL", "修改"): 8,
|
|
8662
|
+
("DETAIL", "修改记录"): 8,
|
|
8663
|
+
}
|
|
8664
|
+
|
|
8665
|
+
|
|
8666
|
+
def _resolve_system_view_button_logical_id(entry: dict[str, Any]) -> int | None:
|
|
8667
|
+
config_type = _normalize_view_button_config_type(entry.get("configType") or entry.get("config_type")) or ""
|
|
8668
|
+
trigger_action = str(entry.get("triggerAction") or entry.get("trigger_action") or "").strip()
|
|
8669
|
+
if config_type and trigger_action:
|
|
8670
|
+
mapped = _SYSTEM_VIEW_BUTTON_ID_BY_ACTION.get((config_type, trigger_action))
|
|
8671
|
+
if mapped is not None:
|
|
8672
|
+
return mapped
|
|
8673
|
+
button_text = str(entry.get("buttonText") or entry.get("button_text") or "").strip()
|
|
8674
|
+
default_button_text = str(entry.get("defaultButtonText") or entry.get("default_button_text") or "").strip()
|
|
8675
|
+
for candidate in (button_text, default_button_text):
|
|
8676
|
+
if config_type and candidate:
|
|
8677
|
+
mapped = _SYSTEM_VIEW_BUTTON_ID_BY_TEXT.get((config_type, candidate))
|
|
8678
|
+
if mapped is not None:
|
|
8679
|
+
return mapped
|
|
8680
|
+
button_id = _coerce_positive_int(entry.get("buttonId") or entry.get("button_id") or entry.get("id"))
|
|
8681
|
+
if button_id is not None and button_id < 1000:
|
|
8682
|
+
return button_id
|
|
8683
|
+
return None
|
|
8684
|
+
|
|
8685
|
+
|
|
8686
|
+
def _normalize_expected_view_buttons_for_compare(
|
|
8687
|
+
value: Any,
|
|
8688
|
+
*,
|
|
8689
|
+
custom_button_details_by_id: dict[int, dict[str, Any]] | None = None,
|
|
8690
|
+
) -> list[dict[str, Any]]:
|
|
8691
|
+
normalized_entries = _normalize_view_buttons_for_compare(value)
|
|
8692
|
+
if not custom_button_details_by_id:
|
|
8693
|
+
return normalized_entries
|
|
8694
|
+
enriched_entries: list[dict[str, Any]] = []
|
|
8695
|
+
for item in normalized_entries:
|
|
8696
|
+
enriched = deepcopy(item)
|
|
8697
|
+
if enriched.get("button_type") == "CUSTOM":
|
|
8698
|
+
button_id = _coerce_positive_int(enriched.get("button_id"))
|
|
8699
|
+
detail = custom_button_details_by_id.get(button_id or -1)
|
|
8700
|
+
if isinstance(detail, dict):
|
|
8701
|
+
for key in (
|
|
8702
|
+
"button_text",
|
|
8703
|
+
"button_icon",
|
|
8704
|
+
"background_color",
|
|
8705
|
+
"text_color",
|
|
8706
|
+
"trigger_action",
|
|
8707
|
+
"trigger_link_url",
|
|
8708
|
+
):
|
|
8709
|
+
value = detail.get(key)
|
|
8710
|
+
if value not in {None, ""}:
|
|
8711
|
+
enriched[key] = deepcopy(value)
|
|
8712
|
+
enriched_entries.append(enriched)
|
|
8713
|
+
return enriched_entries
|
|
8714
|
+
|
|
8715
|
+
|
|
8716
|
+
def _partition_view_button_summaries(
|
|
8717
|
+
items: list[dict[str, Any]],
|
|
8718
|
+
) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]:
|
|
8719
|
+
system_buttons: list[dict[str, Any]] = []
|
|
8720
|
+
custom_buttons: list[dict[str, Any]] = []
|
|
8721
|
+
other_buttons: list[dict[str, Any]] = []
|
|
8722
|
+
for item in items:
|
|
8723
|
+
button_type = str(item.get("button_type") or "").strip().upper()
|
|
8724
|
+
if button_type == "SYSTEM":
|
|
8725
|
+
system_buttons.append(item)
|
|
8726
|
+
elif button_type == "CUSTOM":
|
|
8727
|
+
custom_buttons.append(item)
|
|
8728
|
+
else:
|
|
8729
|
+
other_buttons.append(item)
|
|
8730
|
+
return system_buttons, custom_buttons, other_buttons
|
|
8731
|
+
|
|
8732
|
+
|
|
8733
|
+
def _compare_view_button_summaries(
|
|
8734
|
+
*,
|
|
8735
|
+
expected: list[dict[str, Any]],
|
|
8736
|
+
actual: list[dict[str, Any]],
|
|
8737
|
+
) -> dict[str, Any]:
|
|
8738
|
+
if actual == expected:
|
|
8739
|
+
return {
|
|
8740
|
+
"verified": True,
|
|
8741
|
+
"custom_button_readback_pending": False,
|
|
8742
|
+
"pending_custom_buttons": [],
|
|
8743
|
+
}
|
|
8744
|
+
expected_system, expected_custom, expected_other = _partition_view_button_summaries(expected)
|
|
8745
|
+
actual_system, actual_custom, actual_other = _partition_view_button_summaries(actual)
|
|
8746
|
+
custom_button_readback_pending = (
|
|
8747
|
+
bool(expected_custom)
|
|
8748
|
+
and not actual_custom
|
|
8749
|
+
and actual_system == expected_system
|
|
8750
|
+
and actual_other == expected_other
|
|
8751
|
+
)
|
|
8752
|
+
return {
|
|
8753
|
+
"verified": custom_button_readback_pending,
|
|
8754
|
+
"custom_button_readback_pending": custom_button_readback_pending,
|
|
8755
|
+
"pending_custom_buttons": deepcopy(expected_custom) if custom_button_readback_pending else [],
|
|
8756
|
+
}
|
|
8757
|
+
|
|
8758
|
+
|
|
8759
|
+
def _serialize_existing_view_button_entry(entry: dict[str, Any]) -> dict[str, Any]:
|
|
8760
|
+
dto: dict[str, Any] = {}
|
|
8761
|
+
button_type = _normalize_view_button_type(entry.get("buttonType") or entry.get("button_type"))
|
|
8762
|
+
button_id = _coerce_positive_int(entry.get("buttonId") or entry.get("button_id") or entry.get("id"))
|
|
8763
|
+
if button_type == "SYSTEM":
|
|
8764
|
+
button_id = _resolve_system_view_button_logical_id(entry)
|
|
8765
|
+
if button_id is not None:
|
|
8766
|
+
dto["buttonId"] = button_id
|
|
8767
|
+
if button_type is not None:
|
|
8768
|
+
dto["buttonType"] = button_type
|
|
8769
|
+
config_type = _normalize_view_button_config_type(entry.get("configType") or entry.get("config_type"))
|
|
8770
|
+
if config_type is not None:
|
|
8771
|
+
dto["configType"] = config_type
|
|
8772
|
+
dto["beingMain"] = bool(entry.get("beingMain", False))
|
|
8773
|
+
dto["buttonLimit"] = deepcopy(entry.get("buttonLimit") or [])
|
|
8774
|
+
dto["buttonFormula"] = str(entry.get("buttonFormula") or "")
|
|
8775
|
+
dto["buttonFormulaType"] = _coerce_positive_int(entry.get("buttonFormulaType")) or 1
|
|
8776
|
+
dto["printTpls"] = _serialize_print_tpl_ids(entry.get("printTpls"))
|
|
8777
|
+
for source_key, target_key in (
|
|
8778
|
+
("buttonText", "buttonText"),
|
|
8779
|
+
("defaultButtonText", "defaultButtonText"),
|
|
8780
|
+
("buttonIcon", "buttonIcon"),
|
|
8781
|
+
("backgroundColor", "backgroundColor"),
|
|
8782
|
+
("textColor", "textColor"),
|
|
8783
|
+
("triggerAction", "triggerAction"),
|
|
8784
|
+
("triggerLinkUrl", "triggerLinkUrl"),
|
|
8785
|
+
):
|
|
8786
|
+
if source_key in entry:
|
|
8787
|
+
dto[target_key] = deepcopy(entry.get(source_key))
|
|
8788
|
+
if isinstance(entry.get("triggerAddDataConfig"), dict):
|
|
8789
|
+
dto["triggerAddDataConfig"] = deepcopy(entry.get("triggerAddDataConfig"))
|
|
8790
|
+
if isinstance(entry.get("triggerWingsConfig"), dict):
|
|
8791
|
+
dto["triggerWingsConfig"] = deepcopy(entry.get("triggerWingsConfig"))
|
|
8792
|
+
return dto
|
|
8793
|
+
|
|
8794
|
+
|
|
8795
|
+
def _extract_existing_view_button_dtos(config: dict[str, Any]) -> list[dict[str, Any]]:
|
|
8796
|
+
if not isinstance(config, dict):
|
|
8797
|
+
return []
|
|
8798
|
+
entries, source = _extract_view_button_entries(config)
|
|
8799
|
+
if source == "buttonConfigDTOList":
|
|
8800
|
+
return [deepcopy(item) for item in entries if isinstance(item, dict)]
|
|
8801
|
+
return [_serialize_existing_view_button_entry(entry) for entry in entries if isinstance(entry, dict)]
|
|
8802
|
+
|
|
8803
|
+
|
|
8804
|
+
def _resolve_view_button_dtos_for_patch(
|
|
8805
|
+
*,
|
|
8806
|
+
config: dict[str, Any],
|
|
8807
|
+
patch: ViewUpsertPatch,
|
|
8808
|
+
explicit_button_dtos: list[dict[str, Any]] | None,
|
|
8809
|
+
) -> list[dict[str, Any]] | None:
|
|
8810
|
+
if patch.buttons is None:
|
|
8811
|
+
return _extract_existing_view_button_dtos(config)
|
|
8812
|
+
return deepcopy(explicit_button_dtos or [])
|
|
8813
|
+
|
|
8814
|
+
|
|
8815
|
+
def _build_grouped_view_button_config(button_config_dtos: list[dict[str, Any]]) -> dict[str, list[dict[str, Any]]]:
|
|
8816
|
+
grouped = {"topButtonList": [], "mainButtonDetailList": [], "moreButtonDetailList": []}
|
|
8817
|
+
for raw_item in button_config_dtos:
|
|
8818
|
+
if not isinstance(raw_item, dict):
|
|
8819
|
+
continue
|
|
8820
|
+
item = deepcopy(raw_item)
|
|
8821
|
+
config_type = _normalize_view_button_config_type(item.get("configType"))
|
|
8822
|
+
being_main = bool(item.get("beingMain", False))
|
|
8823
|
+
if config_type == "TOP":
|
|
8824
|
+
grouped["topButtonList"].append(item)
|
|
8825
|
+
elif being_main:
|
|
8826
|
+
grouped["mainButtonDetailList"].append(item)
|
|
8827
|
+
else:
|
|
8828
|
+
grouped["moreButtonDetailList"].append(item)
|
|
8829
|
+
return grouped
|
|
8830
|
+
|
|
8831
|
+
|
|
8832
|
+
def _build_view_button_dtos(
|
|
8833
|
+
*,
|
|
8834
|
+
current_fields_by_name: dict[str, dict[str, Any]],
|
|
8835
|
+
bindings: list[ViewButtonBindingPatch],
|
|
8836
|
+
valid_custom_button_ids: set[int],
|
|
8837
|
+
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
8838
|
+
dtos: list[dict[str, Any]] = []
|
|
8839
|
+
issues: list[dict[str, Any]] = []
|
|
8840
|
+
for binding in bindings:
|
|
8841
|
+
dto, binding_issues = _serialize_view_button_binding(
|
|
8842
|
+
binding=binding,
|
|
8843
|
+
current_fields_by_name=current_fields_by_name,
|
|
8844
|
+
valid_custom_button_ids=valid_custom_button_ids,
|
|
8845
|
+
)
|
|
8846
|
+
if binding_issues:
|
|
8847
|
+
issues.extend(binding_issues)
|
|
8848
|
+
continue
|
|
8849
|
+
dtos.append(dto)
|
|
8850
|
+
return dtos, issues
|
|
8851
|
+
|
|
8852
|
+
|
|
8853
|
+
def _serialize_view_button_binding(
|
|
8854
|
+
*,
|
|
8855
|
+
binding: ViewButtonBindingPatch,
|
|
8856
|
+
current_fields_by_name: dict[str, dict[str, Any]],
|
|
8857
|
+
valid_custom_button_ids: set[int],
|
|
8858
|
+
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
8859
|
+
if binding.button_type == PublicViewButtonType.custom and binding.button_id not in valid_custom_button_ids:
|
|
8860
|
+
return {}, [
|
|
8861
|
+
{
|
|
8862
|
+
"error_code": "UNKNOWN_CUSTOM_BUTTON",
|
|
8863
|
+
"reason_path": "buttons[].button_id",
|
|
8864
|
+
"missing_fields": [],
|
|
8865
|
+
"details": {"button_id": binding.button_id},
|
|
8866
|
+
}
|
|
8867
|
+
]
|
|
8868
|
+
translated_limits, limit_issues = _build_view_button_limit_groups(
|
|
8869
|
+
current_fields_by_name=current_fields_by_name,
|
|
8870
|
+
groups=binding.button_limit,
|
|
8871
|
+
)
|
|
8872
|
+
if limit_issues:
|
|
8873
|
+
return {}, limit_issues
|
|
8874
|
+
dto: dict[str, Any] = {
|
|
8875
|
+
"buttonId": binding.button_id,
|
|
8876
|
+
"buttonType": binding.button_type.value,
|
|
8877
|
+
"configType": binding.config_type.value,
|
|
8878
|
+
"beingMain": bool(binding.being_main),
|
|
8879
|
+
"buttonLimit": translated_limits,
|
|
8880
|
+
"buttonFormula": binding.button_formula or "",
|
|
8881
|
+
"buttonFormulaType": binding.button_formula_type,
|
|
8882
|
+
"printTpls": _serialize_print_tpl_ids(binding.print_tpls),
|
|
8883
|
+
}
|
|
8884
|
+
if binding.button_type == PublicViewButtonType.system:
|
|
8885
|
+
dto["buttonText"] = binding.button_text
|
|
8886
|
+
dto["buttonIcon"] = binding.button_icon
|
|
8887
|
+
dto["backgroundColor"] = binding.background_color
|
|
8888
|
+
dto["textColor"] = binding.text_color
|
|
8889
|
+
dto["triggerAction"] = binding.trigger_action
|
|
8890
|
+
return dto, []
|
|
8891
|
+
|
|
8892
|
+
|
|
8893
|
+
def _build_view_button_limit_groups(
|
|
8894
|
+
*,
|
|
8895
|
+
current_fields_by_name: dict[str, dict[str, Any]],
|
|
8896
|
+
groups: list[list[ViewFilterRulePatch]],
|
|
8897
|
+
) -> tuple[list[list[dict[str, Any]]], list[dict[str, Any]]]:
|
|
8898
|
+
translated_groups: list[list[dict[str, Any]]] = []
|
|
8899
|
+
issues: list[dict[str, Any]] = []
|
|
8900
|
+
for raw_group in groups:
|
|
8901
|
+
if not isinstance(raw_group, list):
|
|
8902
|
+
continue
|
|
8903
|
+
translated_rules: list[dict[str, Any]] = []
|
|
8904
|
+
for raw_rule in raw_group:
|
|
8905
|
+
if hasattr(raw_rule, "model_dump"):
|
|
8906
|
+
raw_rule = raw_rule.model_dump(mode="json")
|
|
8907
|
+
if not isinstance(raw_rule, dict):
|
|
8908
|
+
continue
|
|
8909
|
+
field_name = str(raw_rule.get("field_name") or "").strip()
|
|
8910
|
+
field = current_fields_by_name.get(field_name)
|
|
8911
|
+
if field is None:
|
|
8912
|
+
issues.append(
|
|
8913
|
+
{
|
|
8914
|
+
"error_code": "UNKNOWN_VIEW_FIELD",
|
|
8915
|
+
"missing_fields": [field_name] if field_name else [],
|
|
8916
|
+
"reason_path": "buttons[].button_limit[].field_name",
|
|
8917
|
+
}
|
|
8918
|
+
)
|
|
8919
|
+
continue
|
|
8920
|
+
translated_rule, issue = _translate_view_filter_rule(field=field, rule=raw_rule)
|
|
8921
|
+
if issue:
|
|
8922
|
+
issue = deepcopy(issue)
|
|
8923
|
+
issue["reason_path"] = "buttons[].button_limit[].values"
|
|
8924
|
+
issues.append(issue)
|
|
8925
|
+
continue
|
|
8926
|
+
translated_rules.append(translated_rule)
|
|
8927
|
+
if translated_rules:
|
|
8928
|
+
translated_groups.append(translated_rules)
|
|
8929
|
+
return translated_groups, issues
|
|
8930
|
+
|
|
8931
|
+
|
|
7583
8932
|
def _summarize_charts(result: Any) -> list[dict[str, Any]]:
|
|
7584
8933
|
if not isinstance(result, list):
|
|
7585
8934
|
return []
|
|
@@ -7929,6 +9278,8 @@ def _build_view_create_payload(
|
|
|
7929
9278
|
patch: ViewUpsertPatch,
|
|
7930
9279
|
ordinal: int,
|
|
7931
9280
|
view_filters: list[list[dict[str, Any]]],
|
|
9281
|
+
current_fields_by_name: dict[str, dict[str, Any]],
|
|
9282
|
+
explicit_button_dtos: list[dict[str, Any]] | None = None,
|
|
7932
9283
|
) -> JSONObject:
|
|
7933
9284
|
entity = _entity_spec_from_app(base_info=base_info, schema=schema, views=None)
|
|
7934
9285
|
parsed_schema = _parse_schema(schema)
|
|
@@ -7969,6 +9320,7 @@ def _build_view_create_payload(
|
|
|
7969
9320
|
group_que_id=field_map.get(patch.group_by or "") if patch.group_by else None,
|
|
7970
9321
|
view_filters=view_filters,
|
|
7971
9322
|
gantt_payload=gantt_config,
|
|
9323
|
+
button_config_dtos=explicit_button_dtos,
|
|
7972
9324
|
)
|
|
7973
9325
|
|
|
7974
9326
|
|
|
@@ -8111,6 +9463,8 @@ def _build_view_update_payload(
|
|
|
8111
9463
|
schema: dict[str, Any],
|
|
8112
9464
|
patch: ViewUpsertPatch,
|
|
8113
9465
|
view_filters: list[list[dict[str, Any]]],
|
|
9466
|
+
current_fields_by_name: dict[str, dict[str, Any]],
|
|
9467
|
+
explicit_button_dtos: list[dict[str, Any]] | None = None,
|
|
8114
9468
|
) -> JSONObject:
|
|
8115
9469
|
config_response = views.view_get_config(profile=profile, viewgraph_key=source_viewgraph_key)
|
|
8116
9470
|
config = config_response.get("result") if isinstance(config_response.get("result"), dict) else {}
|
|
@@ -8149,7 +9503,11 @@ def _build_view_update_payload(
|
|
|
8149
9503
|
payload.setdefault("defaultRowHigh", "compact")
|
|
8150
9504
|
payload.setdefault("viewgraphLimitType", 1)
|
|
8151
9505
|
payload.setdefault("viewgraphLimit", deepcopy(view_filters) if view_filters else [])
|
|
8152
|
-
|
|
9506
|
+
button_config_dtos = _resolve_view_button_dtos_for_patch(
|
|
9507
|
+
config=config,
|
|
9508
|
+
patch=patch,
|
|
9509
|
+
explicit_button_dtos=explicit_button_dtos,
|
|
9510
|
+
)
|
|
8153
9511
|
|
|
8154
9512
|
normalized_type = patch.type.value
|
|
8155
9513
|
existing_type = _normalize_view_type_name(payload.get("viewgraphType") or payload.get("type"))
|
|
@@ -8180,9 +9538,26 @@ def _build_view_update_payload(
|
|
|
8180
9538
|
group_que_id=field_map.get(patch.group_by or "") if patch.group_by else None,
|
|
8181
9539
|
view_filters=view_filters,
|
|
8182
9540
|
gantt_payload=gantt_payload,
|
|
9541
|
+
button_config_dtos=button_config_dtos,
|
|
8183
9542
|
)
|
|
8184
9543
|
|
|
8185
9544
|
|
|
9545
|
+
_KNOWN_SYSTEM_VIEW_COLUMNS = {
|
|
9546
|
+
"编号",
|
|
9547
|
+
"当前流程状态",
|
|
9548
|
+
"申请人",
|
|
9549
|
+
"申请时间",
|
|
9550
|
+
"更新时间",
|
|
9551
|
+
"流程标题",
|
|
9552
|
+
"当前处理人",
|
|
9553
|
+
"当前处理节点",
|
|
9554
|
+
}
|
|
9555
|
+
|
|
9556
|
+
|
|
9557
|
+
def _filter_known_system_view_columns(columns: list[str]) -> list[str]:
|
|
9558
|
+
return [name for name in columns if str(name or "").strip() and str(name).strip() not in _KNOWN_SYSTEM_VIEW_COLUMNS]
|
|
9559
|
+
|
|
9560
|
+
|
|
8186
9561
|
def _build_minimal_view_payload(
|
|
8187
9562
|
*,
|
|
8188
9563
|
app_key: str,
|
|
@@ -8190,6 +9565,8 @@ def _build_minimal_view_payload(
|
|
|
8190
9565
|
patch: ViewUpsertPatch,
|
|
8191
9566
|
ordinal: int,
|
|
8192
9567
|
view_filters: list[list[dict[str, Any]]],
|
|
9568
|
+
current_fields_by_name: dict[str, dict[str, Any]],
|
|
9569
|
+
explicit_button_dtos: list[dict[str, Any]] | None = None,
|
|
8193
9570
|
) -> JSONObject:
|
|
8194
9571
|
field_map = extract_field_map(schema)
|
|
8195
9572
|
parsed_schema = _parse_schema(schema)
|
|
@@ -8222,6 +9599,7 @@ def _build_minimal_view_payload(
|
|
|
8222
9599
|
group_que_id=field_map.get(patch.group_by or "") if patch.group_by else None,
|
|
8223
9600
|
view_filters=view_filters,
|
|
8224
9601
|
gantt_payload=gantt_payload,
|
|
9602
|
+
button_config_dtos=explicit_button_dtos,
|
|
8225
9603
|
)
|
|
8226
9604
|
|
|
8227
9605
|
|
|
@@ -8233,6 +9611,7 @@ def _hydrate_view_backend_payload(
|
|
|
8233
9611
|
group_que_id: int | None,
|
|
8234
9612
|
view_filters: list[list[dict[str, Any]]] | None = None,
|
|
8235
9613
|
gantt_payload: dict[str, Any] | None = None,
|
|
9614
|
+
button_config_dtos: list[dict[str, Any]] | None = None,
|
|
8236
9615
|
) -> JSONObject:
|
|
8237
9616
|
data = deepcopy(payload)
|
|
8238
9617
|
data.setdefault("beingPinNavigate", True)
|
|
@@ -8268,9 +9647,11 @@ def _hydrate_view_backend_payload(
|
|
|
8268
9647
|
data.setdefault("asosChartConfig", {"limitType": 1, "asosChartIdList": []})
|
|
8269
9648
|
data.setdefault("viewgraphGanttConfigVO", None)
|
|
8270
9649
|
data.setdefault("viewgraphHierarchyConfigVO", None)
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
data["buttonConfig"] =
|
|
9650
|
+
if button_config_dtos is not None:
|
|
9651
|
+
data["buttonConfigDTOList"] = deepcopy(button_config_dtos)
|
|
9652
|
+
data["buttonConfig"] = _build_grouped_view_button_config(button_config_dtos)
|
|
9653
|
+
else:
|
|
9654
|
+
data.pop("buttonConfigVO", None)
|
|
8274
9655
|
if view_type == "table":
|
|
8275
9656
|
data["viewgraphType"] = "tableView"
|
|
8276
9657
|
data["beingShowTitleQue"] = False
|
|
@@ -8305,7 +9686,7 @@ def _resolve_view_visible_field_names(patch: ViewUpsertPatch) -> list[str]:
|
|
|
8305
9686
|
ordered: list[str] = []
|
|
8306
9687
|
for value in [*patch.columns, patch.title_field, patch.start_field, patch.end_field, patch.group_by]:
|
|
8307
9688
|
name = str(value or "").strip()
|
|
8308
|
-
if name and name not in ordered:
|
|
9689
|
+
if name and name not in _KNOWN_SYSTEM_VIEW_COLUMNS and name not in ordered:
|
|
8309
9690
|
ordered.append(name)
|
|
8310
9691
|
return ordered
|
|
8311
9692
|
|