@josephyan/qingflow-app-builder-mcp 1.1.30 → 1.1.33
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 +6 -4
- package/docs/local-agent-install.md +57 -6
- package/entry_point.py +1 -1
- package/npm/bin/qingflow-app-builder-mcp.mjs +1 -1
- package/npm/bin/qingflow-skills.mjs +5 -0
- package/npm/lib/runtime.mjs +20 -59
- package/npm/scripts/postinstall.mjs +1 -10
- package/package.json +3 -2
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-builder/SKILL.md +111 -55
- package/skills/qingflow-app-builder/references/complete-system-development-guide.md +169 -0
- package/skills/qingflow-app-builder/references/create-app.md +31 -24
- package/skills/qingflow-app-builder/references/environments.md +1 -1
- package/skills/qingflow-app-builder/references/gotchas.md +40 -14
- package/skills/qingflow-app-builder/references/match-rules.md +196 -85
- package/skills/qingflow-app-builder/references/public-surface-sync.md +2 -2
- package/skills/qingflow-app-builder/references/single-app-development-guide.md +84 -0
- package/skills/qingflow-app-builder/references/solution-playbooks.md +10 -0
- package/skills/qingflow-app-builder/references/tool-selection.md +40 -25
- package/skills/qingflow-app-builder/references/update-flow.md +27 -52
- package/skills/qingflow-app-builder/references/update-schema.md +14 -0
- package/skills/qingflow-app-builder/references/update-views.md +101 -32
- package/skills/qingflow-app-builder/scripts/validate_system_build_summary.py +124 -0
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +5 -3
- package/skills/qingflow-app-builder-code-integrations/references/code-block.md +1 -1
- package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +1 -1
- package/skills/qingflow-mcp-setup/SKILL.md +13 -9
- package/skills/qingflow-mcp-setup/references/claude-desktop.md +1 -1
- package/skills/qingflow-mcp-setup/references/environments.md +2 -1
- package/skills/qingflow-mcp-setup/references/generic-stdio.md +6 -5
- package/skills/qingflow-workflow-builder/SKILL.md +9 -7
- package/skills/qingflow-workflow-builder/references/01-overview.md +4 -2
- package/skills/qingflow-workflow-builder/references/03-flow-patterns.md +2 -0
- package/skills/qingflow-workflow-builder/references/06-stage3-build-spec.md +59 -7
- package/skills/qingflow-workflow-builder/references/07-stage4-validate-spec.md +14 -5
- package/skills/qingflow-workflow-builder/references/08-stage5-apply-verify.md +19 -3
- package/skills/qingflow-workflow-builder/references/11-troubleshooting.md +2 -0
- package/src/qingflow_mcp/__main__.py +6 -2
- package/src/qingflow_mcp/builder_facade/models.py +473 -138
- package/src/qingflow_mcp/builder_facade/service.py +6356 -1866
- package/src/qingflow_mcp/cli/commands/builder.py +358 -355
- package/src/qingflow_mcp/cli/commands/chart.py +1 -1
- package/src/qingflow_mcp/cli/commands/common.py +12 -3
- package/src/qingflow_mcp/cli/commands/exports.py +2 -2
- package/src/qingflow_mcp/cli/commands/imports.py +3 -3
- package/src/qingflow_mcp/cli/commands/portal.py +2 -2
- package/src/qingflow_mcp/cli/commands/record.py +101 -27
- package/src/qingflow_mcp/cli/commands/task.py +28 -47
- package/src/qingflow_mcp/cli/commands/view.py +1 -1
- package/src/qingflow_mcp/cli/context.py +0 -3
- package/src/qingflow_mcp/cli/formatters.py +784 -16
- package/src/qingflow_mcp/cli/main.py +120 -41
- package/src/qingflow_mcp/errors.py +43 -2
- package/src/qingflow_mcp/public_surface.py +26 -17
- package/src/qingflow_mcp/response_trim.py +81 -17
- package/src/qingflow_mcp/server.py +14 -12
- package/src/qingflow_mcp/server_app_builder.py +83 -39
- package/src/qingflow_mcp/server_app_user.py +22 -16
- package/src/qingflow_mcp/session_store.py +11 -7
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
- package/src/qingflow_mcp/solution/executor.py +114 -15
- package/src/qingflow_mcp/tools/ai_builder_tools.py +3571 -1473
- package/src/qingflow_mcp/tools/app_tools.py +184 -43
- package/src/qingflow_mcp/tools/approval_tools.py +197 -35
- package/src/qingflow_mcp/tools/auth_tools.py +92 -16
- package/src/qingflow_mcp/tools/code_block_tools.py +298 -40
- package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
- package/src/qingflow_mcp/tools/directory_tools.py +236 -72
- package/src/qingflow_mcp/tools/export_tools.py +244 -34
- package/src/qingflow_mcp/tools/feedback_tools.py +9 -0
- package/src/qingflow_mcp/tools/file_tools.py +9 -3
- package/src/qingflow_mcp/tools/import_tools.py +336 -49
- package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
- package/src/qingflow_mcp/tools/package_tools.py +118 -6
- package/src/qingflow_mcp/tools/portal_tools.py +39 -3
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
- package/src/qingflow_mcp/tools/record_tools.py +1141 -356
- package/src/qingflow_mcp/tools/resource_read_tools.py +188 -39
- package/src/qingflow_mcp/tools/role_tools.py +80 -9
- package/src/qingflow_mcp/tools/solution_tools.py +57 -15
- package/src/qingflow_mcp/tools/task_context_tools.py +687 -159
- package/src/qingflow_mcp/tools/task_tools.py +113 -29
- package/src/qingflow_mcp/tools/view_tools.py +106 -3
- package/src/qingflow_mcp/tools/workflow_tools.py +17 -1
- package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
- package/src/qingflow_mcp/version.py +0 -2
- package/skills/qingflow-app-builder/references/build-complete-system.md +0 -234
- package/skills/qingflow-app-builder/references/build-single-app.md +0 -138
|
@@ -71,20 +71,36 @@ class PublicExternalVisibilityMode(str, Enum):
|
|
|
71
71
|
|
|
72
72
|
|
|
73
73
|
FIELD_TYPE_ALIASES: dict[str, PublicFieldType] = {
|
|
74
|
-
"textarea": PublicFieldType.long_text,
|
|
75
74
|
"multiline": PublicFieldType.long_text,
|
|
76
|
-
"multi_line": PublicFieldType.long_text,
|
|
77
75
|
"multiline_text": PublicFieldType.long_text,
|
|
76
|
+
"multi_line": PublicFieldType.long_text,
|
|
77
|
+
"multi_line_text": PublicFieldType.long_text,
|
|
78
|
+
"textarea": PublicFieldType.long_text,
|
|
79
|
+
"longtext": PublicFieldType.long_text,
|
|
80
|
+
"long-text": PublicFieldType.long_text,
|
|
78
81
|
"amount": PublicFieldType.amount,
|
|
79
82
|
"currency": PublicFieldType.amount,
|
|
80
83
|
"mobile": PublicFieldType.phone,
|
|
81
84
|
"user": PublicFieldType.member,
|
|
82
85
|
"users": PublicFieldType.member,
|
|
83
86
|
"select": PublicFieldType.single_select,
|
|
87
|
+
"single_choice": PublicFieldType.single_select,
|
|
88
|
+
"single-choice": PublicFieldType.single_select,
|
|
89
|
+
"single choice": PublicFieldType.single_select,
|
|
90
|
+
"choice": PublicFieldType.single_select,
|
|
91
|
+
"dropdown": PublicFieldType.single_select,
|
|
84
92
|
"radio": PublicFieldType.single_select,
|
|
85
93
|
"checkbox": PublicFieldType.multi_select,
|
|
86
94
|
"multi_select": PublicFieldType.multi_select,
|
|
87
95
|
"multi-select": PublicFieldType.multi_select,
|
|
96
|
+
"multi select": PublicFieldType.multi_select,
|
|
97
|
+
"multiselect": PublicFieldType.multi_select,
|
|
98
|
+
"multi_choice": PublicFieldType.multi_select,
|
|
99
|
+
"multi-choice": PublicFieldType.multi_select,
|
|
100
|
+
"multi choice": PublicFieldType.multi_select,
|
|
101
|
+
"multiple_choice": PublicFieldType.multi_select,
|
|
102
|
+
"multiple-choice": PublicFieldType.multi_select,
|
|
103
|
+
"multiple choice": PublicFieldType.multi_select,
|
|
88
104
|
"departments": PublicFieldType.department,
|
|
89
105
|
"qlinker": PublicFieldType.q_linker,
|
|
90
106
|
"q_linker": PublicFieldType.q_linker,
|
|
@@ -187,6 +203,10 @@ class LayoutPreset(str, Enum):
|
|
|
187
203
|
single_section = "single_section"
|
|
188
204
|
|
|
189
205
|
|
|
206
|
+
class FlowPreset(str, Enum):
|
|
207
|
+
basic_approval = "basic_approval"
|
|
208
|
+
basic_fill_then_approve = "basic_fill_then_approve"
|
|
209
|
+
|
|
190
210
|
|
|
191
211
|
class ViewsPreset(str, Enum):
|
|
192
212
|
default_table = "default_table"
|
|
@@ -227,6 +247,116 @@ class ViewFilterOperator(str, Enum):
|
|
|
227
247
|
not_empty = "not_empty"
|
|
228
248
|
|
|
229
249
|
|
|
250
|
+
_PUBLIC_CONDITION_OPERATOR_ALIASES: dict[str, str] = {
|
|
251
|
+
"eq": "eq",
|
|
252
|
+
"equal": "eq",
|
|
253
|
+
"equals": "eq",
|
|
254
|
+
"=": "eq",
|
|
255
|
+
"==": "eq",
|
|
256
|
+
"neq": "neq",
|
|
257
|
+
"ne": "neq",
|
|
258
|
+
"not_eq": "neq",
|
|
259
|
+
"not_equal": "neq",
|
|
260
|
+
"not_equals": "neq",
|
|
261
|
+
"noteq": "neq",
|
|
262
|
+
"notequal": "neq",
|
|
263
|
+
"notequals": "neq",
|
|
264
|
+
"!=": "neq",
|
|
265
|
+
"in": "in",
|
|
266
|
+
"any": "in",
|
|
267
|
+
"any_of": "in",
|
|
268
|
+
"anyof": "in",
|
|
269
|
+
"one_of": "in",
|
|
270
|
+
"oneof": "in",
|
|
271
|
+
"between_any": "in",
|
|
272
|
+
"betweenany": "in",
|
|
273
|
+
"equal_any": "in",
|
|
274
|
+
"equalany": "in",
|
|
275
|
+
"any_match": "in",
|
|
276
|
+
"anymatch": "in",
|
|
277
|
+
"contains": "contains",
|
|
278
|
+
"contain": "contains",
|
|
279
|
+
"include": "contains",
|
|
280
|
+
"includes": "contains",
|
|
281
|
+
"like": "contains",
|
|
282
|
+
"fuzzy": "contains",
|
|
283
|
+
"fuzzy_match": "contains",
|
|
284
|
+
"fuzzymatch": "contains",
|
|
285
|
+
"gte": "gte",
|
|
286
|
+
"ge": "gte",
|
|
287
|
+
">=": "gte",
|
|
288
|
+
"greater_or_equal": "gte",
|
|
289
|
+
"greaterorequal": "gte",
|
|
290
|
+
"greater_than_or_equal": "gte",
|
|
291
|
+
"greaterthanorequal": "gte",
|
|
292
|
+
"lte": "lte",
|
|
293
|
+
"le": "lte",
|
|
294
|
+
"<=": "lte",
|
|
295
|
+
"less_or_equal": "lte",
|
|
296
|
+
"lessorequal": "lte",
|
|
297
|
+
"less_than_or_equal": "lte",
|
|
298
|
+
"lessthanorequal": "lte",
|
|
299
|
+
"is_empty": "is_empty",
|
|
300
|
+
"isempty": "is_empty",
|
|
301
|
+
"empty": "is_empty",
|
|
302
|
+
"blank": "is_empty",
|
|
303
|
+
"is_blank": "is_empty",
|
|
304
|
+
"isblank": "is_empty",
|
|
305
|
+
"null": "is_empty",
|
|
306
|
+
"is_null": "is_empty",
|
|
307
|
+
"isnull": "is_empty",
|
|
308
|
+
"not_empty": "not_empty",
|
|
309
|
+
"notempty": "not_empty",
|
|
310
|
+
"is_not_empty": "not_empty",
|
|
311
|
+
"isnotempty": "not_empty",
|
|
312
|
+
"not_blank": "not_empty",
|
|
313
|
+
"notblank": "not_empty",
|
|
314
|
+
"not_null": "not_empty",
|
|
315
|
+
"notnull": "not_empty",
|
|
316
|
+
"is_not_null": "not_empty",
|
|
317
|
+
"isnotnull": "not_empty",
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def normalize_public_condition_operator(raw_operator: Any, *, extra_aliases: dict[str, str] | None = None) -> Any:
|
|
322
|
+
if not isinstance(raw_operator, str):
|
|
323
|
+
return raw_operator
|
|
324
|
+
text = raw_operator.strip().lower()
|
|
325
|
+
if not text:
|
|
326
|
+
return text
|
|
327
|
+
lookup = dict(_PUBLIC_CONDITION_OPERATOR_ALIASES)
|
|
328
|
+
if extra_aliases:
|
|
329
|
+
lookup.update(extra_aliases)
|
|
330
|
+
candidates = [
|
|
331
|
+
text,
|
|
332
|
+
re.sub(r"[\s-]+", "_", text),
|
|
333
|
+
re.sub(r"[\s_-]+", "", text),
|
|
334
|
+
]
|
|
335
|
+
for candidate in candidates:
|
|
336
|
+
if candidate in lookup:
|
|
337
|
+
return lookup[candidate]
|
|
338
|
+
return text
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _normalize_public_condition_payload(value: Any) -> Any:
|
|
342
|
+
if not isinstance(value, dict):
|
|
343
|
+
return value
|
|
344
|
+
payload = dict(value)
|
|
345
|
+
if "value" in payload and "values" not in payload:
|
|
346
|
+
raw_value = payload.pop("value")
|
|
347
|
+
payload["values"] = list(raw_value) if isinstance(raw_value, list) else [raw_value]
|
|
348
|
+
elif "values" in payload and not isinstance(payload.get("values"), list):
|
|
349
|
+
payload["values"] = [payload.get("values")]
|
|
350
|
+
raw_operator = payload.get("operator", payload.get("op"))
|
|
351
|
+
if raw_operator is None:
|
|
352
|
+
payload["operator"] = "eq"
|
|
353
|
+
raw_operator = "eq"
|
|
354
|
+
if isinstance(raw_operator, str):
|
|
355
|
+
payload["operator"] = normalize_public_condition_operator(raw_operator)
|
|
356
|
+
payload.pop("op", None)
|
|
357
|
+
return payload
|
|
358
|
+
|
|
359
|
+
|
|
230
360
|
class FlowAssigneePatch(StrictModel):
|
|
231
361
|
role_ids: list[int] = Field(default_factory=list)
|
|
232
362
|
role_names: list[str] = Field(default_factory=list)
|
|
@@ -242,45 +372,13 @@ class FlowNodePermissionsPatch(StrictModel):
|
|
|
242
372
|
|
|
243
373
|
class FlowConditionRulePatch(StrictModel):
|
|
244
374
|
field_name: str = Field(validation_alias=AliasChoices("field_name", "fieldName", "field", "name"))
|
|
245
|
-
operator: FlowConditionOperator = Field(validation_alias=AliasChoices("operator", "op"))
|
|
375
|
+
operator: FlowConditionOperator = Field(default=FlowConditionOperator.eq, validation_alias=AliasChoices("operator", "op"))
|
|
246
376
|
values: list[Any] = Field(default_factory=list)
|
|
247
377
|
|
|
248
378
|
@model_validator(mode="before")
|
|
249
379
|
@classmethod
|
|
250
380
|
def normalize_aliases(cls, value: Any) -> Any:
|
|
251
|
-
|
|
252
|
-
return value
|
|
253
|
-
payload = dict(value)
|
|
254
|
-
if "value" in payload and "values" not in payload:
|
|
255
|
-
raw_value = payload.pop("value")
|
|
256
|
-
payload["values"] = list(raw_value) if isinstance(raw_value, list) else [raw_value]
|
|
257
|
-
raw_operator = payload.get("operator", payload.get("op"))
|
|
258
|
-
if isinstance(raw_operator, str):
|
|
259
|
-
normalized = raw_operator.strip().lower()
|
|
260
|
-
operator_aliases = {
|
|
261
|
-
"equals": FlowConditionOperator.eq.value,
|
|
262
|
-
"equal": FlowConditionOperator.eq.value,
|
|
263
|
-
"=": FlowConditionOperator.eq.value,
|
|
264
|
-
"not_equals": FlowConditionOperator.neq.value,
|
|
265
|
-
"not_equal": FlowConditionOperator.neq.value,
|
|
266
|
-
"!=": FlowConditionOperator.neq.value,
|
|
267
|
-
">=": FlowConditionOperator.gte.value,
|
|
268
|
-
"<=": FlowConditionOperator.lte.value,
|
|
269
|
-
"any_of": FlowConditionOperator.in_.value,
|
|
270
|
-
"one_of": FlowConditionOperator.in_.value,
|
|
271
|
-
"between_any": FlowConditionOperator.in_.value,
|
|
272
|
-
"empty": FlowConditionOperator.is_empty.value,
|
|
273
|
-
"is blank": FlowConditionOperator.is_empty.value,
|
|
274
|
-
"blank": FlowConditionOperator.is_empty.value,
|
|
275
|
-
"not_empty": FlowConditionOperator.not_empty.value,
|
|
276
|
-
"not blank": FlowConditionOperator.not_empty.value,
|
|
277
|
-
}
|
|
278
|
-
if normalized in operator_aliases:
|
|
279
|
-
payload["operator"] = operator_aliases[normalized]
|
|
280
|
-
elif "operator" not in payload:
|
|
281
|
-
payload["operator"] = normalized
|
|
282
|
-
payload.pop("op", None)
|
|
283
|
-
return payload
|
|
381
|
+
return _normalize_public_condition_payload(value)
|
|
284
382
|
|
|
285
383
|
@model_validator(mode="after")
|
|
286
384
|
def validate_shape(self) -> "FlowConditionRulePatch":
|
|
@@ -294,45 +392,13 @@ class FlowConditionRulePatch(StrictModel):
|
|
|
294
392
|
|
|
295
393
|
class ViewFilterRulePatch(StrictModel):
|
|
296
394
|
field_name: str = Field(validation_alias=AliasChoices("field_name", "fieldName", "field", "name"))
|
|
297
|
-
operator: ViewFilterOperator = Field(validation_alias=AliasChoices("operator", "op"))
|
|
395
|
+
operator: ViewFilterOperator = Field(default=ViewFilterOperator.eq, validation_alias=AliasChoices("operator", "op"))
|
|
298
396
|
values: list[Any] = Field(default_factory=list)
|
|
299
397
|
|
|
300
398
|
@model_validator(mode="before")
|
|
301
399
|
@classmethod
|
|
302
400
|
def normalize_aliases(cls, value: Any) -> Any:
|
|
303
|
-
|
|
304
|
-
return value
|
|
305
|
-
payload = dict(value)
|
|
306
|
-
if "value" in payload and "values" not in payload:
|
|
307
|
-
raw_value = payload.pop("value")
|
|
308
|
-
payload["values"] = list(raw_value) if isinstance(raw_value, list) else [raw_value]
|
|
309
|
-
raw_operator = payload.get("operator", payload.get("op"))
|
|
310
|
-
if isinstance(raw_operator, str):
|
|
311
|
-
normalized = raw_operator.strip().lower()
|
|
312
|
-
operator_aliases = {
|
|
313
|
-
"equals": ViewFilterOperator.eq.value,
|
|
314
|
-
"equal": ViewFilterOperator.eq.value,
|
|
315
|
-
"=": ViewFilterOperator.eq.value,
|
|
316
|
-
"not_equals": ViewFilterOperator.neq.value,
|
|
317
|
-
"not_equal": ViewFilterOperator.neq.value,
|
|
318
|
-
"!=": ViewFilterOperator.neq.value,
|
|
319
|
-
">=": ViewFilterOperator.gte.value,
|
|
320
|
-
"<=": ViewFilterOperator.lte.value,
|
|
321
|
-
"any_of": ViewFilterOperator.in_.value,
|
|
322
|
-
"one_of": ViewFilterOperator.in_.value,
|
|
323
|
-
"between_any": ViewFilterOperator.in_.value,
|
|
324
|
-
"empty": ViewFilterOperator.is_empty.value,
|
|
325
|
-
"is blank": ViewFilterOperator.is_empty.value,
|
|
326
|
-
"blank": ViewFilterOperator.is_empty.value,
|
|
327
|
-
"not_empty": ViewFilterOperator.not_empty.value,
|
|
328
|
-
"not blank": ViewFilterOperator.not_empty.value,
|
|
329
|
-
}
|
|
330
|
-
if normalized in operator_aliases:
|
|
331
|
-
payload["operator"] = operator_aliases[normalized]
|
|
332
|
-
elif "operator" not in payload:
|
|
333
|
-
payload["operator"] = normalized
|
|
334
|
-
payload.pop("op", None)
|
|
335
|
-
return payload
|
|
401
|
+
return _normalize_public_condition_payload(value)
|
|
336
402
|
|
|
337
403
|
@model_validator(mode="after")
|
|
338
404
|
def validate_shape(self) -> "ViewFilterRulePatch":
|
|
@@ -1085,6 +1151,91 @@ class ViewAssociatedResourcesPatch(StrictModel):
|
|
|
1085
1151
|
)
|
|
1086
1152
|
|
|
1087
1153
|
|
|
1154
|
+
class ViewActionButtonPatch(StrictModel):
|
|
1155
|
+
button_id: int | None = Field(default=None, validation_alias=AliasChoices("button_id", "buttonId", "id"))
|
|
1156
|
+
client_key: str | None = Field(default=None, validation_alias=AliasChoices("client_key", "clientKey"))
|
|
1157
|
+
text: str = Field(validation_alias=AliasChoices("text", "button_text", "buttonText", "name"))
|
|
1158
|
+
action: PublicButtonTriggerAction = Field(validation_alias=AliasChoices("action", "trigger_action", "triggerAction"))
|
|
1159
|
+
url: str | None = Field(default=None, validation_alias=AliasChoices("url", "link_url", "linkUrl", "trigger_link_url", "triggerLinkUrl"))
|
|
1160
|
+
target_app_key: str | None = Field(default=None, validation_alias=AliasChoices("target_app_key", "targetAppKey", "related_app_key", "relatedAppKey"))
|
|
1161
|
+
target_app_name: str | None = Field(default=None, validation_alias=AliasChoices("target_app_name", "targetAppName", "related_app_name", "relatedAppName"))
|
|
1162
|
+
field_mappings: list[dict[str, Any]] = Field(default_factory=list, validation_alias=AliasChoices("field_mappings", "fieldMappings", "mappings"))
|
|
1163
|
+
default_values: dict[str, Any] = Field(default_factory=dict, validation_alias=AliasChoices("default_values", "defaultValues", "defaults"))
|
|
1164
|
+
placement: PublicButtonPlacement = Field(default=PublicButtonPlacement.detail, validation_alias=AliasChoices("placement", "position"))
|
|
1165
|
+
primary: bool = Field(default=False, validation_alias=AliasChoices("primary", "being_main", "beingMain"))
|
|
1166
|
+
visible_when: list[list[ViewFilterRulePatch]] = Field(
|
|
1167
|
+
default_factory=list,
|
|
1168
|
+
validation_alias=AliasChoices("visible_when", "visibleWhen", "button_limit", "buttonLimit"),
|
|
1169
|
+
)
|
|
1170
|
+
button_formula: str | None = Field(default=None, validation_alias=AliasChoices("button_formula", "buttonFormula"))
|
|
1171
|
+
button_formula_type: int = Field(default=1, validation_alias=AliasChoices("button_formula_type", "buttonFormulaType"))
|
|
1172
|
+
print_tpls: list[Any] = Field(default_factory=list, validation_alias=AliasChoices("print_tpls", "printTpls"))
|
|
1173
|
+
style_preset: str | None = Field(default=None, validation_alias=AliasChoices("style_preset", "stylePreset"))
|
|
1174
|
+
background_color: str | None = Field(default=None, validation_alias=AliasChoices("background_color", "backgroundColor"))
|
|
1175
|
+
text_color: str | None = Field(default=None, validation_alias=AliasChoices("text_color", "textColor"))
|
|
1176
|
+
button_icon: str | None = Field(default=None, validation_alias=AliasChoices("button_icon", "buttonIcon"))
|
|
1177
|
+
external_qrobot_config: dict[str, Any] | None = Field(
|
|
1178
|
+
default=None,
|
|
1179
|
+
validation_alias=AliasChoices(
|
|
1180
|
+
"external_qrobot_config",
|
|
1181
|
+
"externalQrobotConfig",
|
|
1182
|
+
"externalQRobotConfig",
|
|
1183
|
+
"custom_button_external_qrobot_relation_vo",
|
|
1184
|
+
"customButtonExternalQRobotRelationVO",
|
|
1185
|
+
),
|
|
1186
|
+
)
|
|
1187
|
+
trigger_wings_config: dict[str, Any] | None = Field(default=None, validation_alias=AliasChoices("trigger_wings_config", "triggerWingsConfig"))
|
|
1188
|
+
|
|
1189
|
+
@model_validator(mode="before")
|
|
1190
|
+
@classmethod
|
|
1191
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
1192
|
+
if not isinstance(value, dict):
|
|
1193
|
+
return value
|
|
1194
|
+
payload = dict(value)
|
|
1195
|
+
raw_action = payload.get("action", payload.get("trigger_action", payload.get("triggerAction")))
|
|
1196
|
+
if isinstance(raw_action, str):
|
|
1197
|
+
normalized = raw_action.strip()
|
|
1198
|
+
aliases = {
|
|
1199
|
+
"add_data": PublicButtonTriggerAction.add_data.value,
|
|
1200
|
+
"add-data": PublicButtonTriggerAction.add_data.value,
|
|
1201
|
+
"adddata": PublicButtonTriggerAction.add_data.value,
|
|
1202
|
+
"addData": PublicButtonTriggerAction.add_data.value,
|
|
1203
|
+
"link": PublicButtonTriggerAction.link.value,
|
|
1204
|
+
"url": PublicButtonTriggerAction.link.value,
|
|
1205
|
+
"qrobot": PublicButtonTriggerAction.qrobot.value,
|
|
1206
|
+
"qRobot": PublicButtonTriggerAction.qrobot.value,
|
|
1207
|
+
"wings": PublicButtonTriggerAction.wings.value,
|
|
1208
|
+
}
|
|
1209
|
+
payload["action"] = aliases.get(normalized, aliases.get(normalized.lower(), normalized))
|
|
1210
|
+
raw_placement = payload.get("placement", payload.get("position", payload.get("config_type", payload.get("configType"))))
|
|
1211
|
+
if isinstance(raw_placement, str):
|
|
1212
|
+
normalized = raw_placement.strip().lower()
|
|
1213
|
+
if normalized in {"top", "header"}:
|
|
1214
|
+
payload["placement"] = PublicButtonPlacement.header.value
|
|
1215
|
+
elif normalized in {"detail", "data_detail"}:
|
|
1216
|
+
payload["placement"] = PublicButtonPlacement.detail.value
|
|
1217
|
+
elif normalized in {"list", "row", "row_action", "inside"}:
|
|
1218
|
+
payload["placement"] = PublicButtonPlacement.list.value
|
|
1219
|
+
raw_limits = payload.get("visible_when", payload.get("visibleWhen", payload.get("button_limit", payload.get("buttonLimit"))))
|
|
1220
|
+
if isinstance(raw_limits, list) and raw_limits and all(isinstance(item, dict) for item in raw_limits):
|
|
1221
|
+
payload["visible_when"] = [raw_limits]
|
|
1222
|
+
return payload
|
|
1223
|
+
|
|
1224
|
+
@model_validator(mode="after")
|
|
1225
|
+
def validate_shape(self) -> "ViewActionButtonPatch":
|
|
1226
|
+
if not str(self.text or "").strip():
|
|
1227
|
+
raise ValueError("action_buttons[].text is required")
|
|
1228
|
+
if self.action == PublicButtonTriggerAction.link and not str(self.url or "").strip():
|
|
1229
|
+
raise ValueError("link action_buttons require url")
|
|
1230
|
+
if self.action == PublicButtonTriggerAction.add_data and not str(self.target_app_key or "").strip():
|
|
1231
|
+
raise ValueError("add_data action_buttons require target_app_key")
|
|
1232
|
+
if self.action == PublicButtonTriggerAction.qrobot and self.external_qrobot_config is None:
|
|
1233
|
+
raise ValueError("qRobot action_buttons require external_qrobot_config")
|
|
1234
|
+
if self.action == PublicButtonTriggerAction.wings and self.trigger_wings_config is None:
|
|
1235
|
+
raise ValueError("wings action_buttons require trigger_wings_config")
|
|
1236
|
+
return self
|
|
1237
|
+
|
|
1238
|
+
|
|
1088
1239
|
class ViewUpsertPatch(StrictModel):
|
|
1089
1240
|
name: str
|
|
1090
1241
|
view_key: str | None = Field(default=None, validation_alias=AliasChoices("view_key", "viewKey"))
|
|
@@ -1096,6 +1247,8 @@ class ViewUpsertPatch(StrictModel):
|
|
|
1096
1247
|
end_field: str | None = Field(default=None, validation_alias=AliasChoices("end_field", "endField"))
|
|
1097
1248
|
title_field: str | None = Field(default=None, validation_alias=AliasChoices("title_field", "titleField"))
|
|
1098
1249
|
buttons: list["ViewButtonBindingPatch"] | None = None
|
|
1250
|
+
action_buttons: list[ViewActionButtonPatch] | None = Field(default=None, validation_alias=AliasChoices("action_buttons", "actionButtons"))
|
|
1251
|
+
action_buttons_mode: str = Field(default="merge", validation_alias=AliasChoices("action_buttons_mode", "actionButtonsMode", "button_mode", "buttonMode"))
|
|
1099
1252
|
visibility: VisibilityPatch | None = None
|
|
1100
1253
|
query_conditions: ViewQueryConditionsPatch | None = Field(default=None, validation_alias=AliasChoices("query_conditions", "queryConditions", "query_condition", "queryCondition"))
|
|
1101
1254
|
associated_resources: ViewAssociatedResourcesPatch | None = Field(
|
|
@@ -1145,6 +1298,12 @@ class ViewUpsertPatch(StrictModel):
|
|
|
1145
1298
|
payload["associated_resources"] = payload.pop("associatedReports")
|
|
1146
1299
|
if "asosChartConfig" in payload and "associated_resources" not in payload:
|
|
1147
1300
|
payload["associated_resources"] = payload.pop("asosChartConfig")
|
|
1301
|
+
if "actionButtons" in payload and "action_buttons" not in payload:
|
|
1302
|
+
payload["action_buttons"] = payload.pop("actionButtons")
|
|
1303
|
+
if "actionButtonsMode" in payload and "action_buttons_mode" not in payload:
|
|
1304
|
+
payload["action_buttons_mode"] = payload.pop("actionButtonsMode")
|
|
1305
|
+
if "buttonMode" in payload and "action_buttons_mode" not in payload:
|
|
1306
|
+
payload["action_buttons_mode"] = payload.pop("buttonMode")
|
|
1148
1307
|
raw_type = payload.get("type")
|
|
1149
1308
|
if isinstance(raw_type, str):
|
|
1150
1309
|
normalized = raw_type.strip().lower()
|
|
@@ -1166,6 +1325,10 @@ class ViewUpsertPatch(StrictModel):
|
|
|
1166
1325
|
raise ValueError("board view requires group_by")
|
|
1167
1326
|
if self.type == PublicViewType.gantt and not (self.start_field and self.end_field):
|
|
1168
1327
|
raise ValueError("gantt view requires start_field and end_field")
|
|
1328
|
+
normalized_mode = str(self.action_buttons_mode or "merge").strip().lower()
|
|
1329
|
+
if normalized_mode not in {"merge", "replace"}:
|
|
1330
|
+
raise ValueError("action_buttons_mode must be merge or replace")
|
|
1331
|
+
self.action_buttons_mode = normalized_mode
|
|
1169
1332
|
return self
|
|
1170
1333
|
|
|
1171
1334
|
|
|
@@ -1191,6 +1354,76 @@ class ViewPartialPatch(StrictModel):
|
|
|
1191
1354
|
return self
|
|
1192
1355
|
|
|
1193
1356
|
|
|
1357
|
+
def public_button_trigger_action_value(value: Any) -> str:
|
|
1358
|
+
raw = value.value if isinstance(value, PublicButtonTriggerAction) else str(value or "").strip()
|
|
1359
|
+
normalized = raw.lower()
|
|
1360
|
+
if raw == PublicButtonTriggerAction.add_data.value or normalized in {"add_data", "add-data", "adddata"}:
|
|
1361
|
+
return "add_data"
|
|
1362
|
+
if normalized in {PublicButtonTriggerAction.link.value, "url"}:
|
|
1363
|
+
return "link"
|
|
1364
|
+
if raw == PublicButtonTriggerAction.qrobot.value or normalized == "qrobot":
|
|
1365
|
+
return "qRobot"
|
|
1366
|
+
if normalized == PublicButtonTriggerAction.wings.value:
|
|
1367
|
+
return "wings"
|
|
1368
|
+
return raw
|
|
1369
|
+
|
|
1370
|
+
|
|
1371
|
+
def _drop_empty_public_payload_values(payload: dict[str, Any]) -> dict[str, Any]:
|
|
1372
|
+
compact: dict[str, Any] = {}
|
|
1373
|
+
for key, item in payload.items():
|
|
1374
|
+
if item is None or item == "" or item == [] or item == {}:
|
|
1375
|
+
continue
|
|
1376
|
+
if key == "primary" and item is False:
|
|
1377
|
+
continue
|
|
1378
|
+
compact[key] = item
|
|
1379
|
+
return compact
|
|
1380
|
+
|
|
1381
|
+
|
|
1382
|
+
def public_view_action_button_payload(button: Any, *, compact: bool = False) -> dict[str, Any]:
|
|
1383
|
+
if isinstance(button, ViewActionButtonPatch):
|
|
1384
|
+
payload = button.model_dump(mode="json")
|
|
1385
|
+
elif isinstance(button, dict):
|
|
1386
|
+
payload = dict(button)
|
|
1387
|
+
else:
|
|
1388
|
+
return {}
|
|
1389
|
+
raw_action = payload.get("action", payload.get("trigger_action", payload.get("triggerAction")))
|
|
1390
|
+
if raw_action is not None:
|
|
1391
|
+
payload["action"] = public_button_trigger_action_value(raw_action)
|
|
1392
|
+
payload.pop("trigger_action", None)
|
|
1393
|
+
payload.pop("triggerAction", None)
|
|
1394
|
+
if compact:
|
|
1395
|
+
return _drop_empty_public_payload_values(payload)
|
|
1396
|
+
return payload
|
|
1397
|
+
|
|
1398
|
+
|
|
1399
|
+
def public_view_upsert_payload(view: ViewUpsertPatch) -> dict[str, Any]:
|
|
1400
|
+
payload = view.model_dump(mode="json")
|
|
1401
|
+
if view.action_buttons is not None:
|
|
1402
|
+
payload["action_buttons"] = [
|
|
1403
|
+
public_view_action_button_payload(button, compact=True)
|
|
1404
|
+
for button in view.action_buttons
|
|
1405
|
+
]
|
|
1406
|
+
return payload
|
|
1407
|
+
|
|
1408
|
+
|
|
1409
|
+
def public_view_partial_payload(patch: ViewPartialPatch) -> dict[str, Any]:
|
|
1410
|
+
payload = patch.model_dump(mode="json")
|
|
1411
|
+
patch_set = payload.get("set")
|
|
1412
|
+
if isinstance(patch_set, dict):
|
|
1413
|
+
raw_buttons = patch_set.get("action_buttons")
|
|
1414
|
+
if raw_buttons is None:
|
|
1415
|
+
raw_buttons = patch_set.get("actionButtons")
|
|
1416
|
+
if isinstance(raw_buttons, list):
|
|
1417
|
+
patch_set = dict(patch_set)
|
|
1418
|
+
patch_set.pop("actionButtons", None)
|
|
1419
|
+
patch_set["action_buttons"] = [
|
|
1420
|
+
public_view_action_button_payload(button, compact=True)
|
|
1421
|
+
for button in raw_buttons
|
|
1422
|
+
]
|
|
1423
|
+
payload["set"] = patch_set
|
|
1424
|
+
return payload
|
|
1425
|
+
|
|
1426
|
+
|
|
1194
1427
|
class CustomButtonJudgeValuePatch(StrictModel):
|
|
1195
1428
|
id: int | str | None = None
|
|
1196
1429
|
value: Any | None = None
|
|
@@ -1259,13 +1492,39 @@ class CustomButtonFieldMappingPatch(StrictModel):
|
|
|
1259
1492
|
|
|
1260
1493
|
|
|
1261
1494
|
class FieldMatchMappingPatch(StrictModel):
|
|
1262
|
-
target_field: Any = Field(validation_alias=AliasChoices("target_field", "targetField", "target", "field"))
|
|
1495
|
+
target_field: Any = Field(validation_alias=AliasChoices("target_field", "targetField", "target", "field", "field_name", "fieldName", "name"))
|
|
1263
1496
|
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"))
|
|
1497
|
+
value: Any | None = Field(default=None, validation_alias=AliasChoices("value", "values", "static_value", "staticValue"))
|
|
1265
1498
|
operator: str = Field(default="eq", validation_alias=AliasChoices("operator", "op", "judge_type", "judgeType"))
|
|
1266
1499
|
|
|
1500
|
+
@model_validator(mode="before")
|
|
1501
|
+
@classmethod
|
|
1502
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
1503
|
+
if not isinstance(value, dict):
|
|
1504
|
+
return value
|
|
1505
|
+
payload = dict(value)
|
|
1506
|
+
if "values" in payload and "value" not in payload:
|
|
1507
|
+
raw_values = payload.pop("values")
|
|
1508
|
+
if isinstance(raw_values, list) and len(raw_values) == 1:
|
|
1509
|
+
payload["value"] = raw_values[0]
|
|
1510
|
+
else:
|
|
1511
|
+
payload["value"] = raw_values
|
|
1512
|
+
raw_operator = payload.get("operator", payload.get("op", payload.get("judge_type", payload.get("judgeType"))))
|
|
1513
|
+
if isinstance(raw_operator, str):
|
|
1514
|
+
payload["operator"] = normalize_public_condition_operator(
|
|
1515
|
+
raw_operator,
|
|
1516
|
+
extra_aliases={"include_any": "include_any", "includes_any": "include_any", "includeany": "include_any"},
|
|
1517
|
+
)
|
|
1518
|
+
for alias in ("op", "judge_type", "judgeType"):
|
|
1519
|
+
payload.pop(alias, None)
|
|
1520
|
+
return payload
|
|
1521
|
+
|
|
1267
1522
|
@model_validator(mode="after")
|
|
1268
1523
|
def validate_shape(self) -> "FieldMatchMappingPatch":
|
|
1524
|
+
if str(self.operator or "").strip().lower() in {"is_empty", "not_empty"}:
|
|
1525
|
+
self.source_field = None
|
|
1526
|
+
self.value = None
|
|
1527
|
+
return self
|
|
1269
1528
|
has_source = self.source_field is not None and str(self.source_field).strip() != ""
|
|
1270
1529
|
has_value = self.value is not None
|
|
1271
1530
|
if has_source == has_value:
|
|
@@ -1459,9 +1718,6 @@ class CustomButtonViewButtonBindingPatch(StrictModel):
|
|
|
1459
1718
|
raw_limits = payload.get("button_limit", payload.get("buttonLimit", payload.get("visible_when", payload.get("visibleWhen"))))
|
|
1460
1719
|
if isinstance(raw_limits, list) and raw_limits and all(isinstance(item, dict) for item in raw_limits):
|
|
1461
1720
|
payload["button_limit"] = [raw_limits]
|
|
1462
|
-
for alias in ("buttonLimit", "visible_when", "visibleWhen"):
|
|
1463
|
-
if alias in payload and alias != "button_limit":
|
|
1464
|
-
payload.pop(alias, None)
|
|
1465
1721
|
return payload
|
|
1466
1722
|
|
|
1467
1723
|
|
|
@@ -1613,6 +1869,7 @@ class AssociatedResourcesApplyRequest(StrictModel):
|
|
|
1613
1869
|
if not isinstance(value, dict):
|
|
1614
1870
|
return value
|
|
1615
1871
|
payload = dict(value)
|
|
1872
|
+
default_target_app_key = str(payload.get("app_key", payload.get("appKey", "")) or "").strip()
|
|
1616
1873
|
if "upsertResources" in payload and "upsert_resources" not in payload:
|
|
1617
1874
|
payload["upsert_resources"] = payload.pop("upsertResources")
|
|
1618
1875
|
if "patchResources" in payload and "patch_resources" not in payload:
|
|
@@ -1625,6 +1882,16 @@ class AssociatedResourcesApplyRequest(StrictModel):
|
|
|
1625
1882
|
payload["reorder_associated_item_ids"] = payload.pop("reorderAssociatedItemIds")
|
|
1626
1883
|
if "viewConfigs" in payload and "view_configs" not in payload:
|
|
1627
1884
|
payload["view_configs"] = payload.pop("viewConfigs")
|
|
1885
|
+
if default_target_app_key and isinstance(payload.get("upsert_resources"), list):
|
|
1886
|
+
normalized_resources = []
|
|
1887
|
+
for item in payload["upsert_resources"]:
|
|
1888
|
+
if isinstance(item, dict) and not any(
|
|
1889
|
+
str(item.get(key) or "").strip()
|
|
1890
|
+
for key in ("target_app_key", "targetAppKey", "app_key", "appKey")
|
|
1891
|
+
):
|
|
1892
|
+
item = {**item, "target_app_key": default_target_app_key}
|
|
1893
|
+
normalized_resources.append(item)
|
|
1894
|
+
payload["upsert_resources"] = normalized_resources
|
|
1628
1895
|
return payload
|
|
1629
1896
|
|
|
1630
1897
|
@model_validator(mode="after")
|
|
@@ -1648,7 +1915,7 @@ class ViewButtonBindingPatch(StrictModel):
|
|
|
1648
1915
|
being_main: bool = Field(default=False, validation_alias=AliasChoices("being_main", "beingMain"))
|
|
1649
1916
|
button_limit: list[list[ViewFilterRulePatch]] = Field(
|
|
1650
1917
|
default_factory=list,
|
|
1651
|
-
validation_alias=AliasChoices("button_limit", "buttonLimit"
|
|
1918
|
+
validation_alias=AliasChoices("button_limit", "buttonLimit"),
|
|
1652
1919
|
)
|
|
1653
1920
|
button_formula: str | None = Field(default=None, validation_alias=AliasChoices("button_formula", "buttonFormula"))
|
|
1654
1921
|
button_formula_type: int = Field(default=1, validation_alias=AliasChoices("button_formula_type", "buttonFormulaType"))
|
|
@@ -1677,12 +1944,9 @@ class ViewButtonBindingPatch(StrictModel):
|
|
|
1677
1944
|
payload["config_type"] = "DETAIL"
|
|
1678
1945
|
elif normalized_config in {"inside", "list", "row", "row_action"}:
|
|
1679
1946
|
payload["config_type"] = "INSIDE"
|
|
1680
|
-
raw_limits = payload.get("button_limit", payload.get("buttonLimit"
|
|
1947
|
+
raw_limits = payload.get("button_limit", payload.get("buttonLimit"))
|
|
1681
1948
|
if isinstance(raw_limits, list) and raw_limits and all(isinstance(item, dict) for item in raw_limits):
|
|
1682
1949
|
payload["button_limit"] = [raw_limits]
|
|
1683
|
-
for alias in ("buttonLimit", "visible_when", "visibleWhen"):
|
|
1684
|
-
if alias in payload and alias != "button_limit":
|
|
1685
|
-
payload.pop(alias, None)
|
|
1686
1950
|
return payload
|
|
1687
1951
|
|
|
1688
1952
|
@model_validator(mode="after")
|
|
@@ -1722,45 +1986,13 @@ class ViewButtonBindingPatch(StrictModel):
|
|
|
1722
1986
|
|
|
1723
1987
|
class ChartFilterRulePatch(StrictModel):
|
|
1724
1988
|
field_name: str = Field(validation_alias=AliasChoices("field_name", "fieldName", "field", "name"))
|
|
1725
|
-
operator: ViewFilterOperator = Field(validation_alias=AliasChoices("operator", "op"))
|
|
1989
|
+
operator: ViewFilterOperator = Field(default=ViewFilterOperator.eq, validation_alias=AliasChoices("operator", "op"))
|
|
1726
1990
|
values: list[Any] = Field(default_factory=list)
|
|
1727
1991
|
|
|
1728
1992
|
@model_validator(mode="before")
|
|
1729
1993
|
@classmethod
|
|
1730
1994
|
def normalize_aliases(cls, value: Any) -> Any:
|
|
1731
|
-
|
|
1732
|
-
return value
|
|
1733
|
-
payload = dict(value)
|
|
1734
|
-
if "value" in payload and "values" not in payload:
|
|
1735
|
-
raw_value = payload.pop("value")
|
|
1736
|
-
payload["values"] = list(raw_value) if isinstance(raw_value, list) else [raw_value]
|
|
1737
|
-
raw_operator = payload.get("operator", payload.get("op"))
|
|
1738
|
-
if isinstance(raw_operator, str):
|
|
1739
|
-
normalized = raw_operator.strip().lower()
|
|
1740
|
-
operator_aliases = {
|
|
1741
|
-
"equals": ViewFilterOperator.eq.value,
|
|
1742
|
-
"equal": ViewFilterOperator.eq.value,
|
|
1743
|
-
"=": ViewFilterOperator.eq.value,
|
|
1744
|
-
"not_equals": ViewFilterOperator.neq.value,
|
|
1745
|
-
"not_equal": ViewFilterOperator.neq.value,
|
|
1746
|
-
"!=": ViewFilterOperator.neq.value,
|
|
1747
|
-
">=": ViewFilterOperator.gte.value,
|
|
1748
|
-
"<=": ViewFilterOperator.lte.value,
|
|
1749
|
-
"any_of": ViewFilterOperator.in_.value,
|
|
1750
|
-
"one_of": ViewFilterOperator.in_.value,
|
|
1751
|
-
"between_any": ViewFilterOperator.in_.value,
|
|
1752
|
-
"empty": ViewFilterOperator.is_empty.value,
|
|
1753
|
-
"is blank": ViewFilterOperator.is_empty.value,
|
|
1754
|
-
"blank": ViewFilterOperator.is_empty.value,
|
|
1755
|
-
"not_empty": ViewFilterOperator.not_empty.value,
|
|
1756
|
-
"not blank": ViewFilterOperator.not_empty.value,
|
|
1757
|
-
}
|
|
1758
|
-
if normalized in operator_aliases:
|
|
1759
|
-
payload["operator"] = operator_aliases[normalized]
|
|
1760
|
-
elif "operator" not in payload:
|
|
1761
|
-
payload["operator"] = normalized
|
|
1762
|
-
payload.pop("op", None)
|
|
1763
|
-
return payload
|
|
1995
|
+
return _normalize_public_condition_payload(value)
|
|
1764
1996
|
|
|
1765
1997
|
@model_validator(mode="after")
|
|
1766
1998
|
def validate_shape(self) -> "ChartFilterRulePatch":
|
|
@@ -2049,6 +2281,22 @@ class ChartApplyRequest(StrictModel):
|
|
|
2049
2281
|
return self
|
|
2050
2282
|
|
|
2051
2283
|
|
|
2284
|
+
class PortalInlineChartPatch(ChartUpsertPatch):
|
|
2285
|
+
app_key: str = Field(validation_alias=AliasChoices("app_key", "appKey"))
|
|
2286
|
+
|
|
2287
|
+
@model_validator(mode="before")
|
|
2288
|
+
@classmethod
|
|
2289
|
+
def normalize_inline_aliases(cls, value: Any) -> Any:
|
|
2290
|
+
if not isinstance(value, dict):
|
|
2291
|
+
return value
|
|
2292
|
+
payload = dict(value)
|
|
2293
|
+
if "chart_name" in payload and "name" not in payload:
|
|
2294
|
+
payload["name"] = payload.pop("chart_name")
|
|
2295
|
+
if "chartName" in payload and "name" not in payload:
|
|
2296
|
+
payload["name"] = payload.pop("chartName")
|
|
2297
|
+
return payload
|
|
2298
|
+
|
|
2299
|
+
|
|
2052
2300
|
class PortalComponentPositionPatch(StrictModel):
|
|
2053
2301
|
pc_x: int = Field(default=0, validation_alias=AliasChoices("pc_x", "pcX", "x"))
|
|
2054
2302
|
pc_y: int = Field(default=0, validation_alias=AliasChoices("pc_y", "pcY", "y"))
|
|
@@ -2095,7 +2343,6 @@ class PortalComponentPositionPatch(StrictModel):
|
|
|
2095
2343
|
class PortalChartRefPatch(StrictModel):
|
|
2096
2344
|
app_key: str | None = None
|
|
2097
2345
|
chart_id: str | None = None
|
|
2098
|
-
chart_key: str | None = None
|
|
2099
2346
|
chart_name: str | None = None
|
|
2100
2347
|
|
|
2101
2348
|
@model_validator(mode="before")
|
|
@@ -2104,24 +2351,26 @@ class PortalChartRefPatch(StrictModel):
|
|
|
2104
2351
|
if not isinstance(value, dict):
|
|
2105
2352
|
return value
|
|
2106
2353
|
payload = dict(value)
|
|
2107
|
-
if "chartKey" in payload and "chart_key" not in payload:
|
|
2108
|
-
payload["chart_key"] = payload.pop("chartKey")
|
|
2109
2354
|
if "chartId" in payload and "chart_id" not in payload:
|
|
2110
2355
|
payload["chart_id"] = payload.pop("chartId")
|
|
2111
|
-
if "
|
|
2112
|
-
payload["chart_id"] = payload.pop("
|
|
2356
|
+
if "chart_key" in payload and "chart_id" not in payload:
|
|
2357
|
+
payload["chart_id"] = payload.pop("chart_key")
|
|
2358
|
+
if "chartKey" in payload and "chart_id" not in payload:
|
|
2359
|
+
payload["chart_id"] = payload.pop("chartKey")
|
|
2113
2360
|
if "chartName" in payload and "chart_name" not in payload:
|
|
2114
2361
|
payload["chart_name"] = payload.pop("chartName")
|
|
2362
|
+
if "name" in payload and "chart_name" not in payload:
|
|
2363
|
+
payload["chart_name"] = payload.pop("name")
|
|
2115
2364
|
if "appKey" in payload and "app_key" not in payload:
|
|
2116
2365
|
payload["app_key"] = payload.pop("appKey")
|
|
2117
2366
|
return payload
|
|
2118
2367
|
|
|
2119
2368
|
@model_validator(mode="after")
|
|
2120
2369
|
def validate_target(self) -> "PortalChartRefPatch":
|
|
2121
|
-
if not (self.chart_id or self.
|
|
2122
|
-
raise ValueError("chart_ref requires chart_id
|
|
2123
|
-
if self.chart_name and not self.
|
|
2124
|
-
raise ValueError("chart_ref
|
|
2370
|
+
if not (self.chart_id or self.chart_name):
|
|
2371
|
+
raise ValueError("chart_ref requires chart_id or chart_name")
|
|
2372
|
+
if self.chart_name and not self.chart_id and not self.app_key:
|
|
2373
|
+
raise ValueError("chart_ref.app_key is required when resolving by chart_name")
|
|
2125
2374
|
return self
|
|
2126
2375
|
|
|
2127
2376
|
|
|
@@ -2147,12 +2396,12 @@ class PortalViewRefPatch(StrictModel):
|
|
|
2147
2396
|
class PortalSectionPatch(StrictModel):
|
|
2148
2397
|
title: str
|
|
2149
2398
|
source_type: str = Field(validation_alias=AliasChoices("source_type", "sourceType"))
|
|
2150
|
-
role: str | None = None
|
|
2399
|
+
role: str | None = Field(default=None, validation_alias=AliasChoices("role", "zone", "section_role", "sectionRole"))
|
|
2151
2400
|
position: PortalComponentPositionPatch | None = None
|
|
2152
2401
|
dash_style_config: dict[str, Any] | None = Field(default=None, validation_alias=AliasChoices("dash_style_config", "dashStyleConfigBO"))
|
|
2153
2402
|
config: dict[str, Any] = Field(default_factory=dict)
|
|
2154
|
-
chart: dict[str, Any] | None = None
|
|
2155
2403
|
chart_ref: PortalChartRefPatch | None = None
|
|
2404
|
+
chart: PortalInlineChartPatch | None = Field(default=None, validation_alias=AliasChoices("chart", "inline_chart", "inlineChart", "create_chart", "createChart"))
|
|
2156
2405
|
view_ref: PortalViewRefPatch | None = None
|
|
2157
2406
|
text: str | None = None
|
|
2158
2407
|
url: str | None = None
|
|
@@ -2166,8 +2415,32 @@ class PortalSectionPatch(StrictModel):
|
|
|
2166
2415
|
raw_type = payload.get("source_type", payload.get("sourceType"))
|
|
2167
2416
|
if isinstance(raw_type, str):
|
|
2168
2417
|
payload["source_type"] = raw_type.strip().lower()
|
|
2418
|
+
raw_role = payload.get("role", payload.get("zone", payload.get("section_role", payload.get("sectionRole"))))
|
|
2419
|
+
if isinstance(raw_role, str):
|
|
2420
|
+
payload["role"] = raw_role.strip().lower()
|
|
2169
2421
|
if "chartRef" in payload and "chart_ref" not in payload:
|
|
2170
2422
|
payload["chart_ref"] = payload.pop("chartRef")
|
|
2423
|
+
if "inlineChart" in payload and "chart" not in payload:
|
|
2424
|
+
payload["chart"] = payload.pop("inlineChart")
|
|
2425
|
+
if "inline_chart" in payload and "chart" not in payload:
|
|
2426
|
+
payload["chart"] = payload.pop("inline_chart")
|
|
2427
|
+
if "createChart" in payload and "chart" not in payload:
|
|
2428
|
+
payload["chart"] = payload.pop("createChart")
|
|
2429
|
+
if "create_chart" in payload and "chart" not in payload:
|
|
2430
|
+
payload["chart"] = payload.pop("create_chart")
|
|
2431
|
+
raw_chart = payload.get("chart")
|
|
2432
|
+
if isinstance(raw_chart, dict):
|
|
2433
|
+
chart_payload = dict(raw_chart)
|
|
2434
|
+
has_ref_id = any(key in chart_payload for key in ("chart_id", "chartId", "chart_key", "chartKey"))
|
|
2435
|
+
has_chart_type = any(key in chart_payload for key in ("chart_type", "chartType"))
|
|
2436
|
+
has_create_config = has_chart_type or any(
|
|
2437
|
+
key in chart_payload
|
|
2438
|
+
for key in ("metric", "metrics", "group_by", "groupBy", "where", "filters", "dimensions")
|
|
2439
|
+
)
|
|
2440
|
+
has_ref_name = any(key in chart_payload for key in ("chart_name", "chartName", "name"))
|
|
2441
|
+
if has_ref_id or (has_ref_name and not has_create_config):
|
|
2442
|
+
payload["chart_ref"] = chart_payload
|
|
2443
|
+
payload.pop("chart", None)
|
|
2171
2444
|
if "viewRef" in payload and "view_ref" not in payload:
|
|
2172
2445
|
payload["view_ref"] = payload.pop("viewRef")
|
|
2173
2446
|
if "dashStyleConfigBO" in payload and "dash_style_config" not in payload:
|
|
@@ -2179,18 +2452,8 @@ class PortalSectionPatch(StrictModel):
|
|
|
2179
2452
|
supported = {"chart", "view", "grid", "filter", "text", "link"}
|
|
2180
2453
|
if self.source_type not in supported:
|
|
2181
2454
|
raise ValueError(f"unsupported portal source_type '{self.source_type}'")
|
|
2182
|
-
if self.
|
|
2183
|
-
|
|
2184
|
-
if normalized_role not in {"metric"}:
|
|
2185
|
-
raise ValueError(f"unsupported portal section role '{self.role}'")
|
|
2186
|
-
self.role = normalized_role
|
|
2187
|
-
if self.source_type != "chart":
|
|
2188
|
-
raise ValueError("portal section role is only supported for chart sections")
|
|
2189
|
-
if self.source_type == "chart":
|
|
2190
|
-
if self.chart is not None and self.chart_ref is not None:
|
|
2191
|
-
raise ValueError("chart section accepts either chart or chart_ref, not both")
|
|
2192
|
-
if self.chart is None and self.chart_ref is None:
|
|
2193
|
-
raise ValueError("chart section requires chart or chart_ref")
|
|
2455
|
+
if self.source_type == "chart" and self.chart_ref is None and self.chart is None:
|
|
2456
|
+
raise ValueError("chart section requires chart_ref or chart")
|
|
2194
2457
|
if self.source_type == "view" and self.view_ref is None:
|
|
2195
2458
|
raise ValueError("view section requires view_ref")
|
|
2196
2459
|
if self.source_type == "text" and self.text is None:
|
|
@@ -2327,6 +2590,7 @@ AppReadSummaryResponse = AppGetResponse
|
|
|
2327
2590
|
AppFieldsReadResponse = AppGetFieldsResponse
|
|
2328
2591
|
AppLayoutReadResponse = AppGetLayoutResponse
|
|
2329
2592
|
AppViewsReadResponse = AppGetViewsResponse
|
|
2593
|
+
AppFlowReadResponse = AppGetFlowResponse
|
|
2330
2594
|
AppChartsReadResponse = AppGetChartsResponse
|
|
2331
2595
|
|
|
2332
2596
|
|
|
@@ -2393,7 +2657,6 @@ class SchemaPlanRequest(StrictModel):
|
|
|
2393
2657
|
icon: str | None = None
|
|
2394
2658
|
color: str | None = None
|
|
2395
2659
|
visibility: VisibilityPatch | None = None
|
|
2396
|
-
create_if_missing: bool = False
|
|
2397
2660
|
add_fields: list[FieldPatch] = Field(default_factory=list)
|
|
2398
2661
|
update_fields: list[FieldUpdatePatch] = Field(default_factory=list)
|
|
2399
2662
|
remove_fields: list[FieldRemovePatch] = Field(default_factory=list)
|
|
@@ -2416,6 +2679,40 @@ class LayoutPlanRequest(StrictModel):
|
|
|
2416
2679
|
return payload
|
|
2417
2680
|
|
|
2418
2681
|
|
|
2682
|
+
class FlowPlanRequest(StrictModel):
|
|
2683
|
+
app_key: str
|
|
2684
|
+
mode: str = "replace"
|
|
2685
|
+
nodes: list[FlowNodePatch] = Field(default_factory=list)
|
|
2686
|
+
transitions: list[FlowTransitionPatch] = Field(default_factory=list)
|
|
2687
|
+
preset: FlowPreset | None = None
|
|
2688
|
+
|
|
2689
|
+
@model_validator(mode="before")
|
|
2690
|
+
@classmethod
|
|
2691
|
+
def normalize_mode_alias(cls, value: Any) -> Any:
|
|
2692
|
+
if not isinstance(value, dict):
|
|
2693
|
+
return value
|
|
2694
|
+
payload = dict(value)
|
|
2695
|
+
if str(payload.get("mode") or "").strip().lower() == "overwrite":
|
|
2696
|
+
payload["mode"] = "replace"
|
|
2697
|
+
raw_preset = payload.get("preset")
|
|
2698
|
+
if raw_preset is None and isinstance(payload.get("base_preset"), str):
|
|
2699
|
+
raw_preset = payload["base_preset"]
|
|
2700
|
+
payload["preset"] = raw_preset
|
|
2701
|
+
if isinstance(raw_preset, str):
|
|
2702
|
+
normalized_preset = raw_preset.strip().lower()
|
|
2703
|
+
preset_aliases = {
|
|
2704
|
+
"default_approval": FlowPreset.basic_approval.value,
|
|
2705
|
+
"approval": FlowPreset.basic_approval.value,
|
|
2706
|
+
"basic approval": FlowPreset.basic_approval.value,
|
|
2707
|
+
"default_fill_then_approve": FlowPreset.basic_fill_then_approve.value,
|
|
2708
|
+
"default-fill-then-approve": FlowPreset.basic_fill_then_approve.value,
|
|
2709
|
+
"fill_then_approve": FlowPreset.basic_fill_then_approve.value,
|
|
2710
|
+
}
|
|
2711
|
+
if normalized_preset in preset_aliases:
|
|
2712
|
+
payload["preset"] = preset_aliases[normalized_preset]
|
|
2713
|
+
return payload
|
|
2714
|
+
|
|
2715
|
+
|
|
2419
2716
|
class ViewsPlanRequest(StrictModel):
|
|
2420
2717
|
app_key: str
|
|
2421
2718
|
upsert_views: list[ViewUpsertPatch] = Field(default_factory=list)
|
|
@@ -2443,8 +2740,18 @@ def _normalize_field_payload(value: Any) -> Any:
|
|
|
2443
2740
|
if not isinstance(value, dict):
|
|
2444
2741
|
return value
|
|
2445
2742
|
payload = dict(value)
|
|
2743
|
+
if "data_title" in payload and "as_data_title" not in payload and "asDataTitle" not in payload:
|
|
2744
|
+
payload["as_data_title"] = payload.pop("data_title")
|
|
2745
|
+
if "dataTitle" in payload and "as_data_title" not in payload and "asDataTitle" not in payload:
|
|
2746
|
+
payload["as_data_title"] = payload.pop("dataTitle")
|
|
2747
|
+
if "data_cover" in payload and "as_data_cover" not in payload and "asDataCover" not in payload:
|
|
2748
|
+
payload["as_data_cover"] = payload.pop("data_cover")
|
|
2749
|
+
if "dataCover" in payload and "as_data_cover" not in payload and "asDataCover" not in payload:
|
|
2750
|
+
payload["as_data_cover"] = payload.pop("dataCover")
|
|
2446
2751
|
if "fields" in payload and "subfields" not in payload:
|
|
2447
2752
|
payload["subfields"] = payload.pop("fields")
|
|
2753
|
+
if "options" in payload:
|
|
2754
|
+
payload["options"] = _normalize_field_options(payload.get("options"))
|
|
2448
2755
|
raw_type = payload.get("type")
|
|
2449
2756
|
if isinstance(raw_type, int):
|
|
2450
2757
|
normalized_from_id = FIELD_TYPE_ID_ALIASES.get(raw_type)
|
|
@@ -2477,6 +2784,34 @@ def _normalize_field_payload(value: Any) -> Any:
|
|
|
2477
2784
|
return payload
|
|
2478
2785
|
|
|
2479
2786
|
|
|
2787
|
+
def _normalize_field_options(value: Any) -> Any:
|
|
2788
|
+
if not isinstance(value, list):
|
|
2789
|
+
return value
|
|
2790
|
+
normalized: list[str] = []
|
|
2791
|
+
for item in value:
|
|
2792
|
+
if isinstance(item, str):
|
|
2793
|
+
normalized.append(item)
|
|
2794
|
+
continue
|
|
2795
|
+
if isinstance(item, (int, float, bool)):
|
|
2796
|
+
normalized.append(str(item))
|
|
2797
|
+
continue
|
|
2798
|
+
if isinstance(item, dict):
|
|
2799
|
+
label = (
|
|
2800
|
+
item.get("label")
|
|
2801
|
+
or item.get("name")
|
|
2802
|
+
or item.get("title")
|
|
2803
|
+
or item.get("value")
|
|
2804
|
+
or item.get("text")
|
|
2805
|
+
or item.get("optValue")
|
|
2806
|
+
or item.get("optName")
|
|
2807
|
+
)
|
|
2808
|
+
if label is not None:
|
|
2809
|
+
normalized.append(str(label))
|
|
2810
|
+
continue
|
|
2811
|
+
normalized.append(str(item))
|
|
2812
|
+
return normalized
|
|
2813
|
+
|
|
2814
|
+
|
|
2480
2815
|
def _slugify_title(title: str) -> str:
|
|
2481
2816
|
normalized = "".join(ch.lower() if ch.isalnum() else "_" for ch in str(title or ""))
|
|
2482
2817
|
collapsed = "_".join(part for part in normalized.split("_") if part)
|