@josephyan/qingflow-app-builder-mcp 0.2.0-beta.7 → 0.2.0-beta.71

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.
Files changed (70) hide show
  1. package/README.md +5 -3
  2. package/docs/local-agent-install.md +21 -5
  3. package/npm/bin/qingflow-app-builder-mcp.mjs +1 -1
  4. package/npm/lib/runtime.mjs +168 -12
  5. package/package.json +1 -1
  6. package/pyproject.toml +4 -1
  7. package/skills/qingflow-app-builder/SKILL.md +155 -22
  8. package/skills/qingflow-app-builder/references/create-app.md +51 -21
  9. package/skills/qingflow-app-builder/references/environments.md +1 -1
  10. package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +123 -0
  11. package/skills/qingflow-app-builder/references/gotchas.md +28 -1
  12. package/skills/qingflow-app-builder/references/solution-playbooks.md +14 -12
  13. package/skills/qingflow-app-builder/references/tool-selection.md +47 -19
  14. package/skills/qingflow-app-builder/references/update-flow.md +112 -25
  15. package/skills/qingflow-app-builder/references/update-layout.md +11 -24
  16. package/skills/qingflow-app-builder/references/update-schema.md +1 -23
  17. package/skills/qingflow-app-builder/references/update-views.md +87 -21
  18. package/skills/qingflow-app-builder-code-integrations/SKILL.md +137 -0
  19. package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +4 -0
  20. package/skills/qingflow-app-builder-code-integrations/references/code-block.md +66 -0
  21. package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +77 -0
  22. package/src/qingflow_mcp/__init__.py +1 -1
  23. package/src/qingflow_mcp/backend_client.py +210 -0
  24. package/src/qingflow_mcp/builder_facade/models.py +1252 -3
  25. package/src/qingflow_mcp/builder_facade/service.py +11367 -2389
  26. package/src/qingflow_mcp/cli/__init__.py +1 -0
  27. package/src/qingflow_mcp/cli/commands/__init__.py +15 -0
  28. package/src/qingflow_mcp/cli/commands/app.py +40 -0
  29. package/src/qingflow_mcp/cli/commands/auth.py +78 -0
  30. package/src/qingflow_mcp/cli/commands/builder.py +515 -0
  31. package/src/qingflow_mcp/cli/commands/common.py +62 -0
  32. package/src/qingflow_mcp/cli/commands/imports.py +96 -0
  33. package/src/qingflow_mcp/cli/commands/record.py +304 -0
  34. package/src/qingflow_mcp/cli/commands/task.py +89 -0
  35. package/src/qingflow_mcp/cli/commands/workspace.py +33 -0
  36. package/src/qingflow_mcp/cli/context.py +48 -0
  37. package/src/qingflow_mcp/cli/formatters.py +355 -0
  38. package/src/qingflow_mcp/cli/json_io.py +50 -0
  39. package/src/qingflow_mcp/cli/main.py +149 -0
  40. package/src/qingflow_mcp/config.py +39 -0
  41. package/src/qingflow_mcp/import_store.py +121 -0
  42. package/src/qingflow_mcp/list_type_labels.py +24 -0
  43. package/src/qingflow_mcp/response_trim.py +668 -0
  44. package/src/qingflow_mcp/server.py +160 -18
  45. package/src/qingflow_mcp/server_app_builder.py +275 -68
  46. package/src/qingflow_mcp/server_app_user.py +219 -191
  47. package/src/qingflow_mcp/session_store.py +41 -1
  48. package/src/qingflow_mcp/solution/compiler/form_compiler.py +43 -4
  49. package/src/qingflow_mcp/solution/compiler/icon_utils.py +119 -45
  50. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +41 -2
  51. package/src/qingflow_mcp/solution/executor.py +107 -11
  52. package/src/qingflow_mcp/solution/spec_models.py +2 -0
  53. package/src/qingflow_mcp/tools/ai_builder_tools.py +2032 -127
  54. package/src/qingflow_mcp/tools/app_tools.py +419 -12
  55. package/src/qingflow_mcp/tools/approval_tools.py +571 -72
  56. package/src/qingflow_mcp/tools/auth_tools.py +398 -2
  57. package/src/qingflow_mcp/tools/code_block_tools.py +756 -0
  58. package/src/qingflow_mcp/tools/custom_button_tools.py +179 -0
  59. package/src/qingflow_mcp/tools/directory_tools.py +203 -31
  60. package/src/qingflow_mcp/tools/feedback_tools.py +230 -0
  61. package/src/qingflow_mcp/tools/file_tools.py +1 -0
  62. package/src/qingflow_mcp/tools/import_tools.py +2150 -0
  63. package/src/qingflow_mcp/tools/package_tools.py +18 -4
  64. package/src/qingflow_mcp/tools/portal_tools.py +31 -0
  65. package/src/qingflow_mcp/tools/qingbi_report_tools.py +109 -7
  66. package/src/qingflow_mcp/tools/record_tools.py +9894 -1104
  67. package/src/qingflow_mcp/tools/solution_tools.py +115 -3
  68. package/src/qingflow_mcp/tools/task_context_tools.py +2040 -0
  69. package/src/qingflow_mcp/tools/task_tools.py +376 -225
  70. package/src/qingflow_mcp/tools/workspace_tools.py +163 -19
@@ -24,10 +24,17 @@ class PublicFieldType(str, Enum):
24
24
  address = "address"
25
25
  attachment = "attachment"
26
26
  boolean = "boolean"
27
+ q_linker = "q_linker"
28
+ code_block = "code_block"
27
29
  relation = "relation"
28
30
  subtable = "subtable"
29
31
 
30
32
 
33
+ class PublicRelationMode(str, Enum):
34
+ single = "single"
35
+ multiple = "multiple"
36
+
37
+
31
38
  FIELD_TYPE_ALIASES: dict[str, PublicFieldType] = {
32
39
  "textarea": PublicFieldType.long_text,
33
40
  "amount": PublicFieldType.amount,
@@ -41,6 +48,30 @@ FIELD_TYPE_ALIASES: dict[str, PublicFieldType] = {
41
48
  "multi_select": PublicFieldType.multi_select,
42
49
  "multi-select": PublicFieldType.multi_select,
43
50
  "departments": PublicFieldType.department,
51
+ "qlinker": PublicFieldType.q_linker,
52
+ "q_linker": PublicFieldType.q_linker,
53
+ "codeblock": PublicFieldType.code_block,
54
+ "code_block": PublicFieldType.code_block,
55
+ }
56
+
57
+ FIELD_TYPE_ID_ALIASES: dict[int, PublicFieldType] = {
58
+ 2: PublicFieldType.text,
59
+ 3: PublicFieldType.long_text,
60
+ 4: PublicFieldType.date,
61
+ 5: PublicFieldType.member,
62
+ 6: PublicFieldType.email,
63
+ 7: PublicFieldType.phone,
64
+ 8: PublicFieldType.number,
65
+ 10: PublicFieldType.boolean,
66
+ 11: PublicFieldType.single_select,
67
+ 12: PublicFieldType.multi_select,
68
+ 13: PublicFieldType.attachment,
69
+ 20: PublicFieldType.q_linker,
70
+ 26: PublicFieldType.code_block,
71
+ 18: PublicFieldType.subtable,
72
+ 21: PublicFieldType.address,
73
+ 22: PublicFieldType.department,
74
+ 25: PublicFieldType.relation,
44
75
  }
45
76
 
46
77
 
@@ -48,6 +79,32 @@ class PublicViewType(str, Enum):
48
79
  table = "table"
49
80
  card = "card"
50
81
  board = "board"
82
+ gantt = "gantt"
83
+
84
+
85
+ class PublicButtonTriggerAction(str, Enum):
86
+ add_data = "addData"
87
+ link = "link"
88
+ qrobot = "qRobot"
89
+ wings = "wings"
90
+
91
+
92
+ class PublicViewButtonType(str, Enum):
93
+ system = "SYSTEM"
94
+ custom = "CUSTOM"
95
+
96
+
97
+ class PublicViewButtonConfigType(str, Enum):
98
+ top = "TOP"
99
+ detail = "DETAIL"
100
+
101
+
102
+ class PublicChartType(str, Enum):
103
+ target = "target"
104
+ pie = "pie"
105
+ bar = "bar"
106
+ line = "line"
107
+ table = "table"
51
108
 
52
109
 
53
110
  class LayoutApplyMode(str, Enum):
@@ -69,6 +126,7 @@ class FlowPreset(str, Enum):
69
126
  class ViewsPreset(str, Enum):
70
127
  default_table = "default_table"
71
128
  status_board = "status_board"
129
+ default_gantt = "default_gantt"
72
130
 
73
131
 
74
132
  class PublicFlowNodeType(str, Enum):
@@ -82,6 +140,143 @@ class PublicFlowNodeType(str, Enum):
82
140
  end = "end"
83
141
 
84
142
 
143
+ class FlowConditionOperator(str, Enum):
144
+ eq = "eq"
145
+ neq = "neq"
146
+ in_ = "in"
147
+ contains = "contains"
148
+ gte = "gte"
149
+ lte = "lte"
150
+ is_empty = "is_empty"
151
+ not_empty = "not_empty"
152
+
153
+
154
+ class ViewFilterOperator(str, Enum):
155
+ eq = "eq"
156
+ neq = "neq"
157
+ in_ = "in"
158
+ contains = "contains"
159
+ gte = "gte"
160
+ lte = "lte"
161
+ is_empty = "is_empty"
162
+ not_empty = "not_empty"
163
+
164
+
165
+ class FlowAssigneePatch(StrictModel):
166
+ role_ids: list[int] = Field(default_factory=list)
167
+ role_names: list[str] = Field(default_factory=list)
168
+ member_uids: list[int] = Field(default_factory=list)
169
+ member_emails: list[str] = Field(default_factory=list)
170
+ member_names: list[str] = Field(default_factory=list)
171
+ include_sub_departs: bool | None = None
172
+
173
+
174
+ class FlowNodePermissionsPatch(StrictModel):
175
+ editable_fields: list[str] = Field(default_factory=list)
176
+
177
+
178
+ class FlowConditionRulePatch(StrictModel):
179
+ field_name: str = Field(validation_alias=AliasChoices("field_name", "fieldName", "field", "name"))
180
+ operator: FlowConditionOperator = Field(validation_alias=AliasChoices("operator", "op"))
181
+ values: list[Any] = Field(default_factory=list)
182
+
183
+ @model_validator(mode="before")
184
+ @classmethod
185
+ def normalize_aliases(cls, value: Any) -> Any:
186
+ if not isinstance(value, dict):
187
+ return value
188
+ payload = dict(value)
189
+ if "value" in payload and "values" not in payload:
190
+ payload["values"] = [payload.pop("value")]
191
+ raw_operator = payload.get("operator", payload.get("op"))
192
+ if isinstance(raw_operator, str):
193
+ normalized = raw_operator.strip().lower()
194
+ operator_aliases = {
195
+ "equals": FlowConditionOperator.eq.value,
196
+ "equal": FlowConditionOperator.eq.value,
197
+ "=": FlowConditionOperator.eq.value,
198
+ "not_equals": FlowConditionOperator.neq.value,
199
+ "not_equal": FlowConditionOperator.neq.value,
200
+ "!=": FlowConditionOperator.neq.value,
201
+ ">=": FlowConditionOperator.gte.value,
202
+ "<=": FlowConditionOperator.lte.value,
203
+ "any_of": FlowConditionOperator.in_.value,
204
+ "one_of": FlowConditionOperator.in_.value,
205
+ "between_any": FlowConditionOperator.in_.value,
206
+ "empty": FlowConditionOperator.is_empty.value,
207
+ "is blank": FlowConditionOperator.is_empty.value,
208
+ "blank": FlowConditionOperator.is_empty.value,
209
+ "not_empty": FlowConditionOperator.not_empty.value,
210
+ "not blank": FlowConditionOperator.not_empty.value,
211
+ }
212
+ if normalized in operator_aliases:
213
+ payload["operator"] = operator_aliases[normalized]
214
+ elif "operator" not in payload:
215
+ payload["operator"] = normalized
216
+ payload.pop("op", None)
217
+ return payload
218
+
219
+ @model_validator(mode="after")
220
+ def validate_shape(self) -> "FlowConditionRulePatch":
221
+ if self.operator in {FlowConditionOperator.is_empty, FlowConditionOperator.not_empty}:
222
+ self.values = []
223
+ return self
224
+ if not self.values:
225
+ raise ValueError("condition rule requires values")
226
+ return self
227
+
228
+
229
+ class ViewFilterRulePatch(StrictModel):
230
+ field_name: str = Field(validation_alias=AliasChoices("field_name", "fieldName", "field", "name"))
231
+ operator: ViewFilterOperator = Field(validation_alias=AliasChoices("operator", "op"))
232
+ values: list[Any] = Field(default_factory=list)
233
+
234
+ @model_validator(mode="before")
235
+ @classmethod
236
+ def normalize_aliases(cls, value: Any) -> Any:
237
+ if not isinstance(value, dict):
238
+ return value
239
+ payload = dict(value)
240
+ if "value" in payload and "values" not in payload:
241
+ payload["values"] = [payload.pop("value")]
242
+ raw_operator = payload.get("operator", payload.get("op"))
243
+ if isinstance(raw_operator, str):
244
+ normalized = raw_operator.strip().lower()
245
+ operator_aliases = {
246
+ "equals": ViewFilterOperator.eq.value,
247
+ "equal": ViewFilterOperator.eq.value,
248
+ "=": ViewFilterOperator.eq.value,
249
+ "not_equals": ViewFilterOperator.neq.value,
250
+ "not_equal": ViewFilterOperator.neq.value,
251
+ "!=": ViewFilterOperator.neq.value,
252
+ ">=": ViewFilterOperator.gte.value,
253
+ "<=": ViewFilterOperator.lte.value,
254
+ "any_of": ViewFilterOperator.in_.value,
255
+ "one_of": ViewFilterOperator.in_.value,
256
+ "between_any": ViewFilterOperator.in_.value,
257
+ "empty": ViewFilterOperator.is_empty.value,
258
+ "is blank": ViewFilterOperator.is_empty.value,
259
+ "blank": ViewFilterOperator.is_empty.value,
260
+ "not_empty": ViewFilterOperator.not_empty.value,
261
+ "not blank": ViewFilterOperator.not_empty.value,
262
+ }
263
+ if normalized in operator_aliases:
264
+ payload["operator"] = operator_aliases[normalized]
265
+ elif "operator" not in payload:
266
+ payload["operator"] = normalized
267
+ payload.pop("op", None)
268
+ return payload
269
+
270
+ @model_validator(mode="after")
271
+ def validate_shape(self) -> "ViewFilterRulePatch":
272
+ if self.operator in {ViewFilterOperator.is_empty, ViewFilterOperator.not_empty}:
273
+ self.values = []
274
+ return self
275
+ if not self.values:
276
+ raise ValueError("view filter rule requires values")
277
+ return self
278
+
279
+
85
280
  class FieldSelector(StrictModel):
86
281
  field_id: str | None = None
87
282
  que_id: int | None = None
@@ -94,6 +289,260 @@ class FieldSelector(StrictModel):
94
289
  return self
95
290
 
96
291
 
292
+ class CodeBlockAliasPathPatch(StrictModel):
293
+ alias_name: str = Field(validation_alias=AliasChoices("alias_name", "aliasName"))
294
+ alias_path: str = Field(validation_alias=AliasChoices("alias_path", "aliasPath"))
295
+ alias_type: int = Field(default=1, validation_alias=AliasChoices("alias_type", "aliasType"))
296
+ alias_id: int | None = Field(default=None, validation_alias=AliasChoices("alias_id", "aliasId"))
297
+ sub_alias: list["CodeBlockAliasPathPatch"] = Field(
298
+ default_factory=list,
299
+ validation_alias=AliasChoices("sub_alias", "subAlias"),
300
+ )
301
+
302
+
303
+ class CodeBlockConfigPatch(StrictModel):
304
+ config_mode: int = Field(default=1, validation_alias=AliasChoices("config_mode", "configMode"))
305
+ code_content: str = Field(default="", validation_alias=AliasChoices("code_content", "codeContent"))
306
+ being_hide_on_form: bool = Field(
307
+ default=False,
308
+ validation_alias=AliasChoices("being_hide_on_form", "beingHideOnForm"),
309
+ )
310
+ result_alias_path: list[CodeBlockAliasPathPatch] = Field(
311
+ default_factory=list,
312
+ validation_alias=AliasChoices("result_alias_path", "resultAliasPath", "alias_config", "aliasConfig"),
313
+ )
314
+
315
+
316
+ class CodeBlockInputBindingPatch(StrictModel):
317
+ field: FieldSelector
318
+ var: str | None = None
319
+
320
+ @model_validator(mode="before")
321
+ @classmethod
322
+ def normalize_aliases(cls, value: Any) -> Any:
323
+ if isinstance(value, str):
324
+ return {"field": {"name": value}}
325
+ if not isinstance(value, dict):
326
+ return value
327
+ payload = dict(value)
328
+ raw_field = payload.get("field")
329
+ if isinstance(raw_field, str):
330
+ payload["field"] = {"name": raw_field}
331
+ elif raw_field is None:
332
+ for key in ("field_name", "fieldName", "name", "title", "label"):
333
+ raw_value = payload.get(key)
334
+ if isinstance(raw_value, str) and raw_value.strip():
335
+ payload["field"] = {"name": raw_value}
336
+ break
337
+ return payload
338
+
339
+
340
+ class QLinkerInputSource(str, Enum):
341
+ query_param = "query_param"
342
+ header = "header"
343
+ url_encoded = "url_encoded"
344
+ json_path = "json_path"
345
+
346
+
347
+ class QLinkerKeyValuePatch(StrictModel):
348
+ key: str
349
+ value: str | None = None
350
+
351
+
352
+ class QLinkerAliasPathPatch(StrictModel):
353
+ alias_name: str = Field(validation_alias=AliasChoices("alias_name", "aliasName"))
354
+ alias_path: str = Field(validation_alias=AliasChoices("alias_path", "aliasPath"))
355
+ alias_id: int | None = Field(default=None, validation_alias=AliasChoices("alias_id", "aliasId"))
356
+
357
+
358
+ class RemoteLookupConfigPatch(StrictModel):
359
+ config_mode: int = Field(default=1, validation_alias=AliasChoices("config_mode", "configMode"))
360
+ url: str = ""
361
+ method: str = "GET"
362
+ headers: list[QLinkerKeyValuePatch] = Field(default_factory=list)
363
+ body_type: int = Field(default=1, validation_alias=AliasChoices("body_type", "bodyType"))
364
+ url_encoded_value: list[QLinkerKeyValuePatch] = Field(
365
+ default_factory=list,
366
+ validation_alias=AliasChoices("url_encoded_value", "urlEncodedValue"),
367
+ )
368
+ json_value: str | None = Field(default=None, validation_alias=AliasChoices("json_value", "jsonValue"))
369
+ xml_value: str | None = Field(default=None, validation_alias=AliasChoices("xml_value", "xmlValue"))
370
+ result_type: int = Field(default=1, validation_alias=AliasChoices("result_type", "resultType"))
371
+ result_format_path: list[QLinkerAliasPathPatch] = Field(
372
+ default_factory=list,
373
+ validation_alias=AliasChoices("result_format_path", "resultFormatPath"),
374
+ )
375
+ query_params: list[QLinkerKeyValuePatch] = Field(
376
+ default_factory=list,
377
+ validation_alias=AliasChoices("query_params", "queryParams"),
378
+ )
379
+ auto_trigger: bool | None = Field(default=None, validation_alias=AliasChoices("auto_trigger", "autoTrigger"))
380
+ custom_button_text_enabled: bool | None = Field(
381
+ default=None,
382
+ validation_alias=AliasChoices("custom_button_text_enabled", "customButtonTextEnabled", "custom_btn_text_status", "customBtnTextStatus"),
383
+ )
384
+ custom_button_text: str | None = Field(
385
+ default=None,
386
+ validation_alias=AliasChoices("custom_button_text", "customButtonText", "custom_btn_text", "customBtnText"),
387
+ )
388
+ being_insert_value_directly: bool | None = Field(
389
+ default=None,
390
+ validation_alias=AliasChoices("being_insert_value_directly", "beingInsertValueDirectly"),
391
+ )
392
+ being_hide_on_form: bool | None = Field(
393
+ default=None,
394
+ validation_alias=AliasChoices("being_hide_on_form", "beingHideOnForm"),
395
+ )
396
+
397
+
398
+ class QLinkerInputBindingPatch(StrictModel):
399
+ field: FieldSelector
400
+ key: str
401
+ source: QLinkerInputSource
402
+
403
+ @model_validator(mode="before")
404
+ @classmethod
405
+ def normalize_aliases(cls, value: Any) -> Any:
406
+ if isinstance(value, str):
407
+ return {"field": {"name": value}, "key": value, "source": QLinkerInputSource.query_param.value}
408
+ if not isinstance(value, dict):
409
+ return value
410
+ payload = dict(value)
411
+ raw_field = payload.get("field")
412
+ if isinstance(raw_field, str):
413
+ payload["field"] = {"name": raw_field}
414
+ elif raw_field is None:
415
+ for key in ("field_name", "fieldName", "name", "title", "label"):
416
+ raw_value = payload.get(key)
417
+ if isinstance(raw_value, str) and raw_value.strip():
418
+ payload["field"] = {"name": raw_value}
419
+ break
420
+ raw_source = payload.get("source")
421
+ if isinstance(raw_source, str):
422
+ payload["source"] = raw_source.strip().lower()
423
+ return payload
424
+
425
+
426
+ class QLinkerOutputBindingPatch(StrictModel):
427
+ alias: str
428
+ path: str
429
+ target_field: FieldSelector = Field(validation_alias=AliasChoices("target_field", "targetField"))
430
+
431
+ @model_validator(mode="before")
432
+ @classmethod
433
+ def normalize_aliases(cls, value: Any) -> Any:
434
+ if not isinstance(value, dict):
435
+ return value
436
+ payload = dict(value)
437
+ if "alias_name" in payload and "alias" not in payload:
438
+ payload["alias"] = payload.pop("alias_name")
439
+ if "aliasName" in payload and "alias" not in payload:
440
+ payload["alias"] = payload.pop("aliasName")
441
+ if "alias_path" in payload and "path" not in payload:
442
+ payload["path"] = payload.pop("alias_path")
443
+ if "aliasPath" in payload and "path" not in payload:
444
+ payload["path"] = payload.pop("aliasPath")
445
+ raw_target = payload.get("target_field", payload.get("targetField"))
446
+ if isinstance(raw_target, str):
447
+ payload["target_field"] = {"name": raw_target}
448
+ return payload
449
+
450
+
451
+ class QLinkerRequestPatch(StrictModel):
452
+ url: str = ""
453
+ method: str = "GET"
454
+ headers: list[QLinkerKeyValuePatch] = Field(default_factory=list)
455
+ query_params: list[QLinkerKeyValuePatch] = Field(
456
+ default_factory=list,
457
+ validation_alias=AliasChoices("query_params", "queryParams"),
458
+ )
459
+ body_type: int = Field(default=1, validation_alias=AliasChoices("body_type", "bodyType"))
460
+ url_encoded_value: list[QLinkerKeyValuePatch] = Field(
461
+ default_factory=list,
462
+ validation_alias=AliasChoices("url_encoded_value", "urlEncodedValue"),
463
+ )
464
+ json_value: str | None = Field(default=None, validation_alias=AliasChoices("json_value", "jsonValue"))
465
+ xml_value: str | None = Field(default=None, validation_alias=AliasChoices("xml_value", "xmlValue"))
466
+ result_type: int = Field(default=1, validation_alias=AliasChoices("result_type", "resultType"))
467
+ auto_trigger: bool | None = Field(default=None, validation_alias=AliasChoices("auto_trigger", "autoTrigger"))
468
+ custom_button_text_enabled: bool | None = Field(
469
+ default=None,
470
+ validation_alias=AliasChoices("custom_button_text_enabled", "customButtonTextEnabled", "custom_btn_text_status", "customBtnTextStatus"),
471
+ )
472
+ custom_button_text: str | None = Field(
473
+ default=None,
474
+ validation_alias=AliasChoices("custom_button_text", "customButtonText", "custom_btn_text", "customBtnText"),
475
+ )
476
+ being_insert_value_directly: bool | None = Field(
477
+ default=None,
478
+ validation_alias=AliasChoices("being_insert_value_directly", "beingInsertValueDirectly"),
479
+ )
480
+ being_hide_on_form: bool | None = Field(
481
+ default=None,
482
+ validation_alias=AliasChoices("being_hide_on_form", "beingHideOnForm"),
483
+ )
484
+
485
+
486
+ class QLinkerBindingPatch(StrictModel):
487
+ inputs: list[QLinkerInputBindingPatch] = Field(default_factory=list)
488
+ request: QLinkerRequestPatch
489
+ outputs: list[QLinkerOutputBindingPatch] = Field(default_factory=list)
490
+
491
+
492
+ class CodeBlockOutputBindingPatch(StrictModel):
493
+ alias: str
494
+ path: str
495
+ target_field: FieldSelector = Field(
496
+ validation_alias=AliasChoices("target_field", "targetField"),
497
+ )
498
+
499
+ @model_validator(mode="before")
500
+ @classmethod
501
+ def normalize_aliases(cls, value: Any) -> Any:
502
+ if not isinstance(value, dict):
503
+ return value
504
+ payload = dict(value)
505
+ if "alias_name" in payload and "alias" not in payload:
506
+ payload["alias"] = payload.pop("alias_name")
507
+ if "aliasName" in payload and "alias" not in payload:
508
+ payload["alias"] = payload.pop("aliasName")
509
+ if "alias_path" in payload and "path" not in payload:
510
+ payload["path"] = payload.pop("alias_path")
511
+ if "aliasPath" in payload and "path" not in payload:
512
+ payload["path"] = payload.pop("aliasPath")
513
+ raw_target = payload.get("target_field", payload.get("targetField"))
514
+ if isinstance(raw_target, str):
515
+ payload["target_field"] = {"name": raw_target}
516
+ return payload
517
+
518
+
519
+ class CodeBlockBindingPatch(StrictModel):
520
+ inputs: list[CodeBlockInputBindingPatch] = Field(default_factory=list)
521
+ code: str = ""
522
+ auto_trigger: bool | None = Field(default=None, validation_alias=AliasChoices("auto_trigger", "autoTrigger"))
523
+ custom_button_text_enabled: bool | None = Field(
524
+ default=None,
525
+ validation_alias=AliasChoices("custom_button_text_enabled", "customButtonTextEnabled", "custom_btn_text_status", "customBtnTextStatus"),
526
+ )
527
+ custom_button_text: str | None = Field(
528
+ default=None,
529
+ validation_alias=AliasChoices("custom_button_text", "customButtonText", "custom_btn_text", "customBtnText"),
530
+ )
531
+ outputs: list[CodeBlockOutputBindingPatch] = Field(default_factory=list)
532
+
533
+ @model_validator(mode="before")
534
+ @classmethod
535
+ def normalize_aliases(cls, value: Any) -> Any:
536
+ if not isinstance(value, dict):
537
+ return value
538
+ payload = dict(value)
539
+ if "code_content" in payload and "code" not in payload:
540
+ payload["code"] = payload.pop("code_content")
541
+ if "codeContent" in payload and "code" not in payload:
542
+ payload["code"] = payload.pop("codeContent")
543
+ return payload
544
+
545
+
97
546
  class FieldPatch(StrictModel):
98
547
  name: str = Field(validation_alias=AliasChoices("name", "title", "label"))
99
548
  type: PublicFieldType
@@ -101,6 +550,34 @@ class FieldPatch(StrictModel):
101
550
  description: str | None = None
102
551
  options: list[str] = Field(default_factory=list)
103
552
  target_app_key: str | None = None
553
+ display_field: FieldSelector | None = None
554
+ visible_fields: list[FieldSelector] = Field(default_factory=list)
555
+ relation_mode: PublicRelationMode | None = Field(default=None, validation_alias=AliasChoices("relation_mode", "relationMode", "selection_mode", "selectionMode"))
556
+ remote_lookup_config: RemoteLookupConfigPatch | None = Field(
557
+ default=None,
558
+ validation_alias=AliasChoices("remote_lookup_config", "remoteLookupConfig"),
559
+ )
560
+ q_linker_binding: QLinkerBindingPatch | None = Field(
561
+ default=None,
562
+ validation_alias=AliasChoices("q_linker_binding", "qLinkerBinding"),
563
+ )
564
+ code_block_config: CodeBlockConfigPatch | None = Field(
565
+ default=None,
566
+ validation_alias=AliasChoices("code_block_config", "codeBlockConfig"),
567
+ )
568
+ code_block_binding: CodeBlockBindingPatch | None = Field(
569
+ default=None,
570
+ validation_alias=AliasChoices("code_block_binding", "codeBlockBinding"),
571
+ )
572
+ auto_trigger: bool | None = Field(default=None, validation_alias=AliasChoices("auto_trigger", "autoTrigger"))
573
+ custom_button_text_enabled: bool | None = Field(
574
+ default=None,
575
+ validation_alias=AliasChoices("custom_button_text_enabled", "customButtonTextEnabled", "custom_btn_text_status", "customBtnTextStatus"),
576
+ )
577
+ custom_button_text: str | None = Field(
578
+ default=None,
579
+ validation_alias=AliasChoices("custom_button_text", "customButtonText", "custom_btn_text", "customBtnText"),
580
+ )
104
581
  subfields: list["FieldPatch"] = Field(default_factory=list)
105
582
 
106
583
  @model_validator(mode="after")
@@ -109,6 +586,21 @@ class FieldPatch(StrictModel):
109
586
  raise ValueError("relation field requires target_app_key")
110
587
  if self.type != PublicFieldType.relation and self.target_app_key:
111
588
  raise ValueError("target_app_key is only allowed for relation fields")
589
+ if self.type != PublicFieldType.relation and (self.display_field is not None or self.visible_fields or self.relation_mode is not None):
590
+ raise ValueError("display_field, visible_fields, and relation_mode are only allowed for relation fields")
591
+ if self.type != PublicFieldType.q_linker and (
592
+ self.remote_lookup_config is not None
593
+ or self.q_linker_binding is not None
594
+ ):
595
+ raise ValueError("remote_lookup_config and q_linker_binding are only allowed for q_linker fields")
596
+ if self.type != PublicFieldType.code_block and (
597
+ self.code_block_config is not None
598
+ or self.code_block_binding is not None
599
+ or self.auto_trigger is not None
600
+ or self.custom_button_text_enabled is not None
601
+ or self.custom_button_text is not None
602
+ ):
603
+ raise ValueError("code_block_config, code_block_binding, auto_trigger, custom_button_text_enabled, and custom_button_text are only allowed for code_block fields")
112
604
  if self.type == PublicFieldType.subtable and not self.subfields:
113
605
  raise ValueError("subtable field requires subfields")
114
606
  if self.type != PublicFieldType.subtable and self.subfields:
@@ -128,12 +620,55 @@ class FieldMutation(StrictModel):
128
620
  description: str | None = None
129
621
  options: list[str] | None = None
130
622
  target_app_key: str | None = None
623
+ display_field: FieldSelector | None = None
624
+ visible_fields: list[FieldSelector] | None = None
625
+ relation_mode: PublicRelationMode | None = Field(default=None, validation_alias=AliasChoices("relation_mode", "relationMode", "selection_mode", "selectionMode"))
626
+ remote_lookup_config: RemoteLookupConfigPatch | None = Field(
627
+ default=None,
628
+ validation_alias=AliasChoices("remote_lookup_config", "remoteLookupConfig"),
629
+ )
630
+ q_linker_binding: QLinkerBindingPatch | None = Field(
631
+ default=None,
632
+ validation_alias=AliasChoices("q_linker_binding", "qLinkerBinding"),
633
+ )
634
+ code_block_config: CodeBlockConfigPatch | None = Field(
635
+ default=None,
636
+ validation_alias=AliasChoices("code_block_config", "codeBlockConfig"),
637
+ )
638
+ code_block_binding: CodeBlockBindingPatch | None = Field(
639
+ default=None,
640
+ validation_alias=AliasChoices("code_block_binding", "codeBlockBinding"),
641
+ )
642
+ auto_trigger: bool | None = Field(default=None, validation_alias=AliasChoices("auto_trigger", "autoTrigger"))
643
+ custom_button_text_enabled: bool | None = Field(
644
+ default=None,
645
+ validation_alias=AliasChoices("custom_button_text_enabled", "customButtonTextEnabled", "custom_btn_text_status", "customBtnTextStatus"),
646
+ )
647
+ custom_button_text: str | None = Field(
648
+ default=None,
649
+ validation_alias=AliasChoices("custom_button_text", "customButtonText", "custom_btn_text", "customBtnText"),
650
+ )
131
651
  subfields: list[FieldPatch] | None = None
132
652
 
133
653
  @model_validator(mode="after")
134
654
  def validate_shape(self) -> "FieldMutation":
135
655
  if self.type == PublicFieldType.relation and not self.target_app_key:
136
656
  raise ValueError("relation field requires target_app_key")
657
+ if self.type is not None and self.type != PublicFieldType.relation and (self.display_field is not None or self.visible_fields or self.relation_mode is not None):
658
+ raise ValueError("display_field, visible_fields, and relation_mode are only allowed for relation fields")
659
+ if self.type is not None and self.type != PublicFieldType.q_linker and (
660
+ self.remote_lookup_config is not None
661
+ or self.q_linker_binding is not None
662
+ ):
663
+ raise ValueError("remote_lookup_config and q_linker_binding are only allowed for q_linker fields")
664
+ if self.type is not None and self.type != PublicFieldType.code_block and (
665
+ self.code_block_config is not None
666
+ or self.code_block_binding is not None
667
+ or self.auto_trigger is not None
668
+ or self.custom_button_text_enabled is not None
669
+ or self.custom_button_text is not None
670
+ ):
671
+ raise ValueError("code_block_config, code_block_binding, auto_trigger, custom_button_text_enabled, and custom_button_text are only allowed for code_block fields")
137
672
  if self.type == PublicFieldType.subtable and not self.subfields:
138
673
  raise ValueError("subtable field requires subfields")
139
674
  return self
@@ -161,15 +696,72 @@ class FieldRemovePatch(StrictModel):
161
696
  return self
162
697
 
163
698
 
699
+ def _coerce_layout_columns(value: Any) -> int | None:
700
+ if isinstance(value, bool):
701
+ return None
702
+ if isinstance(value, int):
703
+ return value if value > 0 else None
704
+ if isinstance(value, str):
705
+ stripped = value.strip()
706
+ if stripped.isdigit():
707
+ parsed = int(stripped)
708
+ return parsed if parsed > 0 else None
709
+ return None
710
+
711
+
712
+ def _normalize_layout_rows(value: Any, *, columns: int | None = None) -> Any:
713
+ if not isinstance(value, list):
714
+ return value
715
+ if value and all(isinstance(item, list) for item in value):
716
+ return value
717
+ if not value:
718
+ return []
719
+ width = columns if columns and columns > 0 else None
720
+ if width is None:
721
+ return [list(value)]
722
+ return [list(value[index : index + width]) for index in range(0, len(value), width) if value[index : index + width]]
723
+
724
+
164
725
  class LayoutSectionPatch(StrictModel):
165
- section_id: str | None = Field(default=None, validation_alias=AliasChoices("section_id", "sectionId"))
166
- title: str
167
- rows: list[list[str]] = Field(default_factory=list)
726
+ type: str | None = Field(default=None, validation_alias=AliasChoices("type", "kind", "block_type", "blockType"))
727
+ section_id: str | None = Field(default=None, validation_alias=AliasChoices("section_id", "sectionId", "paragraph_id", "paragraphId"))
728
+ title: str = Field(validation_alias=AliasChoices("title", "name", "paragraph_title", "paragraphTitle"))
729
+ rows: list[list[Any]] = Field(default_factory=list)
730
+
731
+ @model_validator(mode="before")
732
+ @classmethod
733
+ def normalize_aliases(cls, value: Any) -> Any:
734
+ if not isinstance(value, dict):
735
+ return value
736
+ payload = dict(value)
737
+ if "name" in payload and "title" not in payload:
738
+ payload["title"] = payload.pop("name")
739
+ if "paragraph_title" in payload and "title" not in payload:
740
+ payload["title"] = payload.pop("paragraph_title")
741
+ if "paragraphTitle" in payload and "title" not in payload:
742
+ payload["title"] = payload.pop("paragraphTitle")
743
+ shorthand: Any | None = None
744
+ if "rows" not in payload:
745
+ if "fields" in payload:
746
+ shorthand = payload.pop("fields")
747
+ elif "field_ids" in payload:
748
+ shorthand = payload.pop("field_ids")
749
+ if shorthand is not None:
750
+ payload["rows"] = _normalize_layout_rows(
751
+ shorthand,
752
+ columns=_coerce_layout_columns(payload.pop("columns", None)),
753
+ )
754
+ return payload
168
755
 
169
756
  @model_validator(mode="after")
170
757
  def validate_rows(self) -> "LayoutSectionPatch":
758
+ if self.type is not None and str(self.type).strip().lower() != "paragraph":
759
+ raise ValueError("layout section type must be 'paragraph'")
171
760
  if not self.rows:
172
761
  raise ValueError("section rows must be a non-empty list")
762
+ for row in self.rows:
763
+ if not isinstance(row, list) or not row:
764
+ raise ValueError("section rows must be a non-empty list")
173
765
  if not self.section_id:
174
766
  self.section_id = _slugify_title(self.title)
175
767
  return self
@@ -179,8 +771,54 @@ class FlowNodePatch(StrictModel):
179
771
  id: str
180
772
  type: PublicFlowNodeType
181
773
  name: str
774
+ assignees: FlowAssigneePatch = Field(default_factory=FlowAssigneePatch)
775
+ permissions: FlowNodePermissionsPatch = Field(default_factory=FlowNodePermissionsPatch)
776
+ conditions: list[FlowConditionRulePatch] = Field(default_factory=list)
777
+ condition_groups: list[list[FlowConditionRulePatch]] = Field(default_factory=list)
182
778
  config: dict[str, Any] = Field(default_factory=dict)
183
779
 
780
+ @model_validator(mode="before")
781
+ @classmethod
782
+ def normalize_aliases(cls, value: Any) -> Any:
783
+ if not isinstance(value, dict):
784
+ return value
785
+ payload = dict(value)
786
+ assignees = dict(payload.get("assignees") or {})
787
+ permissions = dict(payload.get("permissions") or {})
788
+
789
+ for key in ("role_ids", "role_names", "member_uids", "member_emails", "member_names", "include_sub_departs"):
790
+ if key in payload and key not in assignees:
791
+ assignees[key] = payload.pop(key)
792
+ for key in ("editable_fields",):
793
+ if key in payload and key not in permissions:
794
+ permissions[key] = payload.pop(key)
795
+ if "filters" in payload and "conditions" not in payload:
796
+ payload["conditions"] = payload.pop("filters")
797
+ if "rules" in payload and "conditions" not in payload:
798
+ payload["conditions"] = payload.pop("rules")
799
+ if "conditionRules" in payload and "condition_groups" not in payload:
800
+ payload["condition_groups"] = payload.pop("conditionRules")
801
+ if "conditionGroups" in payload and "condition_groups" not in payload:
802
+ payload["condition_groups"] = payload.pop("conditionGroups")
803
+ if "owners" in payload and "member_names" not in assignees:
804
+ assignees["member_names"] = payload.pop("owners")
805
+ if "approvers" in payload and "role_names" not in assignees:
806
+ assignees["role_names"] = payload.pop("approvers")
807
+ if assignees:
808
+ payload["assignees"] = assignees
809
+ if permissions:
810
+ payload["permissions"] = permissions
811
+ return payload
812
+
813
+ @model_validator(mode="after")
814
+ def validate_branch_conditions(self) -> "FlowNodePatch":
815
+ if self.conditions:
816
+ self.condition_groups = [list(self.conditions), *self.condition_groups]
817
+ self.conditions = []
818
+ if self.type != PublicFlowNodeType.condition and self.condition_groups:
819
+ raise ValueError("condition_groups are only allowed on condition nodes")
820
+ return self
821
+
184
822
 
185
823
  class FlowTransitionPatch(StrictModel):
186
824
  source: str = Field(alias="from")
@@ -189,9 +827,15 @@ class FlowTransitionPatch(StrictModel):
189
827
 
190
828
  class ViewUpsertPatch(StrictModel):
191
829
  name: str
830
+ view_key: str | None = Field(default=None, validation_alias=AliasChoices("view_key", "viewKey"))
192
831
  type: PublicViewType
193
832
  columns: list[str] = Field(default_factory=list)
194
833
  group_by: str | None = None
834
+ filters: list[ViewFilterRulePatch] = Field(default_factory=list)
835
+ start_field: str | None = Field(default=None, validation_alias=AliasChoices("start_field", "startField"))
836
+ end_field: str | None = Field(default=None, validation_alias=AliasChoices("end_field", "endField"))
837
+ title_field: str | None = Field(default=None, validation_alias=AliasChoices("title_field", "titleField"))
838
+ buttons: list["ViewButtonBindingPatch"] | None = None
195
839
 
196
840
  @model_validator(mode="before")
197
841
  @classmethod
@@ -201,6 +845,14 @@ class ViewUpsertPatch(StrictModel):
201
845
  payload = dict(value)
202
846
  if "fields" in payload and "columns" not in payload:
203
847
  payload["columns"] = payload.pop("fields")
848
+ if "column_names" in payload and "columns" not in payload:
849
+ payload["columns"] = payload.pop("column_names")
850
+ if "columnNames" in payload and "columns" not in payload:
851
+ payload["columns"] = payload.pop("columnNames")
852
+ if "filter_rules" in payload and "filters" not in payload:
853
+ payload["filters"] = payload.pop("filter_rules")
854
+ if "filterRules" in payload and "filters" not in payload:
855
+ payload["filters"] = payload.pop("filterRules")
204
856
  raw_type = payload.get("type")
205
857
  if isinstance(raw_type, str):
206
858
  normalized = raw_type.strip().lower()
@@ -210,6 +862,8 @@ class ViewUpsertPatch(StrictModel):
210
862
  payload["type"] = "card"
211
863
  elif normalized == "kanban":
212
864
  payload["type"] = "board"
865
+ elif normalized == "ganttview":
866
+ payload["type"] = "gantt"
213
867
  return payload
214
868
 
215
869
  @model_validator(mode="after")
@@ -218,6 +872,466 @@ class ViewUpsertPatch(StrictModel):
218
872
  raise ValueError("table/card views require columns")
219
873
  if self.type == PublicViewType.board and not self.group_by:
220
874
  raise ValueError("board view requires group_by")
875
+ if self.type == PublicViewType.gantt and not (self.start_field and self.end_field):
876
+ raise ValueError("gantt view requires start_field and end_field")
877
+ return self
878
+
879
+
880
+ class CustomButtonJudgeValuePatch(StrictModel):
881
+ id: int | str | None = None
882
+ value: Any | None = None
883
+
884
+ @model_validator(mode="before")
885
+ @classmethod
886
+ def normalize_aliases(cls, value: Any) -> Any:
887
+ if not isinstance(value, dict):
888
+ return value
889
+ payload = dict(value)
890
+ if "opt_id" in payload and "id" not in payload:
891
+ payload["id"] = payload.pop("opt_id")
892
+ if "member_id" in payload and "id" not in payload:
893
+ payload["id"] = payload.pop("member_id")
894
+ return payload
895
+
896
+
897
+ class CustomButtonQuestionRefPatch(StrictModel):
898
+ que_id: int = Field(validation_alias=AliasChoices("que_id", "queId"))
899
+ que_title: str | None = Field(default=None, validation_alias=AliasChoices("que_title", "queTitle"))
900
+ que_type: int | None = Field(default=None, validation_alias=AliasChoices("que_type", "queType"))
901
+
902
+
903
+ class CustomButtonMatchRulePatch(StrictModel):
904
+ que_id: int = Field(validation_alias=AliasChoices("que_id", "queId"))
905
+ que_title: str | None = Field(default=None, validation_alias=AliasChoices("que_title", "queTitle"))
906
+ que_type: int | None = Field(default=None, validation_alias=AliasChoices("que_type", "queType"))
907
+ date_type: int | None = Field(default=None, validation_alias=AliasChoices("date_type", "dateType"))
908
+ judge_type: int | None = Field(default=None, validation_alias=AliasChoices("judge_type", "judgeType"))
909
+ match_type: int | None = Field(default=None, validation_alias=AliasChoices("match_type", "matchType"))
910
+ judge_values: list[str] = Field(default_factory=list, validation_alias=AliasChoices("judge_values", "judgeValues"))
911
+ judge_que_type: int | None = Field(default=None, validation_alias=AliasChoices("judge_que_type", "judgeQueType"))
912
+ judge_que_id: int | None = Field(default=None, validation_alias=AliasChoices("judge_que_id", "judgeQueId"))
913
+ judge_que_detail: CustomButtonQuestionRefPatch | None = Field(
914
+ default=None,
915
+ validation_alias=AliasChoices("judge_que_detail", "judgeQueDetail"),
916
+ )
917
+ judge_value_details: list[CustomButtonJudgeValuePatch] = Field(
918
+ default_factory=list,
919
+ validation_alias=AliasChoices("judge_value_details", "judgeValueDetails"),
920
+ )
921
+ path_value: str | None = Field(default=None, validation_alias=AliasChoices("path_value", "pathValue"))
922
+ table_update_type: int | None = Field(default=None, validation_alias=AliasChoices("table_update_type", "tableUpdateType"))
923
+ multi_value: bool | None = Field(default=None, validation_alias=AliasChoices("multi_value", "multiValue"))
924
+ add_rule: str | None = Field(default=None, validation_alias=AliasChoices("add_rule", "addRule"))
925
+ filter_condition: list[list["CustomButtonMatchRulePatch"]] = Field(
926
+ default_factory=list,
927
+ validation_alias=AliasChoices("filter_condition", "filterCondition"),
928
+ )
929
+ field_id_prefix: str | None = Field(default=None, validation_alias=AliasChoices("field_id_prefix", "fieldIdPrefix"))
930
+
931
+ @model_validator(mode="before")
932
+ @classmethod
933
+ def normalize_aliases(cls, value: Any) -> Any:
934
+ if not isinstance(value, dict):
935
+ return value
936
+ payload = dict(value)
937
+ if "judgeValue" in payload and "judge_values" not in payload and "judgeValues" not in payload:
938
+ payload["judge_values"] = [payload.pop("judgeValue")]
939
+ return payload
940
+
941
+
942
+ class CustomButtonAddDataConfigPatch(StrictModel):
943
+ related_app_key: str | None = Field(default=None, validation_alias=AliasChoices("related_app_key", "relatedAppKey"))
944
+ related_app_name: str | None = Field(default=None, validation_alias=AliasChoices("related_app_name", "relatedAppName"))
945
+ que_relation: list[CustomButtonMatchRulePatch] = Field(
946
+ default_factory=list,
947
+ validation_alias=AliasChoices("que_relation", "queRelation"),
948
+ )
949
+
950
+
951
+ class CustomButtonExternalQRobotConfigPatch(StrictModel):
952
+ external_qrobot_config_id: int | None = Field(
953
+ default=None,
954
+ validation_alias=AliasChoices("external_qrobot_config_id", "externalQRobotConfigId"),
955
+ )
956
+ triggered_text: str | None = Field(default=None, validation_alias=AliasChoices("triggered_text", "triggeredText"))
957
+
958
+
959
+ class CustomButtonWingsConfigPatch(StrictModel):
960
+ wings_agent_id: int | None = Field(default=None, validation_alias=AliasChoices("wings_agent_id", "wingsAgentId"))
961
+ wings_agent_name: str | None = Field(default=None, validation_alias=AliasChoices("wings_agent_name", "wingsAgentName"))
962
+ bind_que_id_list: list[int] = Field(default_factory=list, validation_alias=AliasChoices("bind_que_id_list", "bindQueIdList"))
963
+ bind_file_que_id_list: list[int] = Field(
964
+ default_factory=list,
965
+ validation_alias=AliasChoices("bind_file_que_id_list", "bindFileQueIdList"),
966
+ )
967
+ default_prompt: str | None = Field(default=None, validation_alias=AliasChoices("default_prompt", "defaultPrompt"))
968
+ being_auto_send: bool | None = Field(default=None, validation_alias=AliasChoices("being_auto_send", "beingAutoSend"))
969
+
970
+ @model_validator(mode="before")
971
+ @classmethod
972
+ def normalize_aliases(cls, value: Any) -> Any:
973
+ if not isinstance(value, dict):
974
+ return value
975
+ payload = dict(value)
976
+ raw_agent_id = payload.get("wings_agent_id", payload.get("wingsAgentId"))
977
+ if isinstance(raw_agent_id, str) and raw_agent_id.strip().isdigit():
978
+ payload["wings_agent_id"] = int(raw_agent_id.strip())
979
+ return payload
980
+
981
+
982
+ class CustomButtonPatch(StrictModel):
983
+ button_text: str = Field(validation_alias=AliasChoices("button_text", "buttonText"))
984
+ background_color: str = Field(validation_alias=AliasChoices("background_color", "backgroundColor"))
985
+ text_color: str = Field(validation_alias=AliasChoices("text_color", "textColor"))
986
+ button_icon: str = Field(validation_alias=AliasChoices("button_icon", "buttonIcon"))
987
+ icon_color: str | None = Field(default=None, validation_alias=AliasChoices("icon_color", "iconColor"))
988
+ trigger_action: PublicButtonTriggerAction = Field(validation_alias=AliasChoices("trigger_action", "triggerAction"))
989
+ trigger_link_url: str | None = Field(default=None, validation_alias=AliasChoices("trigger_link_url", "triggerLinkUrl"))
990
+ trigger_add_data_config: CustomButtonAddDataConfigPatch | None = Field(
991
+ default=None,
992
+ validation_alias=AliasChoices("trigger_add_data_config", "triggerAddDataConfig"),
993
+ )
994
+ external_qrobot_config: CustomButtonExternalQRobotConfigPatch | None = Field(
995
+ default=None,
996
+ validation_alias=AliasChoices(
997
+ "external_qrobot_config",
998
+ "externalQrobotConfig",
999
+ "custom_button_external_qrobot_relation_vo",
1000
+ "customButtonExternalQRobotRelationVO",
1001
+ ),
1002
+ )
1003
+ trigger_wings_config: CustomButtonWingsConfigPatch | None = Field(
1004
+ default=None,
1005
+ validation_alias=AliasChoices("trigger_wings_config", "triggerWingsConfig"),
1006
+ )
1007
+
1008
+ @model_validator(mode="after")
1009
+ def validate_shape(self) -> "CustomButtonPatch":
1010
+ if self.trigger_action == PublicButtonTriggerAction.link and not str(self.trigger_link_url or "").strip():
1011
+ raise ValueError("link buttons require trigger_link_url")
1012
+ if self.trigger_action == PublicButtonTriggerAction.add_data and self.trigger_add_data_config is None:
1013
+ raise ValueError("addData buttons require trigger_add_data_config")
1014
+ if self.trigger_action == PublicButtonTriggerAction.qrobot and self.external_qrobot_config is None:
1015
+ raise ValueError("qRobot buttons require external_qrobot_config")
1016
+ if self.trigger_action == PublicButtonTriggerAction.wings and self.trigger_wings_config is None:
1017
+ raise ValueError("wings buttons require trigger_wings_config")
1018
+ return self
1019
+
1020
+
1021
+ class ViewButtonBindingPatch(StrictModel):
1022
+ button_type: PublicViewButtonType = Field(validation_alias=AliasChoices("button_type", "buttonType"))
1023
+ config_type: PublicViewButtonConfigType = Field(validation_alias=AliasChoices("config_type", "configType"))
1024
+ button_id: int = Field(validation_alias=AliasChoices("button_id", "buttonId", "id"))
1025
+ button_text: str | None = Field(default=None, validation_alias=AliasChoices("button_text", "buttonText"))
1026
+ button_icon: str | None = Field(default=None, validation_alias=AliasChoices("button_icon", "buttonIcon"))
1027
+ icon_color: str | None = Field(default=None, validation_alias=AliasChoices("icon_color", "iconColor"))
1028
+ background_color: str | None = Field(default=None, validation_alias=AliasChoices("background_color", "backgroundColor"))
1029
+ text_color: str | None = Field(default=None, validation_alias=AliasChoices("text_color", "textColor"))
1030
+ trigger_action: str | None = Field(default=None, validation_alias=AliasChoices("trigger_action", "triggerAction"))
1031
+ print_tpls: list[Any] = Field(default_factory=list, validation_alias=AliasChoices("print_tpls", "printTpls"))
1032
+ being_main: bool = Field(default=False, validation_alias=AliasChoices("being_main", "beingMain"))
1033
+ button_limit: list[list[ViewFilterRulePatch]] = Field(
1034
+ default_factory=list,
1035
+ validation_alias=AliasChoices("button_limit", "buttonLimit"),
1036
+ )
1037
+ button_formula: str | None = Field(default=None, validation_alias=AliasChoices("button_formula", "buttonFormula"))
1038
+ button_formula_type: int = Field(default=1, validation_alias=AliasChoices("button_formula_type", "buttonFormulaType"))
1039
+
1040
+ @model_validator(mode="before")
1041
+ @classmethod
1042
+ def normalize_aliases(cls, value: Any) -> Any:
1043
+ if not isinstance(value, dict):
1044
+ return value
1045
+ payload = dict(value)
1046
+ raw_button_type = payload.get("button_type", payload.get("buttonType"))
1047
+ if isinstance(raw_button_type, str):
1048
+ normalized_type = raw_button_type.strip().lower()
1049
+ if normalized_type == "system":
1050
+ payload["button_type"] = "SYSTEM"
1051
+ elif normalized_type == "custom":
1052
+ payload["button_type"] = "CUSTOM"
1053
+ raw_config_type = payload.get("config_type", payload.get("configType"))
1054
+ if isinstance(raw_config_type, str):
1055
+ normalized_config = raw_config_type.strip().lower()
1056
+ if normalized_config == "top":
1057
+ payload["config_type"] = "TOP"
1058
+ elif normalized_config == "detail":
1059
+ payload["config_type"] = "DETAIL"
1060
+ raw_limits = payload.get("button_limit", payload.get("buttonLimit"))
1061
+ if isinstance(raw_limits, list) and raw_limits and all(isinstance(item, dict) for item in raw_limits):
1062
+ payload["button_limit"] = [raw_limits]
1063
+ return payload
1064
+
1065
+ @model_validator(mode="after")
1066
+ def validate_shape(self) -> "ViewButtonBindingPatch":
1067
+ if self.button_type == PublicViewButtonType.system:
1068
+ missing = [
1069
+ field_name
1070
+ for field_name, value in (
1071
+ ("button_icon", self.button_icon),
1072
+ ("background_color", self.background_color),
1073
+ ("text_color", self.text_color),
1074
+ ("trigger_action", self.trigger_action),
1075
+ )
1076
+ if not str(value or "").strip()
1077
+ ]
1078
+ if missing:
1079
+ raise ValueError(f"system button bindings require {', '.join(missing)}")
1080
+ return self
1081
+
1082
+
1083
+ class ChartFilterRulePatch(StrictModel):
1084
+ field_name: str = Field(validation_alias=AliasChoices("field_name", "fieldName", "field", "name"))
1085
+ operator: ViewFilterOperator = Field(validation_alias=AliasChoices("operator", "op"))
1086
+ values: list[Any] = Field(default_factory=list)
1087
+
1088
+ @model_validator(mode="before")
1089
+ @classmethod
1090
+ def normalize_aliases(cls, value: Any) -> Any:
1091
+ if not isinstance(value, dict):
1092
+ return value
1093
+ payload = dict(value)
1094
+ if "value" in payload and "values" not in payload:
1095
+ payload["values"] = [payload.pop("value")]
1096
+ raw_operator = payload.get("operator", payload.get("op"))
1097
+ if isinstance(raw_operator, str):
1098
+ normalized = raw_operator.strip().lower()
1099
+ operator_aliases = {
1100
+ "equals": ViewFilterOperator.eq.value,
1101
+ "equal": ViewFilterOperator.eq.value,
1102
+ "=": ViewFilterOperator.eq.value,
1103
+ "not_equals": ViewFilterOperator.neq.value,
1104
+ "not_equal": ViewFilterOperator.neq.value,
1105
+ "!=": ViewFilterOperator.neq.value,
1106
+ ">=": ViewFilterOperator.gte.value,
1107
+ "<=": ViewFilterOperator.lte.value,
1108
+ "any_of": ViewFilterOperator.in_.value,
1109
+ "one_of": ViewFilterOperator.in_.value,
1110
+ "between_any": ViewFilterOperator.in_.value,
1111
+ "empty": ViewFilterOperator.is_empty.value,
1112
+ "is blank": ViewFilterOperator.is_empty.value,
1113
+ "blank": ViewFilterOperator.is_empty.value,
1114
+ "not_empty": ViewFilterOperator.not_empty.value,
1115
+ "not blank": ViewFilterOperator.not_empty.value,
1116
+ }
1117
+ if normalized in operator_aliases:
1118
+ payload["operator"] = operator_aliases[normalized]
1119
+ elif "operator" not in payload:
1120
+ payload["operator"] = normalized
1121
+ payload.pop("op", None)
1122
+ return payload
1123
+
1124
+ @model_validator(mode="after")
1125
+ def validate_shape(self) -> "ChartFilterRulePatch":
1126
+ if self.operator in {ViewFilterOperator.is_empty, ViewFilterOperator.not_empty}:
1127
+ self.values = []
1128
+ return self
1129
+ if not self.values:
1130
+ raise ValueError("chart filter rule requires values")
1131
+ return self
1132
+
1133
+
1134
+ class ChartUpsertPatch(StrictModel):
1135
+ chart_id: str | None = None
1136
+ name: str
1137
+ chart_type: PublicChartType
1138
+ dimension_field_ids: list[str] = Field(default_factory=list)
1139
+ indicator_field_ids: list[str] = Field(default_factory=list)
1140
+ filters: list[ChartFilterRulePatch] = Field(default_factory=list)
1141
+ question_config: list[dict[str, Any]] = Field(default_factory=list)
1142
+ user_config: list[dict[str, Any]] = Field(default_factory=list)
1143
+ config: dict[str, Any] = Field(default_factory=dict)
1144
+
1145
+ @model_validator(mode="before")
1146
+ @classmethod
1147
+ def normalize_aliases(cls, value: Any) -> Any:
1148
+ if not isinstance(value, dict):
1149
+ return value
1150
+ payload = dict(value)
1151
+ if "id" in payload and "chart_id" not in payload:
1152
+ payload["chart_id"] = payload.pop("id")
1153
+ if "type" in payload and "chart_type" not in payload:
1154
+ payload["chart_type"] = payload.pop("type")
1155
+ if "dimension_fields" in payload and "dimension_field_ids" not in payload:
1156
+ payload["dimension_field_ids"] = payload.pop("dimension_fields")
1157
+ if "indicator_fields" in payload and "indicator_field_ids" not in payload:
1158
+ payload["indicator_field_ids"] = payload.pop("indicator_fields")
1159
+ if "metric_field_ids" in payload and "indicator_field_ids" not in payload:
1160
+ payload["indicator_field_ids"] = payload.pop("metric_field_ids")
1161
+ raw_type = payload.get("chart_type")
1162
+ if isinstance(raw_type, str):
1163
+ normalized = raw_type.strip().lower()
1164
+ aliases = {
1165
+ "targetchart": PublicChartType.target.value,
1166
+ "piechart": PublicChartType.pie.value,
1167
+ "barchart": PublicChartType.bar.value,
1168
+ "linechart": PublicChartType.line.value,
1169
+ "tablechart": PublicChartType.table.value,
1170
+ }
1171
+ if normalized in aliases:
1172
+ payload["chart_type"] = aliases[normalized]
1173
+ if isinstance(payload.get("chart_id"), int):
1174
+ payload["chart_id"] = str(payload["chart_id"])
1175
+ if isinstance(payload.get("dimension_field_ids"), list):
1176
+ payload["dimension_field_ids"] = [str(item) for item in payload["dimension_field_ids"] if item is not None and str(item).strip()]
1177
+ if isinstance(payload.get("indicator_field_ids"), list):
1178
+ payload["indicator_field_ids"] = [str(item) for item in payload["indicator_field_ids"] if item is not None and str(item).strip()]
1179
+ return payload
1180
+
1181
+
1182
+ class ChartApplyRequest(StrictModel):
1183
+ app_key: str
1184
+ upsert_charts: list[ChartUpsertPatch] = Field(default_factory=list)
1185
+ remove_chart_ids: list[str] = Field(default_factory=list)
1186
+ reorder_chart_ids: list[str] = Field(default_factory=list)
1187
+
1188
+ @model_validator(mode="before")
1189
+ @classmethod
1190
+ def normalize_ids(cls, value: Any) -> Any:
1191
+ if not isinstance(value, dict):
1192
+ return value
1193
+ payload = dict(value)
1194
+ for key in ("remove_chart_ids", "reorder_chart_ids"):
1195
+ raw = payload.get(key)
1196
+ if isinstance(raw, list):
1197
+ payload[key] = [str(item) for item in raw if item is not None and str(item).strip()]
1198
+ return payload
1199
+
1200
+ @model_validator(mode="after")
1201
+ def validate_shape(self) -> "ChartApplyRequest":
1202
+ if not self.upsert_charts and not self.remove_chart_ids and not self.reorder_chart_ids:
1203
+ raise ValueError("chart apply requires at least one upsert, remove, or reorder operation")
1204
+ return self
1205
+
1206
+
1207
+ class PortalComponentPositionPatch(StrictModel):
1208
+ pc_x: int = Field(default=0, validation_alias=AliasChoices("pc_x", "pcX", "x"))
1209
+ pc_y: int = Field(default=0, validation_alias=AliasChoices("pc_y", "pcY", "y"))
1210
+ pc_w: int = Field(default=12, validation_alias=AliasChoices("pc_w", "pcW", "w"))
1211
+ pc_h: int = Field(default=8, validation_alias=AliasChoices("pc_h", "pcH", "h"))
1212
+ mobile_x: int = Field(default=0, validation_alias=AliasChoices("mobile_x", "mobileX"))
1213
+ mobile_y: int = Field(default=0, validation_alias=AliasChoices("mobile_y", "mobileY"))
1214
+ mobile_w: int = Field(default=12, validation_alias=AliasChoices("mobile_w", "mobileW"))
1215
+ mobile_h: int = Field(default=8, validation_alias=AliasChoices("mobile_h", "mobileH"))
1216
+
1217
+ @model_validator(mode="before")
1218
+ @classmethod
1219
+ def normalize_nested_layout(cls, value: Any) -> Any:
1220
+ if not isinstance(value, dict):
1221
+ return value
1222
+ payload = dict(value)
1223
+ pc = payload.pop("pc", None)
1224
+ mobile = payload.pop("mobile", None)
1225
+ if isinstance(pc, dict):
1226
+ if "pc_x" not in payload and "x" in pc:
1227
+ payload["pc_x"] = pc.get("x")
1228
+ if "pc_y" not in payload and "y" in pc:
1229
+ payload["pc_y"] = pc.get("y")
1230
+ if "pc_w" not in payload and "cols" in pc:
1231
+ payload["pc_w"] = pc.get("cols")
1232
+ if "pc_h" not in payload and "rows" in pc:
1233
+ payload["pc_h"] = pc.get("rows")
1234
+ if isinstance(mobile, dict):
1235
+ if "mobile_x" not in payload and "x" in mobile:
1236
+ payload["mobile_x"] = mobile.get("x")
1237
+ if "mobile_y" not in payload and "y" in mobile:
1238
+ payload["mobile_y"] = mobile.get("y")
1239
+ if "mobile_w" not in payload and "cols" in mobile:
1240
+ payload["mobile_w"] = mobile.get("cols")
1241
+ if "mobile_h" not in payload and "rows" in mobile:
1242
+ payload["mobile_h"] = mobile.get("rows")
1243
+ return payload
1244
+
1245
+
1246
+ class PortalChartRefPatch(StrictModel):
1247
+ app_key: str
1248
+ chart_id: str | None = None
1249
+ chart_name: str | None = None
1250
+
1251
+ @model_validator(mode="after")
1252
+ def validate_target(self) -> "PortalChartRefPatch":
1253
+ if not (self.chart_id or self.chart_name):
1254
+ raise ValueError("chart_ref requires chart_id or chart_name")
1255
+ return self
1256
+
1257
+
1258
+ class PortalViewRefPatch(StrictModel):
1259
+ app_key: str
1260
+ view_key: str | None = None
1261
+ view_name: str | None = None
1262
+
1263
+ @model_validator(mode="after")
1264
+ def validate_target(self) -> "PortalViewRefPatch":
1265
+ if not (self.view_key or self.view_name):
1266
+ raise ValueError("view_ref requires view_key or view_name")
1267
+ return self
1268
+
1269
+
1270
+ class PortalSectionPatch(StrictModel):
1271
+ title: str
1272
+ source_type: str = Field(validation_alias=AliasChoices("source_type", "sourceType"))
1273
+ position: PortalComponentPositionPatch | None = None
1274
+ dash_style_config: dict[str, Any] | None = Field(default=None, validation_alias=AliasChoices("dash_style_config", "dashStyleConfigBO"))
1275
+ config: dict[str, Any] = Field(default_factory=dict)
1276
+ chart_ref: PortalChartRefPatch | None = None
1277
+ view_ref: PortalViewRefPatch | None = None
1278
+ text: str | None = None
1279
+ url: str | None = None
1280
+
1281
+ @model_validator(mode="before")
1282
+ @classmethod
1283
+ def normalize_aliases(cls, value: Any) -> Any:
1284
+ if not isinstance(value, dict):
1285
+ return value
1286
+ payload = dict(value)
1287
+ raw_type = payload.get("source_type", payload.get("sourceType"))
1288
+ if isinstance(raw_type, str):
1289
+ payload["source_type"] = raw_type.strip().lower()
1290
+ if "chartRef" in payload and "chart_ref" not in payload:
1291
+ payload["chart_ref"] = payload.pop("chartRef")
1292
+ if "viewRef" in payload and "view_ref" not in payload:
1293
+ payload["view_ref"] = payload.pop("viewRef")
1294
+ if "dashStyleConfigBO" in payload and "dash_style_config" not in payload:
1295
+ payload["dash_style_config"] = payload.pop("dashStyleConfigBO")
1296
+ return payload
1297
+
1298
+ @model_validator(mode="after")
1299
+ def validate_shape(self) -> "PortalSectionPatch":
1300
+ supported = {"chart", "view", "grid", "filter", "text", "link"}
1301
+ if self.source_type not in supported:
1302
+ raise ValueError(f"unsupported portal source_type '{self.source_type}'")
1303
+ if self.source_type == "chart" and self.chart_ref is None:
1304
+ raise ValueError("chart section requires chart_ref")
1305
+ if self.source_type == "view" and self.view_ref is None:
1306
+ raise ValueError("view section requires view_ref")
1307
+ if self.source_type == "text" and self.text is None:
1308
+ raise ValueError("text section requires text")
1309
+ if self.source_type == "link" and self.url is None:
1310
+ raise ValueError("link section requires url")
1311
+ return self
1312
+
1313
+
1314
+ class PortalApplyRequest(StrictModel):
1315
+ dash_key: str | None = None
1316
+ dash_name: str | None = None
1317
+ package_tag_id: int | None = None
1318
+ publish: bool = True
1319
+ sections: list[PortalSectionPatch] = Field(default_factory=list)
1320
+ auth: dict[str, Any] | None = None
1321
+ icon: str | None = None
1322
+ color: str | None = None
1323
+ hide_copyright: bool | None = Field(default=None, validation_alias=AliasChoices("hide_copyright", "hideCopyright"))
1324
+ dash_global_config: dict[str, Any] | None = Field(default=None, validation_alias=AliasChoices("dash_global_config", "dashGlobalConfig"))
1325
+ config: dict[str, Any] = Field(default_factory=dict)
1326
+
1327
+ @model_validator(mode="after")
1328
+ def validate_shape(self) -> "PortalApplyRequest":
1329
+ if not self.dash_key and not self.package_tag_id:
1330
+ raise ValueError("package_tag_id is required when dash_key is empty")
1331
+ if not self.dash_key and not self.dash_name:
1332
+ raise ValueError("dash_name is required when creating a portal")
1333
+ if not self.sections:
1334
+ raise ValueError("portal apply requires a non-empty sections list")
221
1335
  return self
222
1336
 
223
1337
 
@@ -227,6 +1341,7 @@ FieldPatch.model_rebuild()
227
1341
  class AppReadSummaryResponse(StrictModel):
228
1342
  app_key: str
229
1343
  title: str | None = None
1344
+ app_icon: str | None = None
230
1345
  tag_ids: list[int] = Field(default_factory=list)
231
1346
  publish_status: int | None = None
232
1347
  field_count: int = 0
@@ -261,10 +1376,65 @@ class AppFlowReadResponse(StrictModel):
261
1376
  transitions: list[dict[str, Any]] = Field(default_factory=list)
262
1377
 
263
1378
 
1379
+ class AppChartsReadResponse(StrictModel):
1380
+ app_key: str
1381
+ charts: list[dict[str, Any]] = Field(default_factory=list)
1382
+ chart_count: int = 0
1383
+
1384
+
1385
+ class PortalListResponse(StrictModel):
1386
+ items: list[dict[str, Any]] = Field(default_factory=list)
1387
+ total: int = 0
1388
+
1389
+
1390
+ class PortalReadSummaryResponse(StrictModel):
1391
+ dash_key: str
1392
+ being_draft: bool = True
1393
+ dash_name: str | None = None
1394
+ package_tag_ids: list[int] = Field(default_factory=list)
1395
+ dash_icon: str | None = None
1396
+ hide_copyright: bool | None = None
1397
+ config_keys: list[str] = Field(default_factory=list)
1398
+ dash_global_config_keys: list[str] = Field(default_factory=list)
1399
+ section_count: int = 0
1400
+ sections: list[dict[str, Any]] = Field(default_factory=list)
1401
+
1402
+
1403
+ class PortalGetResponse(StrictModel):
1404
+ dash_key: str
1405
+ being_draft: bool = True
1406
+ dash_name: str | None = None
1407
+ package_tag_ids: list[int] = Field(default_factory=list)
1408
+ dash_icon: str | None = None
1409
+ hide_copyright: bool | None = None
1410
+ auth: dict[str, Any] = Field(default_factory=dict)
1411
+ config: dict[str, Any] = Field(default_factory=dict)
1412
+ dash_global_config: dict[str, Any] = Field(default_factory=dict)
1413
+ component_count: int = 0
1414
+ components: list[dict[str, Any]] = Field(default_factory=list)
1415
+
1416
+
1417
+ class ViewGetResponse(StrictModel):
1418
+ viewgraph_key: str
1419
+ base_info: dict[str, Any] = Field(default_factory=dict)
1420
+ config: dict[str, Any] = Field(default_factory=dict)
1421
+ questions: list[dict[str, Any]] = Field(default_factory=list)
1422
+ associations: list[dict[str, Any]] = Field(default_factory=list)
1423
+
1424
+
1425
+ class ChartGetResponse(StrictModel):
1426
+ chart_id: str
1427
+ base: dict[str, Any] = Field(default_factory=dict)
1428
+ config: dict[str, Any] = Field(default_factory=dict)
1429
+ data: dict[str, Any] = Field(default_factory=dict)
1430
+
1431
+
264
1432
  class SchemaPlanRequest(StrictModel):
265
1433
  app_key: str = ""
266
1434
  package_tag_id: int | None = None
267
1435
  app_name: str = Field(default="", validation_alias=AliasChoices("app_name", "app_title", "title"))
1436
+ icon: str | None = None
1437
+ color: str | None = None
268
1438
  create_if_missing: bool = False
269
1439
  add_fields: list[FieldPatch] = Field(default_factory=list)
270
1440
  update_fields: list[FieldUpdatePatch] = Field(default_factory=list)
@@ -303,6 +1473,22 @@ class FlowPlanRequest(StrictModel):
303
1473
  payload = dict(value)
304
1474
  if str(payload.get("mode") or "").strip().lower() == "overwrite":
305
1475
  payload["mode"] = "replace"
1476
+ raw_preset = payload.get("preset")
1477
+ if raw_preset is None and isinstance(payload.get("base_preset"), str):
1478
+ raw_preset = payload["base_preset"]
1479
+ payload["preset"] = raw_preset
1480
+ if isinstance(raw_preset, str):
1481
+ normalized_preset = raw_preset.strip().lower()
1482
+ preset_aliases = {
1483
+ "default_approval": FlowPreset.basic_approval.value,
1484
+ "approval": FlowPreset.basic_approval.value,
1485
+ "basic approval": FlowPreset.basic_approval.value,
1486
+ "default_fill_then_approve": FlowPreset.basic_fill_then_approve.value,
1487
+ "default-fill-then-approve": FlowPreset.basic_fill_then_approve.value,
1488
+ "fill_then_approve": FlowPreset.basic_fill_then_approve.value,
1489
+ }
1490
+ if normalized_preset in preset_aliases:
1491
+ payload["preset"] = preset_aliases[normalized_preset]
306
1492
  return payload
307
1493
 
308
1494
 
@@ -332,11 +1518,37 @@ def _normalize_field_payload(value: Any) -> Any:
332
1518
  if not isinstance(value, dict):
333
1519
  return value
334
1520
  payload = dict(value)
1521
+ if "fields" in payload and "subfields" not in payload:
1522
+ payload["subfields"] = payload.pop("fields")
335
1523
  raw_type = payload.get("type")
1524
+ if isinstance(raw_type, int):
1525
+ normalized_from_id = FIELD_TYPE_ID_ALIASES.get(raw_type)
1526
+ if normalized_from_id is not None:
1527
+ payload["type"] = normalized_from_id.value
336
1528
  if isinstance(raw_type, str):
337
1529
  normalized = FIELD_TYPE_ALIASES.get(raw_type.strip().lower())
338
1530
  if normalized is not None:
339
1531
  payload["type"] = normalized.value
1532
+ normalized_relation_mode = _normalize_public_relation_mode(
1533
+ payload.get("relation_mode", payload.get("relationMode", payload.get("selection_mode", payload.get("selectionMode"))))
1534
+ )
1535
+ if normalized_relation_mode is None:
1536
+ for alias_key in ("optional_data_num", "optionalDataNum", "multiple", "allow_multiple"):
1537
+ if alias_key in payload:
1538
+ normalized_relation_mode = _normalize_public_relation_mode(payload.get(alias_key))
1539
+ break
1540
+ if normalized_relation_mode is not None:
1541
+ payload["relation_mode"] = normalized_relation_mode
1542
+ for alias_key in (
1543
+ "relationMode",
1544
+ "selection_mode",
1545
+ "selectionMode",
1546
+ "optional_data_num",
1547
+ "optionalDataNum",
1548
+ "multiple",
1549
+ "allow_multiple",
1550
+ ):
1551
+ payload.pop(alias_key, None)
340
1552
  return payload
341
1553
 
342
1554
 
@@ -344,3 +1556,40 @@ def _slugify_title(title: str) -> str:
344
1556
  normalized = "".join(ch.lower() if ch.isalnum() else "_" for ch in str(title or ""))
345
1557
  collapsed = "_".join(part for part in normalized.split("_") if part)
346
1558
  return collapsed or "section"
1559
+
1560
+
1561
+ def _normalize_public_relation_mode(value: Any) -> str | None:
1562
+ if value is None:
1563
+ return None
1564
+ if isinstance(value, bool):
1565
+ return PublicRelationMode.multiple.value if value else PublicRelationMode.single.value
1566
+ if isinstance(value, int):
1567
+ if value == 0:
1568
+ return PublicRelationMode.multiple.value
1569
+ if value == 1:
1570
+ return PublicRelationMode.single.value
1571
+ return None
1572
+ if isinstance(value, str):
1573
+ normalized = value.strip().lower()
1574
+ aliases = {
1575
+ "single": PublicRelationMode.single.value,
1576
+ "single_select": PublicRelationMode.single.value,
1577
+ "single-select": PublicRelationMode.single.value,
1578
+ "one": PublicRelationMode.single.value,
1579
+ "1": PublicRelationMode.single.value,
1580
+ "multiple": PublicRelationMode.multiple.value,
1581
+ "multi": PublicRelationMode.multiple.value,
1582
+ "multi_select": PublicRelationMode.multiple.value,
1583
+ "multi-select": PublicRelationMode.multiple.value,
1584
+ "many": PublicRelationMode.multiple.value,
1585
+ "0": PublicRelationMode.multiple.value,
1586
+ }
1587
+ return aliases.get(normalized, normalized or None)
1588
+ return None
1589
+
1590
+
1591
+ CustomButtonMatchRulePatch.model_rebuild()
1592
+ CustomButtonAddDataConfigPatch.model_rebuild()
1593
+ CodeBlockAliasPathPatch.model_rebuild()
1594
+ ViewButtonBindingPatch.model_rebuild()
1595
+ ViewUpsertPatch.model_rebuild()