@qingflow-tech/qingflow-app-user-mcp 1.0.7 → 1.0.9

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 CHANGED
@@ -3,13 +3,13 @@
3
3
  Install:
4
4
 
5
5
  ```bash
6
- npm install @qingflow-tech/qingflow-app-user-mcp@1.0.7
6
+ npm install @qingflow-tech/qingflow-app-user-mcp@1.0.9
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @qingflow-tech/qingflow-app-user-mcp@1.0.7 qingflow-app-user-mcp
12
+ npx -y -p @qingflow-tech/qingflow-app-user-mcp@1.0.9 qingflow-app-user-mcp
13
13
  ```
14
14
 
15
15
  Environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qingflow-tech/qingflow-app-user-mcp",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "Operational end-user MCP for Qingflow records, tasks, comments, and directory workflows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qingflow-mcp"
7
- version = "1.0.7"
7
+ version = "1.0.9"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -39,11 +39,14 @@ It is not a user-facing product spec. It exists to prevent skill drift.
39
39
  - `app_layout_apply`
40
40
  - `app_flow_apply`
41
41
  - `app_views_apply`
42
+ - `app_custom_buttons_apply`
43
+ - `app_associated_resources_apply`
42
44
  - `app_charts_apply`
43
45
  - `portal_apply`
44
46
  - `app_publish_verify`
45
47
  - `portal_apply` edit mode may omit `sections` for base-info-only updates
46
48
  - `app_charts_apply.visibility` is a public capability and should be treated as a base-only visibility update
49
+ - Existing object parameter replacement should use `patch_views`, `patch_buttons`, `patch_resources`, and `patch_charts`; `upsert_*` is for creation or full target config
47
50
  - `app_get.editability` uses:
48
51
  - `can_edit_app_base`
49
52
  - `can_edit_form`
@@ -9,6 +9,22 @@ from .button_style_catalog import resolve_button_style
9
9
  from ..solution.spec_models import StrictModel
10
10
 
11
11
 
12
+ def _normalize_builder_view_key_value(value: Any) -> Any:
13
+ if not isinstance(value, str):
14
+ return value
15
+ raw = value.strip()
16
+ if raw.startswith("custom:"):
17
+ return raw.split(":", 1)[1].strip()
18
+ return raw
19
+
20
+
21
+ def _normalize_builder_view_key_payload(payload: dict[str, Any]) -> dict[str, Any]:
22
+ for key in ("view_key", "viewKey", "viewgraphKey", "viewGraphKey"):
23
+ if key in payload:
24
+ payload[key] = _normalize_builder_view_key_value(payload[key])
25
+ return payload
26
+
27
+
12
28
  class PublicFieldType(str, Enum):
13
29
  text = "text"
14
30
  long_text = "long_text"
@@ -115,7 +131,7 @@ class PublicViewButtonType(str, Enum):
115
131
  class PublicViewButtonConfigType(str, Enum):
116
132
  top = "TOP"
117
133
  detail = "DETAIL"
118
- list = "LIST"
134
+ list = "INSIDE"
119
135
 
120
136
 
121
137
  class PublicButtonPlacement(str, Enum):
@@ -126,10 +142,34 @@ class PublicButtonPlacement(str, Enum):
126
142
 
127
143
  class PublicChartType(str, Enum):
128
144
  target = "target"
145
+ indicator = "indicator"
146
+ summary = "summary"
129
147
  pie = "pie"
130
148
  bar = "bar"
149
+ columnar = "columnar"
131
150
  line = "line"
132
151
  table = "table"
152
+ detail = "detail"
153
+ area = "area"
154
+ stacked_area = "stacked_area"
155
+ pct_stack_area = "pct_stack_area"
156
+ funnel = "funnel"
157
+ waterfall = "waterfall"
158
+ gauge = "gauge"
159
+ heatmap = "heatmap"
160
+ histogram = "histogram"
161
+ treemap = "treemap"
162
+ radar = "radar"
163
+ stacked_bar = "stacked_bar"
164
+ pct_stack_bar = "pct_stack_bar"
165
+ stacked_column = "stacked_column"
166
+ pct_stack_col = "pct_stack_col"
167
+ scatter = "scatter"
168
+ ring = "ring"
169
+ rose = "rose"
170
+ dualaxes = "dualaxes"
171
+ map = "map"
172
+ timeline = "timeline"
133
173
 
134
174
 
135
175
  class LayoutApplyMode(str, Enum):
@@ -1068,6 +1108,11 @@ class ViewUpsertPatch(StrictModel):
1068
1108
  "asosChartConfig",
1069
1109
  ),
1070
1110
  )
1111
+ partial_update: bool = Field(default=False, exclude=True, validation_alias=AliasChoices("_partial_update", "partial_update", "partialUpdate"))
1112
+ preserve_filters: bool = Field(default=False, exclude=True, validation_alias=AliasChoices("_preserve_filters", "preserve_filters", "preserveFilters"))
1113
+ preserve_buttons: bool = Field(default=False, exclude=True, validation_alias=AliasChoices("_preserve_buttons", "preserve_buttons", "preserveButtons"))
1114
+ preserve_query_conditions: bool = Field(default=False, exclude=True, validation_alias=AliasChoices("_preserve_query_conditions", "preserve_query_conditions", "preserveQueryConditions"))
1115
+ preserve_associated_resources: bool = Field(default=False, exclude=True, validation_alias=AliasChoices("_preserve_associated_resources", "preserve_associated_resources", "preserveAssociatedResources"))
1071
1116
 
1072
1117
  @model_validator(mode="before")
1073
1118
  @classmethod
@@ -1075,6 +1120,7 @@ class ViewUpsertPatch(StrictModel):
1075
1120
  if not isinstance(value, dict):
1076
1121
  return value
1077
1122
  payload = dict(value)
1123
+ payload = _normalize_builder_view_key_payload(payload)
1078
1124
  if "fields" in payload and "columns" not in payload:
1079
1125
  payload["columns"] = payload.pop("fields")
1080
1126
  if "column_names" in payload and "columns" not in payload:
@@ -1123,6 +1169,28 @@ class ViewUpsertPatch(StrictModel):
1123
1169
  return self
1124
1170
 
1125
1171
 
1172
+ class ViewPartialPatch(StrictModel):
1173
+ view_key: str | None = Field(default=None, validation_alias=AliasChoices("view_key", "viewKey", "viewgraphKey", "viewGraphKey"))
1174
+ name: str | None = Field(default=None, validation_alias=AliasChoices("name", "view_name", "viewName"))
1175
+ set: dict[str, Any] = Field(default_factory=dict, validation_alias=AliasChoices("set", "update", "values"))
1176
+ unset: list[str] = Field(default_factory=list, validation_alias=AliasChoices("unset", "clear", "remove"))
1177
+
1178
+ @model_validator(mode="before")
1179
+ @classmethod
1180
+ def normalize_aliases(cls, value: Any) -> Any:
1181
+ if not isinstance(value, dict):
1182
+ return value
1183
+ return _normalize_builder_view_key_payload(dict(value))
1184
+
1185
+ @model_validator(mode="after")
1186
+ def validate_shape(self) -> "ViewPartialPatch":
1187
+ if not str(self.view_key or "").strip() and not str(self.name or "").strip():
1188
+ raise ValueError("patch_views[] requires view_key or unique name")
1189
+ if not self.set and not self.unset:
1190
+ raise ValueError("patch_views[] requires set or unset")
1191
+ return self
1192
+
1193
+
1126
1194
  class CustomButtonJudgeValuePatch(StrictModel):
1127
1195
  id: int | str | None = None
1128
1196
  value: Any | None = None
@@ -1190,6 +1258,21 @@ class CustomButtonFieldMappingPatch(StrictModel):
1190
1258
  target_field: Any = Field(validation_alias=AliasChoices("target_field", "targetField", "target"))
1191
1259
 
1192
1260
 
1261
+ class FieldMatchMappingPatch(StrictModel):
1262
+ target_field: Any = Field(validation_alias=AliasChoices("target_field", "targetField", "target", "field"))
1263
+ source_field: Any | None = Field(default=None, validation_alias=AliasChoices("source_field", "sourceField", "source"))
1264
+ value: Any | None = Field(default=None, validation_alias=AliasChoices("value", "static_value", "staticValue"))
1265
+ operator: str = Field(default="eq", validation_alias=AliasChoices("operator", "op", "judge_type", "judgeType"))
1266
+
1267
+ @model_validator(mode="after")
1268
+ def validate_shape(self) -> "FieldMatchMappingPatch":
1269
+ has_source = self.source_field is not None and str(self.source_field).strip() != ""
1270
+ has_value = self.value is not None
1271
+ if has_source == has_value:
1272
+ raise ValueError("match_mappings[] requires exactly one of source_field or value")
1273
+ return self
1274
+
1275
+
1193
1276
  class CustomButtonAddDataConfigPatch(StrictModel):
1194
1277
  related_app_key: str | None = Field(
1195
1278
  default=None,
@@ -1319,6 +1402,22 @@ class CustomButtonUpsertPatch(CustomButtonPatch):
1319
1402
  client_key: str | None = Field(default=None, validation_alias=AliasChoices("client_key", "clientKey"))
1320
1403
 
1321
1404
 
1405
+ class CustomButtonPartialPatch(StrictModel):
1406
+ button_id: int | None = Field(default=None, validation_alias=AliasChoices("button_id", "buttonId", "id"))
1407
+ button_text: str | None = Field(default=None, validation_alias=AliasChoices("button_text", "buttonText", "name"))
1408
+ client_key: str | None = Field(default=None, validation_alias=AliasChoices("client_key", "clientKey"))
1409
+ set: dict[str, Any] = Field(default_factory=dict, validation_alias=AliasChoices("set", "update", "values"))
1410
+ unset: list[str] = Field(default_factory=list, validation_alias=AliasChoices("unset", "clear", "remove"))
1411
+
1412
+ @model_validator(mode="after")
1413
+ def validate_shape(self) -> "CustomButtonPartialPatch":
1414
+ if self.button_id is None and not str(self.button_text or "").strip():
1415
+ raise ValueError("patch_buttons[] requires button_id or unique button_text")
1416
+ if not self.set and not self.unset:
1417
+ raise ValueError("patch_buttons[] requires set or unset")
1418
+ return self
1419
+
1420
+
1322
1421
  class CustomButtonRemovePatch(StrictModel):
1323
1422
  button_id: int | None = Field(default=None, validation_alias=AliasChoices("button_id", "buttonId", "id"))
1324
1423
  button_text: str | None = Field(default=None, validation_alias=AliasChoices("button_text", "buttonText", "name"))
@@ -1368,6 +1467,13 @@ class CustomButtonViewConfigPatch(StrictModel):
1368
1467
  mode: str = Field(default="merge", validation_alias=AliasChoices("mode", "apply_mode", "applyMode"))
1369
1468
  buttons: list[CustomButtonViewButtonBindingPatch] = Field(default_factory=list)
1370
1469
 
1470
+ @model_validator(mode="before")
1471
+ @classmethod
1472
+ def normalize_aliases(cls, value: Any) -> Any:
1473
+ if not isinstance(value, dict):
1474
+ return value
1475
+ return _normalize_builder_view_key_payload(dict(value))
1476
+
1371
1477
  @model_validator(mode="after")
1372
1478
  def validate_mode(self) -> "CustomButtonViewConfigPatch":
1373
1479
  normalized = str(self.mode or "").strip().lower()
@@ -1382,6 +1488,7 @@ class CustomButtonViewConfigPatch(StrictModel):
1382
1488
  class CustomButtonsApplyRequest(StrictModel):
1383
1489
  app_key: str
1384
1490
  upsert_buttons: list[CustomButtonUpsertPatch] = Field(default_factory=list)
1491
+ patch_buttons: list[CustomButtonPartialPatch] = Field(default_factory=list)
1385
1492
  remove_buttons: list[CustomButtonRemovePatch] = Field(default_factory=list)
1386
1493
  view_configs: list[CustomButtonViewConfigPatch] = Field(default_factory=list)
1387
1494
 
@@ -1395,6 +1502,8 @@ class CustomButtonsApplyRequest(StrictModel):
1395
1502
  payload["upsert_buttons"] = payload.pop("buttons")
1396
1503
  if "upsertButtons" in payload and "upsert_buttons" not in payload:
1397
1504
  payload["upsert_buttons"] = payload.pop("upsertButtons")
1505
+ if "patchButtons" in payload and "patch_buttons" not in payload:
1506
+ payload["patch_buttons"] = payload.pop("patchButtons")
1398
1507
  if "removeButtons" in payload and "remove_buttons" not in payload:
1399
1508
  payload["remove_buttons"] = payload.pop("removeButtons")
1400
1509
  if "viewConfigs" in payload and "view_configs" not in payload:
@@ -1403,8 +1512,8 @@ class CustomButtonsApplyRequest(StrictModel):
1403
1512
 
1404
1513
  @model_validator(mode="after")
1405
1514
  def validate_shape(self) -> "CustomButtonsApplyRequest":
1406
- if not self.upsert_buttons and not self.remove_buttons and not self.view_configs:
1407
- raise ValueError("custom button apply requires at least one upsert, remove, or view config operation")
1515
+ if not self.upsert_buttons and not self.patch_buttons and not self.remove_buttons and not self.view_configs:
1516
+ raise ValueError("custom button apply requires at least one upsert, patch, remove, or view config operation")
1408
1517
  return self
1409
1518
 
1410
1519
 
@@ -1421,6 +1530,10 @@ class AssociatedResourceUpsertPatch(StrictModel):
1421
1530
  report_source: str | None = Field(default=None, validation_alias=AliasChoices("report_source", "reportSource"))
1422
1531
  source_type: str | None = Field(default=None, validation_alias=AliasChoices("source_type", "sourceType"))
1423
1532
  match_rules: list[CustomButtonMatchRulePatch] = Field(default_factory=list, validation_alias=AliasChoices("match_rules", "matchRules"))
1533
+ match_mappings: list[FieldMatchMappingPatch] = Field(
1534
+ default_factory=list,
1535
+ validation_alias=AliasChoices("match_mappings", "matchMappings"),
1536
+ )
1424
1537
 
1425
1538
  @model_validator(mode="before")
1426
1539
  @classmethod
@@ -1428,6 +1541,7 @@ class AssociatedResourceUpsertPatch(StrictModel):
1428
1541
  if not isinstance(value, dict):
1429
1542
  return value
1430
1543
  payload = dict(value)
1544
+ payload = _normalize_builder_view_key_payload(payload)
1431
1545
  if "chart_id" in payload and "chart_key" not in payload and "chartKey" not in payload:
1432
1546
  payload["chart_key"] = str(payload.pop("chart_id"))
1433
1547
  raw_graph_type = str(payload.get("graph_type", payload.get("graphType", "")) or "").strip().lower()
@@ -1449,6 +1563,21 @@ class AssociatedResourceUpsertPatch(StrictModel):
1449
1563
  return payload
1450
1564
 
1451
1565
 
1566
+ class AssociatedResourcePartialPatch(StrictModel):
1567
+ associated_item_id: int = Field(validation_alias=AliasChoices("associated_item_id", "associatedItemId", "asosChartId", "id"))
1568
+ client_key: str | None = Field(default=None, validation_alias=AliasChoices("client_key", "clientKey"))
1569
+ set: dict[str, Any] = Field(default_factory=dict, validation_alias=AliasChoices("set", "update", "values"))
1570
+ unset: list[str] = Field(default_factory=list, validation_alias=AliasChoices("unset", "clear", "remove"))
1571
+
1572
+ @model_validator(mode="after")
1573
+ def validate_shape(self) -> "AssociatedResourcePartialPatch":
1574
+ if self.associated_item_id <= 0:
1575
+ raise ValueError("patch_resources[].associated_item_id must be a positive integer")
1576
+ if not self.set and not self.unset:
1577
+ raise ValueError("patch_resources[] requires set or unset")
1578
+ return self
1579
+
1580
+
1452
1581
  class AssociatedResourceViewConfigPatch(StrictModel):
1453
1582
  view_key: str = Field(validation_alias=AliasChoices("view_key", "viewKey", "viewgraphKey", "viewGraphKey"))
1454
1583
  visible: bool = Field(default=True, validation_alias=AliasChoices("visible", "enabled", "asosChartVisible"))
@@ -1459,12 +1588,20 @@ class AssociatedResourceViewConfigPatch(StrictModel):
1459
1588
  )
1460
1589
  associated_item_refs: list[str] = Field(default_factory=list, validation_alias=AliasChoices("associated_item_refs", "associatedItemRefs", "refs"))
1461
1590
 
1591
+ @model_validator(mode="before")
1592
+ @classmethod
1593
+ def normalize_aliases(cls, value: Any) -> Any:
1594
+ if not isinstance(value, dict):
1595
+ return value
1596
+ return _normalize_builder_view_key_payload(dict(value))
1597
+
1462
1598
 
1463
1599
  class AssociatedResourcesApplyRequest(StrictModel):
1464
1600
  app_key: str
1465
1601
  upsert_resources: list[AssociatedResourceUpsertPatch] = Field(default_factory=list)
1466
- remove_associated_item_ids: list[int] = Field(default_factory=list)
1467
- reorder_associated_item_ids: list[int] = Field(default_factory=list)
1602
+ patch_resources: list[AssociatedResourcePartialPatch] = Field(default_factory=list)
1603
+ remove_associated_item_ids: list[Any] = Field(default_factory=list)
1604
+ reorder_associated_item_ids: list[Any] = Field(default_factory=list)
1468
1605
  view_configs: list[AssociatedResourceViewConfigPatch] = Field(default_factory=list)
1469
1606
 
1470
1607
  @model_validator(mode="before")
@@ -1475,6 +1612,8 @@ class AssociatedResourcesApplyRequest(StrictModel):
1475
1612
  payload = dict(value)
1476
1613
  if "upsertResources" in payload and "upsert_resources" not in payload:
1477
1614
  payload["upsert_resources"] = payload.pop("upsertResources")
1615
+ if "patchResources" in payload and "patch_resources" not in payload:
1616
+ payload["patch_resources"] = payload.pop("patchResources")
1478
1617
  if "resources" in payload and "upsert_resources" not in payload:
1479
1618
  payload["upsert_resources"] = payload.pop("resources")
1480
1619
  if "removeAssociatedItemIds" in payload and "remove_associated_item_ids" not in payload:
@@ -1487,8 +1626,8 @@ class AssociatedResourcesApplyRequest(StrictModel):
1487
1626
 
1488
1627
  @model_validator(mode="after")
1489
1628
  def validate_shape(self) -> "AssociatedResourcesApplyRequest":
1490
- if not self.upsert_resources and not self.remove_associated_item_ids and not self.reorder_associated_item_ids and not self.view_configs:
1491
- raise ValueError("associated resources apply requires at least one resource or view config operation")
1629
+ if not self.upsert_resources and not self.patch_resources and not self.remove_associated_item_ids and not self.reorder_associated_item_ids and not self.view_configs:
1630
+ raise ValueError("associated resources apply requires at least one upsert, patch, remove, reorder, or view config operation")
1492
1631
  return self
1493
1632
 
1494
1633
 
@@ -1533,8 +1672,8 @@ class ViewButtonBindingPatch(StrictModel):
1533
1672
  payload["config_type"] = "TOP"
1534
1673
  elif normalized_config == "detail":
1535
1674
  payload["config_type"] = "DETAIL"
1536
- elif normalized_config in {"list", "row", "row_action"}:
1537
- payload["config_type"] = "LIST"
1675
+ elif normalized_config in {"inside", "list", "row", "row_action"}:
1676
+ payload["config_type"] = "INSIDE"
1538
1677
  raw_limits = payload.get("button_limit", payload.get("buttonLimit"))
1539
1678
  if isinstance(raw_limits, list) and raw_limits and all(isinstance(item, dict) for item in raw_limits):
1540
1679
  payload["button_limit"] = [raw_limits]
@@ -1660,10 +1799,18 @@ class ChartUpsertPatch(StrictModel):
1660
1799
  normalized = raw_type.strip().lower()
1661
1800
  aliases = {
1662
1801
  "targetchart": PublicChartType.target.value,
1802
+ "indicatorchart": PublicChartType.indicator.value,
1803
+ "summarychart": PublicChartType.summary.value,
1663
1804
  "piechart": PublicChartType.pie.value,
1664
1805
  "barchart": PublicChartType.bar.value,
1806
+ "columnchart": PublicChartType.columnar.value,
1807
+ "columnarchart": PublicChartType.columnar.value,
1665
1808
  "linechart": PublicChartType.line.value,
1666
1809
  "tablechart": PublicChartType.table.value,
1810
+ "detailchart": PublicChartType.detail.value,
1811
+ "percent_stacked_column": PublicChartType.pct_stack_col.value,
1812
+ "percent_stacked_bar": PublicChartType.pct_stack_bar.value,
1813
+ "percent_stacked_area": PublicChartType.pct_stack_area.value,
1667
1814
  }
1668
1815
  if normalized in aliases:
1669
1816
  payload["chart_type"] = aliases[normalized]
@@ -1676,9 +1823,39 @@ class ChartUpsertPatch(StrictModel):
1676
1823
  return payload
1677
1824
 
1678
1825
 
1826
+ class ChartPartialPatch(StrictModel):
1827
+ chart_id: str | None = None
1828
+ name: str | None = None
1829
+ set: dict[str, Any] = Field(default_factory=dict, validation_alias=AliasChoices("set", "update", "values"))
1830
+ unset: list[str] = Field(default_factory=list, validation_alias=AliasChoices("unset", "clear", "remove"))
1831
+
1832
+ @model_validator(mode="before")
1833
+ @classmethod
1834
+ def normalize_aliases(cls, value: Any) -> Any:
1835
+ if not isinstance(value, dict):
1836
+ return value
1837
+ payload = dict(value)
1838
+ if "id" in payload and "chart_id" not in payload:
1839
+ payload["chart_id"] = str(payload.pop("id"))
1840
+ if "chart_name" in payload and "name" not in payload:
1841
+ payload["name"] = payload.pop("chart_name")
1842
+ if "chartName" in payload and "name" not in payload:
1843
+ payload["name"] = payload.pop("chartName")
1844
+ return payload
1845
+
1846
+ @model_validator(mode="after")
1847
+ def validate_shape(self) -> "ChartPartialPatch":
1848
+ if not str(self.chart_id or "").strip() and not str(self.name or "").strip():
1849
+ raise ValueError("patch_charts[] requires chart_id or unique name")
1850
+ if not self.set and not self.unset:
1851
+ raise ValueError("patch_charts[] requires set or unset")
1852
+ return self
1853
+
1854
+
1679
1855
  class ChartApplyRequest(StrictModel):
1680
1856
  app_key: str
1681
1857
  upsert_charts: list[ChartUpsertPatch] = Field(default_factory=list)
1858
+ patch_charts: list[ChartPartialPatch] = Field(default_factory=list)
1682
1859
  remove_chart_ids: list[str] = Field(default_factory=list)
1683
1860
  reorder_chart_ids: list[str] = Field(default_factory=list)
1684
1861
 
@@ -1688,6 +1865,14 @@ class ChartApplyRequest(StrictModel):
1688
1865
  if not isinstance(value, dict):
1689
1866
  return value
1690
1867
  payload = dict(value)
1868
+ if "upsertCharts" in payload and "upsert_charts" not in payload:
1869
+ payload["upsert_charts"] = payload.pop("upsertCharts")
1870
+ if "patchCharts" in payload and "patch_charts" not in payload:
1871
+ payload["patch_charts"] = payload.pop("patchCharts")
1872
+ if "removeChartIds" in payload and "remove_chart_ids" not in payload:
1873
+ payload["remove_chart_ids"] = payload.pop("removeChartIds")
1874
+ if "reorderChartIds" in payload and "reorder_chart_ids" not in payload:
1875
+ payload["reorder_chart_ids"] = payload.pop("reorderChartIds")
1691
1876
  for key in ("remove_chart_ids", "reorder_chart_ids"):
1692
1877
  raw = payload.get(key)
1693
1878
  if isinstance(raw, list):
@@ -1696,8 +1881,8 @@ class ChartApplyRequest(StrictModel):
1696
1881
 
1697
1882
  @model_validator(mode="after")
1698
1883
  def validate_shape(self) -> "ChartApplyRequest":
1699
- if not self.upsert_charts and not self.remove_chart_ids and not self.reorder_chart_ids:
1700
- raise ValueError("chart apply requires at least one upsert, remove, or reorder operation")
1884
+ if not self.upsert_charts and not self.patch_charts and not self.remove_chart_ids and not self.reorder_chart_ids:
1885
+ raise ValueError("chart apply requires at least one upsert, patch, remove, or reorder operation")
1701
1886
  return self
1702
1887
 
1703
1888
 
@@ -1757,6 +1942,13 @@ class PortalViewRefPatch(StrictModel):
1757
1942
  view_key: str | None = None
1758
1943
  view_name: str | None = None
1759
1944
 
1945
+ @model_validator(mode="before")
1946
+ @classmethod
1947
+ def normalize_aliases(cls, value: Any) -> Any:
1948
+ if not isinstance(value, dict):
1949
+ return value
1950
+ return _normalize_builder_view_key_payload(dict(value))
1951
+
1760
1952
  @model_validator(mode="after")
1761
1953
  def validate_target(self) -> "PortalViewRefPatch":
1762
1954
  if not (self.view_key or self.view_name):
@@ -1944,6 +2136,7 @@ class ViewGetResponse(StrictModel):
1944
2136
  config: dict[str, Any] = Field(default_factory=dict)
1945
2137
  questions: list[dict[str, Any]] = Field(default_factory=list)
1946
2138
  associations: list[dict[str, Any]] = Field(default_factory=list)
2139
+ buttons_config: dict[str, Any] = Field(default_factory=dict)
1947
2140
  associated_resources_config: dict[str, Any] = Field(default_factory=dict)
1948
2141
 
1949
2142
 
@@ -2021,6 +2214,7 @@ class FlowPlanRequest(StrictModel):
2021
2214
  class ViewsPlanRequest(StrictModel):
2022
2215
  app_key: str
2023
2216
  upsert_views: list[ViewUpsertPatch] = Field(default_factory=list)
2217
+ patch_views: list[ViewPartialPatch] = Field(default_factory=list)
2024
2218
  remove_views: list[str] = Field(default_factory=list)
2025
2219
  preset: ViewsPreset | None = None
2026
2220