@josephyan/qingflow-cli 0.2.0-beta.61 → 0.2.0-beta.63
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 +1024 -30
- package/src/qingflow_mcp/cli/commands/builder.py +58 -1
- package/src/qingflow_mcp/cli/commands/task.py +3 -1
- package/src/qingflow_mcp/cli/formatters.py +36 -0
- 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/approval_tools.py +147 -12
- package/src/qingflow_mcp/tools/custom_button_tools.py +177 -0
- package/src/qingflow_mcp/tools/import_tools.py +217 -90
- package/src/qingflow_mcp/tools/task_context_tools.py +621 -29
|
@@ -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,333 @@ 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
|
+
create_result = self.buttons.custom_button_create(
|
|
1410
|
+
profile=profile,
|
|
1411
|
+
app_key=app_key,
|
|
1412
|
+
payload=_serialize_custom_button_payload(payload),
|
|
1413
|
+
)
|
|
1414
|
+
raw_result = create_result.get("result")
|
|
1415
|
+
button_id = _extract_custom_button_id(raw_result)
|
|
1416
|
+
if button_id is None:
|
|
1417
|
+
return finalize(
|
|
1418
|
+
_failed(
|
|
1419
|
+
"CUSTOM_BUTTON_CREATE_FAILED",
|
|
1420
|
+
"custom button create succeeded but no button_id was returned",
|
|
1421
|
+
normalized_args=normalized_args,
|
|
1422
|
+
details={"app_key": app_key, "result": deepcopy(raw_result)},
|
|
1423
|
+
suggested_next_call={"tool_name": "app_custom_button_list", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1424
|
+
)
|
|
1425
|
+
)
|
|
1426
|
+
try:
|
|
1427
|
+
readback = self.buttons.custom_button_get(
|
|
1428
|
+
profile=profile,
|
|
1429
|
+
app_key=app_key,
|
|
1430
|
+
button_id=button_id,
|
|
1431
|
+
being_draft=True,
|
|
1432
|
+
include_raw=False,
|
|
1433
|
+
)
|
|
1434
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1435
|
+
api_error = _coerce_api_error(error)
|
|
1436
|
+
response = {
|
|
1437
|
+
"status": "partial_success",
|
|
1438
|
+
"error_code": "CUSTOM_BUTTON_READBACK_PENDING",
|
|
1439
|
+
"recoverable": True,
|
|
1440
|
+
"message": "created custom button; detail readback unavailable",
|
|
1441
|
+
"normalized_args": normalized_args,
|
|
1442
|
+
"missing_fields": [],
|
|
1443
|
+
"allowed_values": {},
|
|
1444
|
+
"details": {
|
|
1445
|
+
"app_key": app_key,
|
|
1446
|
+
"button_id": button_id,
|
|
1447
|
+
"transport_error": _transport_error_payload(api_error),
|
|
1448
|
+
},
|
|
1449
|
+
"request_id": api_error.request_id,
|
|
1450
|
+
"suggested_next_call": {
|
|
1451
|
+
"tool_name": "app_custom_button_get",
|
|
1452
|
+
"arguments": {"profile": profile, "app_key": app_key, "button_id": button_id},
|
|
1453
|
+
},
|
|
1454
|
+
"noop": False,
|
|
1455
|
+
"warnings": [_warning("CUSTOM_BUTTON_READBACK_PENDING", "custom button was created, but detail readback is not available")],
|
|
1456
|
+
"verification": {"custom_button_verified": False},
|
|
1457
|
+
"verified": False,
|
|
1458
|
+
"app_key": app_key,
|
|
1459
|
+
"button_id": button_id,
|
|
1460
|
+
}
|
|
1461
|
+
if _is_permission_restricted_api_error(api_error):
|
|
1462
|
+
response = _apply_permission_outcomes(
|
|
1463
|
+
response,
|
|
1464
|
+
_verification_read_outcome(
|
|
1465
|
+
resource="custom_button",
|
|
1466
|
+
target={"app_key": app_key, "button_id": button_id},
|
|
1467
|
+
transport_error=api_error,
|
|
1468
|
+
),
|
|
1469
|
+
)
|
|
1470
|
+
return finalize(response)
|
|
1471
|
+
button = _normalize_custom_button_detail(readback.get("result") if isinstance(readback.get("result"), dict) else {})
|
|
1472
|
+
return finalize(
|
|
1473
|
+
{
|
|
1474
|
+
"status": "success",
|
|
1475
|
+
"error_code": None,
|
|
1476
|
+
"recoverable": False,
|
|
1477
|
+
"message": "created custom button",
|
|
1478
|
+
"normalized_args": normalized_args,
|
|
1479
|
+
"missing_fields": [],
|
|
1480
|
+
"allowed_values": {},
|
|
1481
|
+
"details": {},
|
|
1482
|
+
"request_id": None,
|
|
1483
|
+
"suggested_next_call": None,
|
|
1484
|
+
"noop": False,
|
|
1485
|
+
"warnings": [],
|
|
1486
|
+
"verification": {"custom_button_verified": True},
|
|
1487
|
+
"verified": True,
|
|
1488
|
+
"app_key": app_key,
|
|
1489
|
+
"button_id": button_id,
|
|
1490
|
+
"button": button,
|
|
1491
|
+
}
|
|
1492
|
+
)
|
|
1493
|
+
|
|
1494
|
+
def app_custom_button_update(
|
|
1495
|
+
self,
|
|
1496
|
+
*,
|
|
1497
|
+
profile: str,
|
|
1498
|
+
app_key: str,
|
|
1499
|
+
button_id: int,
|
|
1500
|
+
payload: CustomButtonPatch,
|
|
1501
|
+
) -> JSONObject:
|
|
1502
|
+
normalized_args = {"app_key": app_key, "button_id": button_id, "payload": payload.model_dump(mode="json")}
|
|
1503
|
+
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
1504
|
+
permission_outcome = self._guard_app_permission(
|
|
1505
|
+
profile=profile,
|
|
1506
|
+
app_key=app_key,
|
|
1507
|
+
required_permission="edit_app",
|
|
1508
|
+
normalized_args=normalized_args,
|
|
1509
|
+
)
|
|
1510
|
+
if permission_outcome.block is not None:
|
|
1511
|
+
return permission_outcome.block
|
|
1512
|
+
permission_outcomes.append(permission_outcome)
|
|
1513
|
+
|
|
1514
|
+
def finalize(response: JSONObject) -> JSONObject:
|
|
1515
|
+
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
1516
|
+
|
|
1517
|
+
self.buttons.custom_button_update(
|
|
1518
|
+
profile=profile,
|
|
1519
|
+
app_key=app_key,
|
|
1520
|
+
button_id=button_id,
|
|
1521
|
+
payload=_serialize_custom_button_payload(payload),
|
|
1522
|
+
)
|
|
1523
|
+
try:
|
|
1524
|
+
readback = self.buttons.custom_button_get(
|
|
1525
|
+
profile=profile,
|
|
1526
|
+
app_key=app_key,
|
|
1527
|
+
button_id=button_id,
|
|
1528
|
+
being_draft=True,
|
|
1529
|
+
include_raw=False,
|
|
1530
|
+
)
|
|
1531
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1532
|
+
api_error = _coerce_api_error(error)
|
|
1533
|
+
response = {
|
|
1534
|
+
"status": "partial_success",
|
|
1535
|
+
"error_code": "CUSTOM_BUTTON_READBACK_PENDING",
|
|
1536
|
+
"recoverable": True,
|
|
1537
|
+
"message": "updated custom button; detail readback unavailable",
|
|
1538
|
+
"normalized_args": normalized_args,
|
|
1539
|
+
"missing_fields": [],
|
|
1540
|
+
"allowed_values": {},
|
|
1541
|
+
"details": {
|
|
1542
|
+
"app_key": app_key,
|
|
1543
|
+
"button_id": button_id,
|
|
1544
|
+
"transport_error": _transport_error_payload(api_error),
|
|
1545
|
+
},
|
|
1546
|
+
"request_id": api_error.request_id,
|
|
1547
|
+
"suggested_next_call": {
|
|
1548
|
+
"tool_name": "app_custom_button_get",
|
|
1549
|
+
"arguments": {"profile": profile, "app_key": app_key, "button_id": button_id},
|
|
1550
|
+
},
|
|
1551
|
+
"noop": False,
|
|
1552
|
+
"warnings": [_warning("CUSTOM_BUTTON_READBACK_PENDING", "custom button was updated, but detail readback is not available")],
|
|
1553
|
+
"verification": {"custom_button_verified": False},
|
|
1554
|
+
"verified": False,
|
|
1555
|
+
"app_key": app_key,
|
|
1556
|
+
"button_id": button_id,
|
|
1557
|
+
}
|
|
1558
|
+
if _is_permission_restricted_api_error(api_error):
|
|
1559
|
+
response = _apply_permission_outcomes(
|
|
1560
|
+
response,
|
|
1561
|
+
_verification_read_outcome(
|
|
1562
|
+
resource="custom_button",
|
|
1563
|
+
target={"app_key": app_key, "button_id": button_id},
|
|
1564
|
+
transport_error=api_error,
|
|
1565
|
+
),
|
|
1566
|
+
)
|
|
1567
|
+
return finalize(response)
|
|
1568
|
+
button = _normalize_custom_button_detail(readback.get("result") if isinstance(readback.get("result"), dict) else {})
|
|
1569
|
+
return finalize(
|
|
1570
|
+
{
|
|
1571
|
+
"status": "success",
|
|
1572
|
+
"error_code": None,
|
|
1573
|
+
"recoverable": False,
|
|
1574
|
+
"message": "updated custom button",
|
|
1575
|
+
"normalized_args": normalized_args,
|
|
1576
|
+
"missing_fields": [],
|
|
1577
|
+
"allowed_values": {},
|
|
1578
|
+
"details": {},
|
|
1579
|
+
"request_id": None,
|
|
1580
|
+
"suggested_next_call": None,
|
|
1581
|
+
"noop": False,
|
|
1582
|
+
"warnings": [],
|
|
1583
|
+
"verification": {"custom_button_verified": True},
|
|
1584
|
+
"verified": True,
|
|
1585
|
+
"app_key": app_key,
|
|
1586
|
+
"button_id": button_id,
|
|
1587
|
+
"button": button,
|
|
1588
|
+
}
|
|
1589
|
+
)
|
|
1590
|
+
|
|
1591
|
+
def app_custom_button_delete(self, *, profile: str, app_key: str, button_id: int) -> JSONObject:
|
|
1592
|
+
normalized_args = {"app_key": app_key, "button_id": button_id}
|
|
1593
|
+
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
1594
|
+
permission_outcome = self._guard_app_permission(
|
|
1595
|
+
profile=profile,
|
|
1596
|
+
app_key=app_key,
|
|
1597
|
+
required_permission="edit_app",
|
|
1598
|
+
normalized_args=normalized_args,
|
|
1599
|
+
)
|
|
1600
|
+
if permission_outcome.block is not None:
|
|
1601
|
+
return permission_outcome.block
|
|
1602
|
+
permission_outcomes.append(permission_outcome)
|
|
1603
|
+
|
|
1604
|
+
def finalize(response: JSONObject) -> JSONObject:
|
|
1605
|
+
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
1606
|
+
|
|
1607
|
+
self.buttons.custom_button_delete(profile=profile, app_key=app_key, button_id=button_id)
|
|
1608
|
+
return finalize(
|
|
1609
|
+
{
|
|
1610
|
+
"status": "success",
|
|
1611
|
+
"error_code": None,
|
|
1612
|
+
"recoverable": False,
|
|
1613
|
+
"message": "deleted custom button",
|
|
1614
|
+
"normalized_args": normalized_args,
|
|
1615
|
+
"missing_fields": [],
|
|
1616
|
+
"allowed_values": {},
|
|
1617
|
+
"details": {},
|
|
1618
|
+
"request_id": None,
|
|
1619
|
+
"suggested_next_call": None,
|
|
1620
|
+
"noop": False,
|
|
1621
|
+
"warnings": [],
|
|
1622
|
+
"verification": {"custom_button_deleted": True},
|
|
1623
|
+
"verified": True,
|
|
1624
|
+
"app_key": app_key,
|
|
1625
|
+
"button_id": button_id,
|
|
1626
|
+
"deleted": True,
|
|
1627
|
+
}
|
|
1628
|
+
)
|
|
1629
|
+
|
|
1294
1630
|
def _resolve_app_matches_in_visible_apps(
|
|
1295
1631
|
self,
|
|
1296
1632
|
*,
|
|
@@ -3303,6 +3639,35 @@ class AiBuilderFacade:
|
|
|
3303
3639
|
for field in parsed_schema["fields"]
|
|
3304
3640
|
if isinstance(field, dict) and str(field.get("name") or "")
|
|
3305
3641
|
}
|
|
3642
|
+
requires_custom_button_validation = any(
|
|
3643
|
+
any(binding.button_type == PublicViewButtonType.custom for binding in (patch.buttons or []))
|
|
3644
|
+
for patch in upsert_views
|
|
3645
|
+
)
|
|
3646
|
+
valid_custom_button_ids: set[int] = set()
|
|
3647
|
+
if requires_custom_button_validation:
|
|
3648
|
+
try:
|
|
3649
|
+
button_listing = self.buttons.custom_button_list(
|
|
3650
|
+
profile=profile,
|
|
3651
|
+
app_key=app_key,
|
|
3652
|
+
being_draft=True,
|
|
3653
|
+
include_raw=False,
|
|
3654
|
+
)
|
|
3655
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
3656
|
+
api_error = _coerce_api_error(error)
|
|
3657
|
+
return finalize(
|
|
3658
|
+
_failed_from_api_error(
|
|
3659
|
+
"CUSTOM_BUTTON_LIST_FAILED",
|
|
3660
|
+
api_error,
|
|
3661
|
+
normalized_args=normalized_args,
|
|
3662
|
+
details=_with_state_read_blocked_details({"app_key": app_key}, resource="custom_button", error=api_error),
|
|
3663
|
+
suggested_next_call={"tool_name": "app_custom_button_list", "arguments": {"profile": profile, "app_key": app_key}},
|
|
3664
|
+
)
|
|
3665
|
+
)
|
|
3666
|
+
valid_custom_button_ids = {
|
|
3667
|
+
button_id
|
|
3668
|
+
for item in (button_listing.get("items") or [])
|
|
3669
|
+
if isinstance(item, dict) and (button_id := _coerce_positive_int(item.get("button_id"))) is not None
|
|
3670
|
+
}
|
|
3306
3671
|
removed: list[str] = []
|
|
3307
3672
|
view_results: list[dict[str, Any]] = []
|
|
3308
3673
|
for name in remove_views:
|
|
@@ -3396,6 +3761,30 @@ class AiBuilderFacade:
|
|
|
3396
3761
|
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
3762
|
suggested_next_call={"tool_name": "app_read_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
3398
3763
|
)
|
|
3764
|
+
explicit_button_dtos: list[dict[str, Any]] | None = None
|
|
3765
|
+
expected_button_summary: list[dict[str, Any]] | None = None
|
|
3766
|
+
if patch.buttons is not None:
|
|
3767
|
+
explicit_button_dtos, button_issues = _build_view_button_dtos(
|
|
3768
|
+
current_fields_by_name=current_fields_by_name,
|
|
3769
|
+
bindings=patch.buttons,
|
|
3770
|
+
valid_custom_button_ids=valid_custom_button_ids,
|
|
3771
|
+
)
|
|
3772
|
+
if button_issues:
|
|
3773
|
+
first_issue = button_issues[0]
|
|
3774
|
+
return _failed(
|
|
3775
|
+
str(first_issue.get("error_code") or "INVALID_VIEW_BUTTON"),
|
|
3776
|
+
"view buttons reference invalid fields, values, or custom buttons",
|
|
3777
|
+
normalized_args=normalized_args,
|
|
3778
|
+
details={
|
|
3779
|
+
"app_key": app_key,
|
|
3780
|
+
"view_name": patch.name,
|
|
3781
|
+
**first_issue,
|
|
3782
|
+
},
|
|
3783
|
+
missing_fields=list(first_issue.get("missing_fields") or []),
|
|
3784
|
+
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]},
|
|
3785
|
+
suggested_next_call={"tool_name": "app_custom_button_list", "arguments": {"profile": profile, "app_key": app_key}},
|
|
3786
|
+
)
|
|
3787
|
+
expected_button_summary = _normalize_view_buttons_for_compare(explicit_button_dtos or [])
|
|
3399
3788
|
matched_existing_view: dict[str, Any] | None = None
|
|
3400
3789
|
existing_key: str | None = None
|
|
3401
3790
|
if patch.view_key:
|
|
@@ -3441,6 +3830,8 @@ class AiBuilderFacade:
|
|
|
3441
3830
|
schema=schema,
|
|
3442
3831
|
patch=patch,
|
|
3443
3832
|
view_filters=translated_filters,
|
|
3833
|
+
current_fields_by_name=current_fields_by_name,
|
|
3834
|
+
explicit_button_dtos=explicit_button_dtos,
|
|
3444
3835
|
)
|
|
3445
3836
|
self.views.view_update(profile=profile, viewgraph_key=existing_key, payload=payload)
|
|
3446
3837
|
system_view_sync: dict[str, Any] | None = None
|
|
@@ -3481,14 +3872,15 @@ class AiBuilderFacade:
|
|
|
3481
3872
|
updated.append(patch.name)
|
|
3482
3873
|
view_results.append(
|
|
3483
3874
|
{
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3875
|
+
"name": patch.name,
|
|
3876
|
+
"view_key": existing_key,
|
|
3877
|
+
"type": patch.type.value,
|
|
3878
|
+
"status": "updated",
|
|
3879
|
+
"expected_filters": deepcopy(translated_filters),
|
|
3880
|
+
"expected_buttons": deepcopy(expected_button_summary),
|
|
3881
|
+
"system_view_sync": system_view_sync,
|
|
3882
|
+
}
|
|
3883
|
+
)
|
|
3492
3884
|
else:
|
|
3493
3885
|
template_key = _pick_view_template_key(existing_view_list, desired_type=patch.type.value)
|
|
3494
3886
|
should_copy_template = patch.type.value == "table" and template_key and not translated_filters
|
|
@@ -3502,6 +3894,8 @@ class AiBuilderFacade:
|
|
|
3502
3894
|
schema=schema,
|
|
3503
3895
|
patch=patch,
|
|
3504
3896
|
view_filters=translated_filters,
|
|
3897
|
+
current_fields_by_name=current_fields_by_name,
|
|
3898
|
+
explicit_button_dtos=explicit_button_dtos,
|
|
3505
3899
|
)
|
|
3506
3900
|
self.views.view_update(profile=profile, viewgraph_key=created_key, payload=payload)
|
|
3507
3901
|
else:
|
|
@@ -3512,6 +3906,8 @@ class AiBuilderFacade:
|
|
|
3512
3906
|
patch=patch,
|
|
3513
3907
|
ordinal=ordinal,
|
|
3514
3908
|
view_filters=translated_filters,
|
|
3909
|
+
current_fields_by_name=current_fields_by_name,
|
|
3910
|
+
explicit_button_dtos=explicit_button_dtos,
|
|
3515
3911
|
)
|
|
3516
3912
|
create_result = self.views.view_create(profile=profile, payload=payload)
|
|
3517
3913
|
raw_created = create_result.get("result")
|
|
@@ -3522,14 +3918,15 @@ class AiBuilderFacade:
|
|
|
3522
3918
|
created_key = raw_created.strip() or None
|
|
3523
3919
|
created.append(patch.name)
|
|
3524
3920
|
view_results.append(
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3921
|
+
{
|
|
3922
|
+
"name": patch.name,
|
|
3923
|
+
"view_key": created_key,
|
|
3924
|
+
"type": patch.type.value,
|
|
3925
|
+
"status": "created",
|
|
3926
|
+
"expected_filters": deepcopy(translated_filters),
|
|
3927
|
+
"expected_buttons": deepcopy(expected_button_summary),
|
|
3928
|
+
}
|
|
3929
|
+
)
|
|
3533
3930
|
except (QingflowApiError, RuntimeError) as error:
|
|
3534
3931
|
api_error = _coerce_api_error(error)
|
|
3535
3932
|
should_retry_minimal = operation_phase != "default_view_apply_config_sync" and (
|
|
@@ -3540,12 +3937,23 @@ class AiBuilderFacade:
|
|
|
3540
3937
|
try:
|
|
3541
3938
|
if existing_key or created_key:
|
|
3542
3939
|
target_key = created_key or existing_key or ""
|
|
3940
|
+
fallback_button_dtos = explicit_button_dtos
|
|
3941
|
+
if fallback_button_dtos is None:
|
|
3942
|
+
fallback_config_response = self.views.view_get_config(profile=profile, viewgraph_key=target_key)
|
|
3943
|
+
fallback_config = (
|
|
3944
|
+
fallback_config_response.get("result")
|
|
3945
|
+
if isinstance(fallback_config_response.get("result"), dict)
|
|
3946
|
+
else {}
|
|
3947
|
+
)
|
|
3948
|
+
fallback_button_dtos = _extract_existing_view_button_dtos(fallback_config)
|
|
3543
3949
|
fallback_payload = _build_minimal_view_payload(
|
|
3544
3950
|
app_key=app_key,
|
|
3545
3951
|
schema=schema,
|
|
3546
3952
|
patch=patch,
|
|
3547
3953
|
ordinal=ordinal,
|
|
3548
3954
|
view_filters=translated_filters,
|
|
3955
|
+
current_fields_by_name=current_fields_by_name,
|
|
3956
|
+
explicit_button_dtos=fallback_button_dtos,
|
|
3549
3957
|
)
|
|
3550
3958
|
self.views.view_update(profile=profile, viewgraph_key=target_key, payload=fallback_payload)
|
|
3551
3959
|
if existing_key:
|
|
@@ -3558,6 +3966,7 @@ class AiBuilderFacade:
|
|
|
3558
3966
|
"status": "updated",
|
|
3559
3967
|
"fallback_applied": True,
|
|
3560
3968
|
"expected_filters": deepcopy(translated_filters),
|
|
3969
|
+
"expected_buttons": deepcopy(expected_button_summary),
|
|
3561
3970
|
}
|
|
3562
3971
|
)
|
|
3563
3972
|
else:
|
|
@@ -3570,6 +3979,7 @@ class AiBuilderFacade:
|
|
|
3570
3979
|
"status": "created",
|
|
3571
3980
|
"fallback_applied": True,
|
|
3572
3981
|
"expected_filters": deepcopy(translated_filters),
|
|
3982
|
+
"expected_buttons": deepcopy(expected_button_summary),
|
|
3573
3983
|
}
|
|
3574
3984
|
)
|
|
3575
3985
|
continue
|
|
@@ -3579,6 +3989,8 @@ class AiBuilderFacade:
|
|
|
3579
3989
|
patch=patch,
|
|
3580
3990
|
ordinal=ordinal,
|
|
3581
3991
|
view_filters=translated_filters,
|
|
3992
|
+
current_fields_by_name=current_fields_by_name,
|
|
3993
|
+
explicit_button_dtos=explicit_button_dtos,
|
|
3582
3994
|
)
|
|
3583
3995
|
self.views.view_create(profile=profile, payload=fallback_payload)
|
|
3584
3996
|
created.append(patch.name)
|
|
@@ -3665,6 +4077,8 @@ class AiBuilderFacade:
|
|
|
3665
4077
|
verification_by_view: list[dict[str, Any]] = []
|
|
3666
4078
|
filter_readback_pending = False
|
|
3667
4079
|
filter_mismatches: list[dict[str, Any]] = []
|
|
4080
|
+
button_readback_pending = False
|
|
4081
|
+
button_mismatches: list[dict[str, Any]] = []
|
|
3668
4082
|
for item in view_results:
|
|
3669
4083
|
status = str(item.get("status") or "")
|
|
3670
4084
|
name = str(item.get("name") or "")
|
|
@@ -3688,6 +4102,7 @@ class AiBuilderFacade:
|
|
|
3688
4102
|
if isinstance(system_view_sync, dict):
|
|
3689
4103
|
verification_entry["system_view_sync"] = deepcopy(system_view_sync)
|
|
3690
4104
|
expected_filters = item.get("expected_filters") or []
|
|
4105
|
+
expected_buttons = item.get("expected_buttons") if isinstance(item.get("expected_buttons"), list) else None
|
|
3691
4106
|
if expected_filters:
|
|
3692
4107
|
if verified_views_unavailable or not present_in_readback:
|
|
3693
4108
|
verification_entry["filters_verified"] = None
|
|
@@ -3743,6 +4158,54 @@ class AiBuilderFacade:
|
|
|
3743
4158
|
"category": api_error.category,
|
|
3744
4159
|
}
|
|
3745
4160
|
filter_readback_pending = True
|
|
4161
|
+
if expected_buttons is not None:
|
|
4162
|
+
if verified_views_unavailable or not present_in_readback:
|
|
4163
|
+
verification_entry["buttons_verified"] = None
|
|
4164
|
+
verification_entry["button_readback_pending"] = True
|
|
4165
|
+
button_readback_pending = True
|
|
4166
|
+
else:
|
|
4167
|
+
verification_key = item_view_key
|
|
4168
|
+
if not verification_key:
|
|
4169
|
+
matched_keys = verified_view_keys_by_name.get(name) or []
|
|
4170
|
+
if len(matched_keys) == 1:
|
|
4171
|
+
verification_key = matched_keys[0]
|
|
4172
|
+
else:
|
|
4173
|
+
verification_entry["buttons_verified"] = None
|
|
4174
|
+
verification_entry["button_readback_pending"] = True
|
|
4175
|
+
verification_entry["readback_ambiguous"] = True
|
|
4176
|
+
verification_entry["matching_view_keys"] = matched_keys
|
|
4177
|
+
button_readback_pending = True
|
|
4178
|
+
verification_by_view.append(verification_entry)
|
|
4179
|
+
continue
|
|
4180
|
+
try:
|
|
4181
|
+
config_response = self.views.view_get_config(profile=profile, viewgraph_key=verification_key)
|
|
4182
|
+
config_result = (config_response.get("result") or {}) if isinstance(config_response.get("result"), dict) else {}
|
|
4183
|
+
actual_buttons = _normalize_view_buttons_for_compare(config_result)
|
|
4184
|
+
buttons_verified = actual_buttons == expected_buttons
|
|
4185
|
+
verification_entry["buttons_verified"] = buttons_verified
|
|
4186
|
+
verification_entry["view_key"] = verification_key
|
|
4187
|
+
verification_entry["expected_buttons"] = deepcopy(expected_buttons)
|
|
4188
|
+
verification_entry["actual_buttons"] = actual_buttons
|
|
4189
|
+
if not buttons_verified:
|
|
4190
|
+
button_mismatches.append(
|
|
4191
|
+
{
|
|
4192
|
+
"name": name,
|
|
4193
|
+
"type": item.get("type"),
|
|
4194
|
+
"expected_buttons": deepcopy(expected_buttons),
|
|
4195
|
+
"actual_buttons": actual_buttons,
|
|
4196
|
+
}
|
|
4197
|
+
)
|
|
4198
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
4199
|
+
api_error = _coerce_api_error(error)
|
|
4200
|
+
verification_entry["buttons_verified"] = None
|
|
4201
|
+
verification_entry["button_readback_pending"] = True
|
|
4202
|
+
verification_entry["request_id"] = api_error.request_id
|
|
4203
|
+
verification_entry["transport_error"] = {
|
|
4204
|
+
"http_status": api_error.http_status,
|
|
4205
|
+
"backend_code": api_error.backend_code,
|
|
4206
|
+
"category": api_error.category,
|
|
4207
|
+
}
|
|
4208
|
+
button_readback_pending = True
|
|
3746
4209
|
verification_by_view.append(verification_entry)
|
|
3747
4210
|
elif status == "removed":
|
|
3748
4211
|
verification_by_view.append(
|
|
@@ -3769,6 +4232,7 @@ class AiBuilderFacade:
|
|
|
3769
4232
|
and all(name not in verified_names for name in removed)
|
|
3770
4233
|
)
|
|
3771
4234
|
view_filters_verified = verified and not filter_readback_pending and not filter_mismatches
|
|
4235
|
+
view_buttons_verified = verified and not button_readback_pending and not button_mismatches
|
|
3772
4236
|
noop = not created and not updated and not removed
|
|
3773
4237
|
if failed_views:
|
|
3774
4238
|
successful_changes = bool(created or updated or removed)
|
|
@@ -3781,34 +4245,48 @@ class AiBuilderFacade:
|
|
|
3781
4245
|
"normalized_args": normalized_args,
|
|
3782
4246
|
"missing_fields": [],
|
|
3783
4247
|
"allowed_values": {"view_types": [member.value for member in PublicViewType], "view.filter.operator": [member.value for member in ViewFilterOperator]},
|
|
3784
|
-
"details": {
|
|
4248
|
+
"details": {
|
|
4249
|
+
"per_view_results": view_results,
|
|
4250
|
+
"filter_mismatches": filter_mismatches,
|
|
4251
|
+
"button_mismatches": button_mismatches,
|
|
4252
|
+
},
|
|
3785
4253
|
"request_id": first_failure.get("request_id"),
|
|
3786
4254
|
"suggested_next_call": {"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
3787
4255
|
"backend_code": first_failure.get("backend_code"),
|
|
3788
4256
|
"http_status": first_failure.get("http_status"),
|
|
3789
4257
|
"noop": noop,
|
|
3790
|
-
"warnings":
|
|
4258
|
+
"warnings": (
|
|
4259
|
+
[_warning("VIEW_FILTERS_UNVERIFIED", "view definitions may exist, but saved filter behavior is not fully verified")] if (filter_readback_pending or filter_mismatches) else []
|
|
4260
|
+
)
|
|
4261
|
+
+ (
|
|
4262
|
+
[_warning("VIEW_BUTTONS_UNVERIFIED", "view definitions may exist, but saved button behavior is not fully verified")] if (button_readback_pending or button_mismatches) else []
|
|
4263
|
+
),
|
|
3791
4264
|
"verification": {
|
|
3792
4265
|
"views_verified": verified,
|
|
3793
4266
|
"view_filters_verified": view_filters_verified,
|
|
4267
|
+
"view_buttons_verified": view_buttons_verified,
|
|
3794
4268
|
"views_read_unavailable": verified_views_unavailable,
|
|
3795
4269
|
"by_view": verification_by_view,
|
|
3796
4270
|
},
|
|
3797
4271
|
"app_key": app_key,
|
|
3798
4272
|
"views_diff": {"created": created, "updated": updated, "removed": removed, "failed": failed_views},
|
|
3799
|
-
"verified": verified and view_filters_verified,
|
|
4273
|
+
"verified": verified and view_filters_verified and view_buttons_verified,
|
|
3800
4274
|
}
|
|
3801
4275
|
return finalize(self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response))
|
|
3802
4276
|
warnings: list[dict[str, Any]] = []
|
|
3803
4277
|
if filter_readback_pending or filter_mismatches:
|
|
3804
4278
|
warnings.append(_warning("VIEW_FILTERS_UNVERIFIED", "view definitions were applied, but saved filter behavior is not fully verified"))
|
|
4279
|
+
if button_readback_pending or button_mismatches:
|
|
4280
|
+
warnings.append(_warning("VIEW_BUTTONS_UNVERIFIED", "view definitions were applied, but saved button behavior is not fully verified"))
|
|
3805
4281
|
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),
|
|
4282
|
+
"status": "success" if verified and view_filters_verified and view_buttons_verified else "partial_success",
|
|
4283
|
+
"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"),
|
|
4284
|
+
"recoverable": not (verified and view_filters_verified and view_buttons_verified),
|
|
3809
4285
|
"message": (
|
|
3810
4286
|
"applied view patch"
|
|
3811
|
-
if verified and view_filters_verified
|
|
4287
|
+
if verified and view_filters_verified and view_buttons_verified
|
|
4288
|
+
else "applied view patch; buttons did not fully verify"
|
|
4289
|
+
if button_mismatches
|
|
3812
4290
|
else "applied view patch; filters did not fully verify"
|
|
3813
4291
|
if filter_mismatches
|
|
3814
4292
|
else "applied view patch; views readback pending"
|
|
@@ -3816,21 +4294,26 @@ class AiBuilderFacade:
|
|
|
3816
4294
|
"normalized_args": normalized_args,
|
|
3817
4295
|
"missing_fields": [],
|
|
3818
4296
|
"allowed_values": {"view_types": [member.value for member in PublicViewType], "view.filter.operator": [member.value for member in ViewFilterOperator]},
|
|
3819
|
-
"details": {
|
|
4297
|
+
"details": {
|
|
4298
|
+
**({"filter_mismatches": filter_mismatches} if filter_mismatches else {}),
|
|
4299
|
+
**({"button_mismatches": button_mismatches} if button_mismatches else {}),
|
|
4300
|
+
},
|
|
3820
4301
|
"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}},
|
|
4302
|
+
"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
4303
|
"noop": noop,
|
|
3823
4304
|
"warnings": warnings,
|
|
3824
4305
|
"verification": {
|
|
3825
4306
|
"views_verified": verified,
|
|
3826
4307
|
"view_filters_verified": view_filters_verified,
|
|
4308
|
+
"view_buttons_verified": view_buttons_verified,
|
|
3827
4309
|
"views_read_unavailable": verified_views_unavailable,
|
|
3828
4310
|
"filter_readback_pending": filter_readback_pending,
|
|
4311
|
+
"button_readback_pending": button_readback_pending,
|
|
3829
4312
|
"by_view": verification_by_view,
|
|
3830
4313
|
},
|
|
3831
4314
|
"app_key": app_key,
|
|
3832
4315
|
"views_diff": {"created": created, "updated": updated, "removed": removed, "failed": []},
|
|
3833
|
-
"verified": verified and view_filters_verified,
|
|
4316
|
+
"verified": verified and view_filters_verified and view_buttons_verified,
|
|
3834
4317
|
}
|
|
3835
4318
|
return finalize(self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response))
|
|
3836
4319
|
|
|
@@ -5002,6 +5485,156 @@ class AiBuilderFacade:
|
|
|
5002
5485
|
raise QingflowApiError(category="runtime", message="failed to attach app to package")
|
|
5003
5486
|
|
|
5004
5487
|
|
|
5488
|
+
def _extract_custom_button_id(value: Any) -> int | None:
|
|
5489
|
+
if isinstance(value, dict):
|
|
5490
|
+
for key in ("buttonId", "customButtonId", "id"):
|
|
5491
|
+
button_id = _coerce_positive_int(value.get(key))
|
|
5492
|
+
if button_id is not None:
|
|
5493
|
+
return button_id
|
|
5494
|
+
nested_result = value.get("result")
|
|
5495
|
+
if nested_result is not value:
|
|
5496
|
+
return _extract_custom_button_id(nested_result)
|
|
5497
|
+
return None
|
|
5498
|
+
return _coerce_positive_int(value)
|
|
5499
|
+
|
|
5500
|
+
|
|
5501
|
+
def _serialize_custom_button_payload(payload: CustomButtonPatch) -> dict[str, Any]:
|
|
5502
|
+
data = payload.model_dump(mode="json", exclude_none=True)
|
|
5503
|
+
serialized: dict[str, Any] = {
|
|
5504
|
+
"buttonText": data["button_text"],
|
|
5505
|
+
"backgroundColor": data["background_color"],
|
|
5506
|
+
"textColor": data["text_color"],
|
|
5507
|
+
"buttonIcon": data["button_icon"],
|
|
5508
|
+
"triggerAction": data["trigger_action"],
|
|
5509
|
+
}
|
|
5510
|
+
if str(data.get("trigger_link_url") or "").strip():
|
|
5511
|
+
serialized["triggerLinkUrl"] = data["trigger_link_url"]
|
|
5512
|
+
trigger_add_data_config = data.get("trigger_add_data_config")
|
|
5513
|
+
if isinstance(trigger_add_data_config, dict):
|
|
5514
|
+
serialized["triggerAddDataConfig"] = _serialize_custom_button_add_data_config(trigger_add_data_config)
|
|
5515
|
+
external_qrobot_config = data.get("external_qrobot_config")
|
|
5516
|
+
if isinstance(external_qrobot_config, dict):
|
|
5517
|
+
serialized["customButtonExternalQRobotRelationVO"] = _serialize_custom_button_external_qrobot_config(external_qrobot_config)
|
|
5518
|
+
trigger_wings_config = data.get("trigger_wings_config")
|
|
5519
|
+
if isinstance(trigger_wings_config, dict):
|
|
5520
|
+
serialized["triggerWingsConfig"] = _serialize_custom_button_wings_config(trigger_wings_config)
|
|
5521
|
+
return serialized
|
|
5522
|
+
|
|
5523
|
+
|
|
5524
|
+
def _serialize_custom_button_add_data_config(value: dict[str, Any]) -> dict[str, Any]:
|
|
5525
|
+
relation_rules = value.get("que_relation") or []
|
|
5526
|
+
return {
|
|
5527
|
+
"relatedAppKey": value.get("related_app_key"),
|
|
5528
|
+
"relatedAppName": value.get("related_app_name"),
|
|
5529
|
+
"queRelation": [_serialize_custom_button_match_rule(rule) for rule in relation_rules if isinstance(rule, dict)],
|
|
5530
|
+
}
|
|
5531
|
+
|
|
5532
|
+
|
|
5533
|
+
def _serialize_custom_button_external_qrobot_config(value: dict[str, Any]) -> dict[str, Any]:
|
|
5534
|
+
return {
|
|
5535
|
+
"externalQRobotConfigId": value.get("external_qrobot_config_id"),
|
|
5536
|
+
"triggeredText": value.get("triggered_text"),
|
|
5537
|
+
}
|
|
5538
|
+
|
|
5539
|
+
|
|
5540
|
+
def _serialize_custom_button_wings_config(value: dict[str, Any]) -> dict[str, Any]:
|
|
5541
|
+
return {
|
|
5542
|
+
"wingsAgentId": value.get("wings_agent_id"),
|
|
5543
|
+
"wingsAgentName": value.get("wings_agent_name"),
|
|
5544
|
+
"bindQueIdList": list(value.get("bind_que_id_list") or []),
|
|
5545
|
+
"bindFileQueIdList": list(value.get("bind_file_que_id_list") or []),
|
|
5546
|
+
"defaultPrompt": value.get("default_prompt"),
|
|
5547
|
+
"beingAutoSend": value.get("being_auto_send"),
|
|
5548
|
+
}
|
|
5549
|
+
|
|
5550
|
+
|
|
5551
|
+
def _serialize_custom_button_match_rule(value: dict[str, Any]) -> dict[str, Any]:
|
|
5552
|
+
serialized = {
|
|
5553
|
+
"queId": value.get("que_id"),
|
|
5554
|
+
"queTitle": value.get("que_title"),
|
|
5555
|
+
"queType": value.get("que_type"),
|
|
5556
|
+
"dateType": value.get("date_type"),
|
|
5557
|
+
"judgeType": value.get("judge_type"),
|
|
5558
|
+
"matchType": value.get("match_type"),
|
|
5559
|
+
"judgeValues": list(value.get("judge_values") or []),
|
|
5560
|
+
"judgeQueType": value.get("judge_que_type"),
|
|
5561
|
+
"judgeQueId": value.get("judge_que_id"),
|
|
5562
|
+
"pathValue": value.get("path_value"),
|
|
5563
|
+
"tableUpdateType": value.get("table_update_type"),
|
|
5564
|
+
"multiValue": value.get("multi_value"),
|
|
5565
|
+
"addRule": value.get("add_rule"),
|
|
5566
|
+
"fieldIdPrefix": value.get("field_id_prefix"),
|
|
5567
|
+
}
|
|
5568
|
+
judge_que_detail = value.get("judge_que_detail")
|
|
5569
|
+
if isinstance(judge_que_detail, dict):
|
|
5570
|
+
serialized["judgeQueDetail"] = {
|
|
5571
|
+
"queId": judge_que_detail.get("que_id"),
|
|
5572
|
+
"queTitle": judge_que_detail.get("que_title"),
|
|
5573
|
+
"queType": judge_que_detail.get("que_type"),
|
|
5574
|
+
}
|
|
5575
|
+
judge_value_details = value.get("judge_value_details")
|
|
5576
|
+
if isinstance(judge_value_details, list):
|
|
5577
|
+
serialized["judgeValueDetails"] = [
|
|
5578
|
+
{"id": item.get("id"), "value": item.get("value")}
|
|
5579
|
+
for item in judge_value_details
|
|
5580
|
+
if isinstance(item, dict)
|
|
5581
|
+
]
|
|
5582
|
+
filter_condition = value.get("filter_condition")
|
|
5583
|
+
if isinstance(filter_condition, list):
|
|
5584
|
+
serialized["filterCondition"] = [
|
|
5585
|
+
[_serialize_custom_button_match_rule(item) for item in group if isinstance(item, dict)]
|
|
5586
|
+
for group in filter_condition
|
|
5587
|
+
if isinstance(group, list)
|
|
5588
|
+
]
|
|
5589
|
+
return {key: deepcopy(item) for key, item in serialized.items() if item is not None}
|
|
5590
|
+
|
|
5591
|
+
|
|
5592
|
+
def _normalize_custom_button_summary(item: dict[str, Any]) -> dict[str, Any]:
|
|
5593
|
+
normalized = {
|
|
5594
|
+
"button_id": _coerce_positive_int(item.get("button_id") or item.get("buttonId") or item.get("id")),
|
|
5595
|
+
"button_text": str(item.get("button_text") or item.get("buttonText") or "").strip() or None,
|
|
5596
|
+
"button_icon": str(item.get("button_icon") or item.get("buttonIcon") or "").strip() or None,
|
|
5597
|
+
"background_color": str(item.get("background_color") or item.get("backgroundColor") or "").strip() or None,
|
|
5598
|
+
"text_color": str(item.get("text_color") or item.get("textColor") or "").strip() or None,
|
|
5599
|
+
"used_in_chart_count": _coerce_nonnegative_int(item.get("used_in_chart_count") or item.get("userInChartCount")),
|
|
5600
|
+
"being_effective_external_qrobot": bool(item.get("being_effective_external_qrobot") or item.get("beingEffectiveExternalQRobot")),
|
|
5601
|
+
}
|
|
5602
|
+
creator = item.get("creator_user_info") if isinstance(item.get("creator_user_info"), dict) else item.get("creatorUserInfo")
|
|
5603
|
+
if isinstance(creator, dict):
|
|
5604
|
+
normalized["creator_user_info"] = {
|
|
5605
|
+
"uid": creator.get("uid"),
|
|
5606
|
+
"name": creator.get("name"),
|
|
5607
|
+
"email": creator.get("email"),
|
|
5608
|
+
}
|
|
5609
|
+
return normalized
|
|
5610
|
+
|
|
5611
|
+
|
|
5612
|
+
def _normalize_custom_button_detail(item: dict[str, Any]) -> dict[str, Any]:
|
|
5613
|
+
normalized = _normalize_custom_button_summary(item)
|
|
5614
|
+
normalized.update(
|
|
5615
|
+
{
|
|
5616
|
+
"trigger_action": str(item.get("trigger_action") or item.get("triggerAction") or "").strip() or None,
|
|
5617
|
+
"trigger_link_url": str(item.get("trigger_link_url") or item.get("triggerLinkUrl") or "").strip() or None,
|
|
5618
|
+
}
|
|
5619
|
+
)
|
|
5620
|
+
trigger_add_data_config = item.get("trigger_add_data_config")
|
|
5621
|
+
if not isinstance(trigger_add_data_config, dict):
|
|
5622
|
+
trigger_add_data_config = item.get("triggerAddDataConfig")
|
|
5623
|
+
if isinstance(trigger_add_data_config, dict):
|
|
5624
|
+
normalized["trigger_add_data_config"] = deepcopy(trigger_add_data_config)
|
|
5625
|
+
external_qrobot_config = item.get("external_qrobot_config")
|
|
5626
|
+
if not isinstance(external_qrobot_config, dict):
|
|
5627
|
+
external_qrobot_config = item.get("customButtonExternalQRobotRelationVO")
|
|
5628
|
+
if isinstance(external_qrobot_config, dict):
|
|
5629
|
+
normalized["external_qrobot_config"] = deepcopy(external_qrobot_config)
|
|
5630
|
+
trigger_wings_config = item.get("trigger_wings_config")
|
|
5631
|
+
if not isinstance(trigger_wings_config, dict):
|
|
5632
|
+
trigger_wings_config = item.get("triggerWingsConfig")
|
|
5633
|
+
if isinstance(trigger_wings_config, dict):
|
|
5634
|
+
normalized["trigger_wings_config"] = deepcopy(trigger_wings_config)
|
|
5635
|
+
return normalized
|
|
5636
|
+
|
|
5637
|
+
|
|
5005
5638
|
def _failed(
|
|
5006
5639
|
error_code: str,
|
|
5007
5640
|
message: str,
|
|
@@ -7467,6 +8100,18 @@ def _merge_view_summary_with_config(base: dict[str, Any], *, config: dict[str, A
|
|
|
7467
8100
|
if not summary.get("group_by") and display_config.get("group_by"):
|
|
7468
8101
|
summary["group_by"] = display_config.get("group_by")
|
|
7469
8102
|
config_enriched = True
|
|
8103
|
+
button_entries, button_source = _extract_view_button_entries(config)
|
|
8104
|
+
if button_entries:
|
|
8105
|
+
summary["buttons"] = [_normalize_view_button_entry(entry) for entry in button_entries]
|
|
8106
|
+
summary["button_count"] = len(button_entries)
|
|
8107
|
+
if button_source:
|
|
8108
|
+
summary["button_read_source"] = button_source
|
|
8109
|
+
config_enriched = True
|
|
8110
|
+
elif button_source:
|
|
8111
|
+
summary["buttons"] = []
|
|
8112
|
+
summary["button_count"] = 0
|
|
8113
|
+
summary["button_read_source"] = button_source
|
|
8114
|
+
config_enriched = True
|
|
7470
8115
|
if config_enriched:
|
|
7471
8116
|
summary["read_source"] = "view_config"
|
|
7472
8117
|
return summary
|
|
@@ -7580,6 +8225,339 @@ def _extract_view_display_config(
|
|
|
7580
8225
|
return display_config
|
|
7581
8226
|
|
|
7582
8227
|
|
|
8228
|
+
def _normalize_view_button_type(value: Any) -> str | None:
|
|
8229
|
+
normalized = str(value or "").strip().upper()
|
|
8230
|
+
if normalized in {"SYSTEM", "CUSTOM"}:
|
|
8231
|
+
return normalized
|
|
8232
|
+
return None
|
|
8233
|
+
|
|
8234
|
+
|
|
8235
|
+
def _normalize_view_button_config_type(value: Any) -> str | None:
|
|
8236
|
+
normalized = str(value or "").strip().upper()
|
|
8237
|
+
if normalized in {"TOP", "DETAIL"}:
|
|
8238
|
+
return normalized
|
|
8239
|
+
return None
|
|
8240
|
+
|
|
8241
|
+
|
|
8242
|
+
def _normalize_print_tpls(value: Any) -> list[dict[str, Any]]:
|
|
8243
|
+
if not isinstance(value, list):
|
|
8244
|
+
return []
|
|
8245
|
+
items: list[dict[str, Any]] = []
|
|
8246
|
+
for item in value:
|
|
8247
|
+
if isinstance(item, dict):
|
|
8248
|
+
tpl_id = item.get("printTplId")
|
|
8249
|
+
if tpl_id is None:
|
|
8250
|
+
tpl_id = item.get("templateId")
|
|
8251
|
+
if tpl_id is None:
|
|
8252
|
+
tpl_id = item.get("id")
|
|
8253
|
+
tpl_name = item.get("printTplName")
|
|
8254
|
+
if tpl_name is None:
|
|
8255
|
+
tpl_name = item.get("templateName")
|
|
8256
|
+
if tpl_name is None:
|
|
8257
|
+
tpl_name = item.get("name")
|
|
8258
|
+
normalized: dict[str, Any] = {}
|
|
8259
|
+
if tpl_id is not None:
|
|
8260
|
+
normalized["id"] = str(tpl_id)
|
|
8261
|
+
if str(tpl_name or "").strip():
|
|
8262
|
+
normalized["name"] = str(tpl_name).strip()
|
|
8263
|
+
if normalized:
|
|
8264
|
+
items.append(normalized)
|
|
8265
|
+
continue
|
|
8266
|
+
scalar = str(item or "").strip()
|
|
8267
|
+
if scalar:
|
|
8268
|
+
items.append({"id": scalar})
|
|
8269
|
+
return items
|
|
8270
|
+
|
|
8271
|
+
|
|
8272
|
+
def _normalize_print_tpls_for_compare(value: Any) -> list[str]:
|
|
8273
|
+
normalized_items: list[str] = []
|
|
8274
|
+
for item in _normalize_print_tpls(value):
|
|
8275
|
+
item_id = str(item.get("id") or "").strip()
|
|
8276
|
+
item_name = str(item.get("name") or "").strip()
|
|
8277
|
+
normalized_items.append(item_id or item_name)
|
|
8278
|
+
return normalized_items
|
|
8279
|
+
|
|
8280
|
+
|
|
8281
|
+
def _serialize_print_tpl_ids(value: Any) -> list[str]:
|
|
8282
|
+
return [item for item in _normalize_print_tpls_for_compare(value) if item]
|
|
8283
|
+
|
|
8284
|
+
|
|
8285
|
+
def _extract_view_button_entries(config: dict[str, Any]) -> tuple[list[dict[str, Any]], str | None]:
|
|
8286
|
+
if not isinstance(config, dict):
|
|
8287
|
+
return [], None
|
|
8288
|
+
raw_vo = config.get("buttonConfigVO")
|
|
8289
|
+
if isinstance(raw_vo, list):
|
|
8290
|
+
return [deepcopy(item) for item in raw_vo if isinstance(item, dict)], "buttonConfigVO"
|
|
8291
|
+
raw_dtos = config.get("buttonConfigDTOList")
|
|
8292
|
+
if isinstance(raw_dtos, list):
|
|
8293
|
+
return [deepcopy(item) for item in raw_dtos if isinstance(item, dict)], "buttonConfigDTOList"
|
|
8294
|
+
grouped = config.get("buttonConfig")
|
|
8295
|
+
if not isinstance(grouped, dict):
|
|
8296
|
+
return [], None
|
|
8297
|
+
entries: list[dict[str, Any]] = []
|
|
8298
|
+
for item in grouped.get("topButtonList") or []:
|
|
8299
|
+
if not isinstance(item, dict):
|
|
8300
|
+
continue
|
|
8301
|
+
entry = deepcopy(item)
|
|
8302
|
+
entry.setdefault("configType", "TOP")
|
|
8303
|
+
entry.setdefault("beingMain", True)
|
|
8304
|
+
entries.append(entry)
|
|
8305
|
+
for item in grouped.get("mainButtonDetailList") or []:
|
|
8306
|
+
if not isinstance(item, dict):
|
|
8307
|
+
continue
|
|
8308
|
+
entry = deepcopy(item)
|
|
8309
|
+
entry.setdefault("configType", "DETAIL")
|
|
8310
|
+
entry["beingMain"] = True
|
|
8311
|
+
entries.append(entry)
|
|
8312
|
+
for item in grouped.get("moreButtonDetailList") or []:
|
|
8313
|
+
if not isinstance(item, dict):
|
|
8314
|
+
continue
|
|
8315
|
+
entry = deepcopy(item)
|
|
8316
|
+
entry.setdefault("configType", "DETAIL")
|
|
8317
|
+
entry["beingMain"] = False
|
|
8318
|
+
entries.append(entry)
|
|
8319
|
+
return entries, "buttonConfig"
|
|
8320
|
+
|
|
8321
|
+
|
|
8322
|
+
def _normalize_view_button_entry(entry: dict[str, Any]) -> dict[str, Any]:
|
|
8323
|
+
button_id = _coerce_positive_int(entry.get("buttonId") or entry.get("button_id") or entry.get("id"))
|
|
8324
|
+
button_text = str(entry.get("buttonText") or "").strip() or None
|
|
8325
|
+
default_button_text = str(entry.get("defaultButtonText") or "").strip() or None
|
|
8326
|
+
normalized: dict[str, Any] = {
|
|
8327
|
+
"button_type": _normalize_view_button_type(entry.get("buttonType") or entry.get("button_type")),
|
|
8328
|
+
"config_type": _normalize_view_button_config_type(entry.get("configType") or entry.get("config_type")),
|
|
8329
|
+
"button_id": button_id,
|
|
8330
|
+
"button_text": button_text or default_button_text,
|
|
8331
|
+
"being_main": bool(entry.get("beingMain", False)),
|
|
8332
|
+
"trigger_action": str(entry.get("triggerAction") or "").strip() or None,
|
|
8333
|
+
"print_tpls": _normalize_print_tpls(entry.get("printTpls")),
|
|
8334
|
+
"button_formula_type": _coerce_positive_int(entry.get("buttonFormulaType")) or 1,
|
|
8335
|
+
"button_limit": _normalize_view_filter_groups_for_compare(entry.get("buttonLimit")),
|
|
8336
|
+
}
|
|
8337
|
+
for public_key, source_key in (
|
|
8338
|
+
("default_button_text", "defaultButtonText"),
|
|
8339
|
+
("button_icon", "buttonIcon"),
|
|
8340
|
+
("background_color", "backgroundColor"),
|
|
8341
|
+
("text_color", "textColor"),
|
|
8342
|
+
("trigger_link_url", "triggerLinkUrl"),
|
|
8343
|
+
("button_formula", "buttonFormula"),
|
|
8344
|
+
):
|
|
8345
|
+
value = entry.get(source_key)
|
|
8346
|
+
if isinstance(value, str):
|
|
8347
|
+
value = value.strip() or None
|
|
8348
|
+
if value not in {None, ""}:
|
|
8349
|
+
normalized[public_key] = deepcopy(value)
|
|
8350
|
+
trigger_add_data_config = entry.get("triggerAddDataConfig")
|
|
8351
|
+
if isinstance(trigger_add_data_config, dict):
|
|
8352
|
+
normalized["trigger_add_data_config"] = deepcopy(trigger_add_data_config)
|
|
8353
|
+
trigger_wings_config = entry.get("triggerWingsConfig")
|
|
8354
|
+
if isinstance(trigger_wings_config, dict):
|
|
8355
|
+
normalized["trigger_wings_config"] = deepcopy(trigger_wings_config)
|
|
8356
|
+
return normalized
|
|
8357
|
+
|
|
8358
|
+
|
|
8359
|
+
def _normalize_view_buttons_for_compare(value: Any) -> list[dict[str, Any]]:
|
|
8360
|
+
if isinstance(value, dict):
|
|
8361
|
+
entries, _ = _extract_view_button_entries(value)
|
|
8362
|
+
elif isinstance(value, list):
|
|
8363
|
+
entries = [item for item in value if isinstance(item, dict)]
|
|
8364
|
+
else:
|
|
8365
|
+
entries = []
|
|
8366
|
+
normalized_entries: list[dict[str, Any]] = []
|
|
8367
|
+
for entry in entries:
|
|
8368
|
+
normalized = _normalize_view_button_entry(entry)
|
|
8369
|
+
normalized_entries.append(
|
|
8370
|
+
{
|
|
8371
|
+
"button_type": normalized.get("button_type"),
|
|
8372
|
+
"config_type": normalized.get("config_type"),
|
|
8373
|
+
"button_id": normalized.get("button_id"),
|
|
8374
|
+
"button_text": normalized.get("button_text"),
|
|
8375
|
+
"button_icon": normalized.get("button_icon"),
|
|
8376
|
+
"background_color": normalized.get("background_color"),
|
|
8377
|
+
"text_color": normalized.get("text_color"),
|
|
8378
|
+
"trigger_action": normalized.get("trigger_action"),
|
|
8379
|
+
"trigger_link_url": normalized.get("trigger_link_url"),
|
|
8380
|
+
"being_main": bool(normalized.get("being_main", False)),
|
|
8381
|
+
"print_tpls": _normalize_print_tpls_for_compare(normalized.get("print_tpls")),
|
|
8382
|
+
"button_formula": str(normalized.get("button_formula") or ""),
|
|
8383
|
+
"button_formula_type": _coerce_positive_int(normalized.get("button_formula_type")) or 1,
|
|
8384
|
+
"button_limit": deepcopy(normalized.get("button_limit") or []),
|
|
8385
|
+
}
|
|
8386
|
+
)
|
|
8387
|
+
return normalized_entries
|
|
8388
|
+
|
|
8389
|
+
|
|
8390
|
+
def _serialize_existing_view_button_entry(entry: dict[str, Any]) -> dict[str, Any]:
|
|
8391
|
+
dto: dict[str, Any] = {}
|
|
8392
|
+
button_id = _coerce_positive_int(entry.get("buttonId") or entry.get("button_id") or entry.get("id"))
|
|
8393
|
+
if button_id is not None:
|
|
8394
|
+
dto["buttonId"] = button_id
|
|
8395
|
+
button_type = _normalize_view_button_type(entry.get("buttonType") or entry.get("button_type"))
|
|
8396
|
+
if button_type is not None:
|
|
8397
|
+
dto["buttonType"] = button_type
|
|
8398
|
+
config_type = _normalize_view_button_config_type(entry.get("configType") or entry.get("config_type"))
|
|
8399
|
+
if config_type is not None:
|
|
8400
|
+
dto["configType"] = config_type
|
|
8401
|
+
dto["beingMain"] = bool(entry.get("beingMain", False))
|
|
8402
|
+
dto["buttonLimit"] = deepcopy(entry.get("buttonLimit") or [])
|
|
8403
|
+
dto["buttonFormula"] = str(entry.get("buttonFormula") or "")
|
|
8404
|
+
dto["buttonFormulaType"] = _coerce_positive_int(entry.get("buttonFormulaType")) or 1
|
|
8405
|
+
dto["printTpls"] = _serialize_print_tpl_ids(entry.get("printTpls"))
|
|
8406
|
+
for source_key, target_key in (
|
|
8407
|
+
("buttonText", "buttonText"),
|
|
8408
|
+
("buttonIcon", "buttonIcon"),
|
|
8409
|
+
("backgroundColor", "backgroundColor"),
|
|
8410
|
+
("textColor", "textColor"),
|
|
8411
|
+
("triggerAction", "triggerAction"),
|
|
8412
|
+
("triggerLinkUrl", "triggerLinkUrl"),
|
|
8413
|
+
):
|
|
8414
|
+
if source_key in entry:
|
|
8415
|
+
dto[target_key] = deepcopy(entry.get(source_key))
|
|
8416
|
+
if isinstance(entry.get("triggerAddDataConfig"), dict):
|
|
8417
|
+
dto["triggerAddDataConfig"] = deepcopy(entry.get("triggerAddDataConfig"))
|
|
8418
|
+
if isinstance(entry.get("triggerWingsConfig"), dict):
|
|
8419
|
+
dto["triggerWingsConfig"] = deepcopy(entry.get("triggerWingsConfig"))
|
|
8420
|
+
return dto
|
|
8421
|
+
|
|
8422
|
+
|
|
8423
|
+
def _extract_existing_view_button_dtos(config: dict[str, Any]) -> list[dict[str, Any]]:
|
|
8424
|
+
if not isinstance(config, dict):
|
|
8425
|
+
return []
|
|
8426
|
+
button_config_dtos = config.get("buttonConfigDTOList")
|
|
8427
|
+
if isinstance(button_config_dtos, list):
|
|
8428
|
+
return [deepcopy(item) for item in button_config_dtos if isinstance(item, dict)]
|
|
8429
|
+
entries, _ = _extract_view_button_entries(config)
|
|
8430
|
+
return [_serialize_existing_view_button_entry(entry) for entry in entries if isinstance(entry, dict)]
|
|
8431
|
+
|
|
8432
|
+
|
|
8433
|
+
def _resolve_view_button_dtos_for_patch(
|
|
8434
|
+
*,
|
|
8435
|
+
config: dict[str, Any],
|
|
8436
|
+
patch: ViewUpsertPatch,
|
|
8437
|
+
explicit_button_dtos: list[dict[str, Any]] | None,
|
|
8438
|
+
) -> list[dict[str, Any]] | None:
|
|
8439
|
+
if patch.buttons is None:
|
|
8440
|
+
return _extract_existing_view_button_dtos(config)
|
|
8441
|
+
return deepcopy(explicit_button_dtos or [])
|
|
8442
|
+
|
|
8443
|
+
|
|
8444
|
+
def _build_grouped_view_button_config(button_config_dtos: list[dict[str, Any]]) -> dict[str, list[dict[str, Any]]]:
|
|
8445
|
+
grouped = {"topButtonList": [], "mainButtonDetailList": [], "moreButtonDetailList": []}
|
|
8446
|
+
for raw_item in button_config_dtos:
|
|
8447
|
+
if not isinstance(raw_item, dict):
|
|
8448
|
+
continue
|
|
8449
|
+
item = deepcopy(raw_item)
|
|
8450
|
+
config_type = _normalize_view_button_config_type(item.get("configType"))
|
|
8451
|
+
being_main = bool(item.get("beingMain", False))
|
|
8452
|
+
if config_type == "TOP":
|
|
8453
|
+
grouped["topButtonList"].append(item)
|
|
8454
|
+
elif being_main:
|
|
8455
|
+
grouped["mainButtonDetailList"].append(item)
|
|
8456
|
+
else:
|
|
8457
|
+
grouped["moreButtonDetailList"].append(item)
|
|
8458
|
+
return grouped
|
|
8459
|
+
|
|
8460
|
+
|
|
8461
|
+
def _build_view_button_dtos(
|
|
8462
|
+
*,
|
|
8463
|
+
current_fields_by_name: dict[str, dict[str, Any]],
|
|
8464
|
+
bindings: list[ViewButtonBindingPatch],
|
|
8465
|
+
valid_custom_button_ids: set[int],
|
|
8466
|
+
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
8467
|
+
dtos: list[dict[str, Any]] = []
|
|
8468
|
+
issues: list[dict[str, Any]] = []
|
|
8469
|
+
for binding in bindings:
|
|
8470
|
+
dto, binding_issues = _serialize_view_button_binding(
|
|
8471
|
+
binding=binding,
|
|
8472
|
+
current_fields_by_name=current_fields_by_name,
|
|
8473
|
+
valid_custom_button_ids=valid_custom_button_ids,
|
|
8474
|
+
)
|
|
8475
|
+
if binding_issues:
|
|
8476
|
+
issues.extend(binding_issues)
|
|
8477
|
+
continue
|
|
8478
|
+
dtos.append(dto)
|
|
8479
|
+
return dtos, issues
|
|
8480
|
+
|
|
8481
|
+
|
|
8482
|
+
def _serialize_view_button_binding(
|
|
8483
|
+
*,
|
|
8484
|
+
binding: ViewButtonBindingPatch,
|
|
8485
|
+
current_fields_by_name: dict[str, dict[str, Any]],
|
|
8486
|
+
valid_custom_button_ids: set[int],
|
|
8487
|
+
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
8488
|
+
if binding.button_type == PublicViewButtonType.custom and binding.button_id not in valid_custom_button_ids:
|
|
8489
|
+
return {}, [
|
|
8490
|
+
{
|
|
8491
|
+
"error_code": "UNKNOWN_CUSTOM_BUTTON",
|
|
8492
|
+
"reason_path": "buttons[].button_id",
|
|
8493
|
+
"missing_fields": [],
|
|
8494
|
+
"details": {"button_id": binding.button_id},
|
|
8495
|
+
}
|
|
8496
|
+
]
|
|
8497
|
+
translated_limits, limit_issues = _build_view_button_limit_groups(
|
|
8498
|
+
current_fields_by_name=current_fields_by_name,
|
|
8499
|
+
groups=binding.button_limit,
|
|
8500
|
+
)
|
|
8501
|
+
if limit_issues:
|
|
8502
|
+
return {}, limit_issues
|
|
8503
|
+
dto: dict[str, Any] = {
|
|
8504
|
+
"buttonId": binding.button_id,
|
|
8505
|
+
"buttonType": binding.button_type.value,
|
|
8506
|
+
"configType": binding.config_type.value,
|
|
8507
|
+
"beingMain": bool(binding.being_main),
|
|
8508
|
+
"buttonLimit": translated_limits,
|
|
8509
|
+
"buttonFormula": binding.button_formula or "",
|
|
8510
|
+
"buttonFormulaType": binding.button_formula_type,
|
|
8511
|
+
"printTpls": _serialize_print_tpl_ids(binding.print_tpls),
|
|
8512
|
+
}
|
|
8513
|
+
if binding.button_type == PublicViewButtonType.system:
|
|
8514
|
+
dto["buttonText"] = binding.button_text
|
|
8515
|
+
dto["buttonIcon"] = binding.button_icon
|
|
8516
|
+
dto["backgroundColor"] = binding.background_color
|
|
8517
|
+
dto["textColor"] = binding.text_color
|
|
8518
|
+
dto["triggerAction"] = binding.trigger_action
|
|
8519
|
+
return dto, []
|
|
8520
|
+
|
|
8521
|
+
|
|
8522
|
+
def _build_view_button_limit_groups(
|
|
8523
|
+
*,
|
|
8524
|
+
current_fields_by_name: dict[str, dict[str, Any]],
|
|
8525
|
+
groups: list[list[ViewFilterRulePatch]],
|
|
8526
|
+
) -> tuple[list[list[dict[str, Any]]], list[dict[str, Any]]]:
|
|
8527
|
+
translated_groups: list[list[dict[str, Any]]] = []
|
|
8528
|
+
issues: list[dict[str, Any]] = []
|
|
8529
|
+
for raw_group in groups:
|
|
8530
|
+
if not isinstance(raw_group, list):
|
|
8531
|
+
continue
|
|
8532
|
+
translated_rules: list[dict[str, Any]] = []
|
|
8533
|
+
for raw_rule in raw_group:
|
|
8534
|
+
if hasattr(raw_rule, "model_dump"):
|
|
8535
|
+
raw_rule = raw_rule.model_dump(mode="json")
|
|
8536
|
+
if not isinstance(raw_rule, dict):
|
|
8537
|
+
continue
|
|
8538
|
+
field_name = str(raw_rule.get("field_name") or "").strip()
|
|
8539
|
+
field = current_fields_by_name.get(field_name)
|
|
8540
|
+
if field is None:
|
|
8541
|
+
issues.append(
|
|
8542
|
+
{
|
|
8543
|
+
"error_code": "UNKNOWN_VIEW_FIELD",
|
|
8544
|
+
"missing_fields": [field_name] if field_name else [],
|
|
8545
|
+
"reason_path": "buttons[].button_limit[].field_name",
|
|
8546
|
+
}
|
|
8547
|
+
)
|
|
8548
|
+
continue
|
|
8549
|
+
translated_rule, issue = _translate_view_filter_rule(field=field, rule=raw_rule)
|
|
8550
|
+
if issue:
|
|
8551
|
+
issue = deepcopy(issue)
|
|
8552
|
+
issue["reason_path"] = "buttons[].button_limit[].values"
|
|
8553
|
+
issues.append(issue)
|
|
8554
|
+
continue
|
|
8555
|
+
translated_rules.append(translated_rule)
|
|
8556
|
+
if translated_rules:
|
|
8557
|
+
translated_groups.append(translated_rules)
|
|
8558
|
+
return translated_groups, issues
|
|
8559
|
+
|
|
8560
|
+
|
|
7583
8561
|
def _summarize_charts(result: Any) -> list[dict[str, Any]]:
|
|
7584
8562
|
if not isinstance(result, list):
|
|
7585
8563
|
return []
|
|
@@ -7929,6 +8907,8 @@ def _build_view_create_payload(
|
|
|
7929
8907
|
patch: ViewUpsertPatch,
|
|
7930
8908
|
ordinal: int,
|
|
7931
8909
|
view_filters: list[list[dict[str, Any]]],
|
|
8910
|
+
current_fields_by_name: dict[str, dict[str, Any]],
|
|
8911
|
+
explicit_button_dtos: list[dict[str, Any]] | None = None,
|
|
7932
8912
|
) -> JSONObject:
|
|
7933
8913
|
entity = _entity_spec_from_app(base_info=base_info, schema=schema, views=None)
|
|
7934
8914
|
parsed_schema = _parse_schema(schema)
|
|
@@ -7969,6 +8949,7 @@ def _build_view_create_payload(
|
|
|
7969
8949
|
group_que_id=field_map.get(patch.group_by or "") if patch.group_by else None,
|
|
7970
8950
|
view_filters=view_filters,
|
|
7971
8951
|
gantt_payload=gantt_config,
|
|
8952
|
+
button_config_dtos=explicit_button_dtos,
|
|
7972
8953
|
)
|
|
7973
8954
|
|
|
7974
8955
|
|
|
@@ -8111,6 +9092,8 @@ def _build_view_update_payload(
|
|
|
8111
9092
|
schema: dict[str, Any],
|
|
8112
9093
|
patch: ViewUpsertPatch,
|
|
8113
9094
|
view_filters: list[list[dict[str, Any]]],
|
|
9095
|
+
current_fields_by_name: dict[str, dict[str, Any]],
|
|
9096
|
+
explicit_button_dtos: list[dict[str, Any]] | None = None,
|
|
8114
9097
|
) -> JSONObject:
|
|
8115
9098
|
config_response = views.view_get_config(profile=profile, viewgraph_key=source_viewgraph_key)
|
|
8116
9099
|
config = config_response.get("result") if isinstance(config_response.get("result"), dict) else {}
|
|
@@ -8149,7 +9132,11 @@ def _build_view_update_payload(
|
|
|
8149
9132
|
payload.setdefault("defaultRowHigh", "compact")
|
|
8150
9133
|
payload.setdefault("viewgraphLimitType", 1)
|
|
8151
9134
|
payload.setdefault("viewgraphLimit", deepcopy(view_filters) if view_filters else [])
|
|
8152
|
-
|
|
9135
|
+
button_config_dtos = _resolve_view_button_dtos_for_patch(
|
|
9136
|
+
config=config,
|
|
9137
|
+
patch=patch,
|
|
9138
|
+
explicit_button_dtos=explicit_button_dtos,
|
|
9139
|
+
)
|
|
8153
9140
|
|
|
8154
9141
|
normalized_type = patch.type.value
|
|
8155
9142
|
existing_type = _normalize_view_type_name(payload.get("viewgraphType") or payload.get("type"))
|
|
@@ -8180,6 +9167,7 @@ def _build_view_update_payload(
|
|
|
8180
9167
|
group_que_id=field_map.get(patch.group_by or "") if patch.group_by else None,
|
|
8181
9168
|
view_filters=view_filters,
|
|
8182
9169
|
gantt_payload=gantt_payload,
|
|
9170
|
+
button_config_dtos=button_config_dtos,
|
|
8183
9171
|
)
|
|
8184
9172
|
|
|
8185
9173
|
|
|
@@ -8190,6 +9178,8 @@ def _build_minimal_view_payload(
|
|
|
8190
9178
|
patch: ViewUpsertPatch,
|
|
8191
9179
|
ordinal: int,
|
|
8192
9180
|
view_filters: list[list[dict[str, Any]]],
|
|
9181
|
+
current_fields_by_name: dict[str, dict[str, Any]],
|
|
9182
|
+
explicit_button_dtos: list[dict[str, Any]] | None = None,
|
|
8193
9183
|
) -> JSONObject:
|
|
8194
9184
|
field_map = extract_field_map(schema)
|
|
8195
9185
|
parsed_schema = _parse_schema(schema)
|
|
@@ -8222,6 +9212,7 @@ def _build_minimal_view_payload(
|
|
|
8222
9212
|
group_que_id=field_map.get(patch.group_by or "") if patch.group_by else None,
|
|
8223
9213
|
view_filters=view_filters,
|
|
8224
9214
|
gantt_payload=gantt_payload,
|
|
9215
|
+
button_config_dtos=explicit_button_dtos,
|
|
8225
9216
|
)
|
|
8226
9217
|
|
|
8227
9218
|
|
|
@@ -8233,6 +9224,7 @@ def _hydrate_view_backend_payload(
|
|
|
8233
9224
|
group_que_id: int | None,
|
|
8234
9225
|
view_filters: list[list[dict[str, Any]]] | None = None,
|
|
8235
9226
|
gantt_payload: dict[str, Any] | None = None,
|
|
9227
|
+
button_config_dtos: list[dict[str, Any]] | None = None,
|
|
8236
9228
|
) -> JSONObject:
|
|
8237
9229
|
data = deepcopy(payload)
|
|
8238
9230
|
data.setdefault("beingPinNavigate", True)
|
|
@@ -8268,9 +9260,11 @@ def _hydrate_view_backend_payload(
|
|
|
8268
9260
|
data.setdefault("asosChartConfig", {"limitType": 1, "asosChartIdList": []})
|
|
8269
9261
|
data.setdefault("viewgraphGanttConfigVO", None)
|
|
8270
9262
|
data.setdefault("viewgraphHierarchyConfigVO", None)
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
data["buttonConfig"] =
|
|
9263
|
+
if button_config_dtos is not None:
|
|
9264
|
+
data["buttonConfigDTOList"] = deepcopy(button_config_dtos)
|
|
9265
|
+
data["buttonConfig"] = _build_grouped_view_button_config(button_config_dtos)
|
|
9266
|
+
else:
|
|
9267
|
+
data.pop("buttonConfigVO", None)
|
|
8274
9268
|
if view_type == "table":
|
|
8275
9269
|
data["viewgraphType"] = "tableView"
|
|
8276
9270
|
data["beingShowTitleQue"] = False
|