@josephyan/qingflow-app-builder-mcp 1.1.31 → 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 -145
- package/src/qingflow_mcp/builder_facade/service.py +6288 -1826
- 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 +3573 -1486
- 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"))
|
|
@@ -1092,11 +1243,12 @@ class ViewUpsertPatch(StrictModel):
|
|
|
1092
1243
|
columns: list[str] = Field(default_factory=list)
|
|
1093
1244
|
group_by: str | None = None
|
|
1094
1245
|
filters: list[ViewFilterRulePatch] = Field(default_factory=list)
|
|
1095
|
-
filter_groups: list[list[ViewFilterRulePatch]] = Field(default_factory=list, validation_alias=AliasChoices("filter_groups", "filterGroups", "or_filters", "orFilters"))
|
|
1096
1246
|
start_field: str | None = Field(default=None, validation_alias=AliasChoices("start_field", "startField"))
|
|
1097
1247
|
end_field: str | None = Field(default=None, validation_alias=AliasChoices("end_field", "endField"))
|
|
1098
1248
|
title_field: str | None = Field(default=None, validation_alias=AliasChoices("title_field", "titleField"))
|
|
1099
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"))
|
|
1100
1252
|
visibility: VisibilityPatch | None = None
|
|
1101
1253
|
query_conditions: ViewQueryConditionsPatch | None = Field(default=None, validation_alias=AliasChoices("query_conditions", "queryConditions", "query_condition", "queryCondition"))
|
|
1102
1254
|
associated_resources: ViewAssociatedResourcesPatch | None = Field(
|
|
@@ -1132,12 +1284,6 @@ class ViewUpsertPatch(StrictModel):
|
|
|
1132
1284
|
payload["filters"] = payload.pop("filter_rules")
|
|
1133
1285
|
if "filterRules" in payload and "filters" not in payload:
|
|
1134
1286
|
payload["filters"] = payload.pop("filterRules")
|
|
1135
|
-
if "filterGroups" in payload and "filter_groups" not in payload:
|
|
1136
|
-
payload["filter_groups"] = payload.pop("filterGroups")
|
|
1137
|
-
if "or_filters" in payload and "filter_groups" not in payload:
|
|
1138
|
-
payload["filter_groups"] = payload.pop("or_filters")
|
|
1139
|
-
if "orFilters" in payload and "filter_groups" not in payload:
|
|
1140
|
-
payload["filter_groups"] = payload.pop("orFilters")
|
|
1141
1287
|
if "queryConditions" in payload and "query_conditions" not in payload:
|
|
1142
1288
|
payload["query_conditions"] = payload.pop("queryConditions")
|
|
1143
1289
|
if "query_condition" in payload and "query_conditions" not in payload:
|
|
@@ -1152,6 +1298,12 @@ class ViewUpsertPatch(StrictModel):
|
|
|
1152
1298
|
payload["associated_resources"] = payload.pop("associatedReports")
|
|
1153
1299
|
if "asosChartConfig" in payload and "associated_resources" not in payload:
|
|
1154
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")
|
|
1155
1307
|
raw_type = payload.get("type")
|
|
1156
1308
|
if isinstance(raw_type, str):
|
|
1157
1309
|
normalized = raw_type.strip().lower()
|
|
@@ -1173,6 +1325,10 @@ class ViewUpsertPatch(StrictModel):
|
|
|
1173
1325
|
raise ValueError("board view requires group_by")
|
|
1174
1326
|
if self.type == PublicViewType.gantt and not (self.start_field and self.end_field):
|
|
1175
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
|
|
1176
1332
|
return self
|
|
1177
1333
|
|
|
1178
1334
|
|
|
@@ -1198,6 +1354,76 @@ class ViewPartialPatch(StrictModel):
|
|
|
1198
1354
|
return self
|
|
1199
1355
|
|
|
1200
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
|
+
|
|
1201
1427
|
class CustomButtonJudgeValuePatch(StrictModel):
|
|
1202
1428
|
id: int | str | None = None
|
|
1203
1429
|
value: Any | None = None
|
|
@@ -1266,13 +1492,39 @@ class CustomButtonFieldMappingPatch(StrictModel):
|
|
|
1266
1492
|
|
|
1267
1493
|
|
|
1268
1494
|
class FieldMatchMappingPatch(StrictModel):
|
|
1269
|
-
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"))
|
|
1270
1496
|
source_field: Any | None = Field(default=None, validation_alias=AliasChoices("source_field", "sourceField", "source"))
|
|
1271
|
-
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"))
|
|
1272
1498
|
operator: str = Field(default="eq", validation_alias=AliasChoices("operator", "op", "judge_type", "judgeType"))
|
|
1273
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
|
+
|
|
1274
1522
|
@model_validator(mode="after")
|
|
1275
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
|
|
1276
1528
|
has_source = self.source_field is not None and str(self.source_field).strip() != ""
|
|
1277
1529
|
has_value = self.value is not None
|
|
1278
1530
|
if has_source == has_value:
|
|
@@ -1466,9 +1718,6 @@ class CustomButtonViewButtonBindingPatch(StrictModel):
|
|
|
1466
1718
|
raw_limits = payload.get("button_limit", payload.get("buttonLimit", payload.get("visible_when", payload.get("visibleWhen"))))
|
|
1467
1719
|
if isinstance(raw_limits, list) and raw_limits and all(isinstance(item, dict) for item in raw_limits):
|
|
1468
1720
|
payload["button_limit"] = [raw_limits]
|
|
1469
|
-
for alias in ("buttonLimit", "visible_when", "visibleWhen"):
|
|
1470
|
-
if alias in payload and alias != "button_limit":
|
|
1471
|
-
payload.pop(alias, None)
|
|
1472
1721
|
return payload
|
|
1473
1722
|
|
|
1474
1723
|
|
|
@@ -1620,6 +1869,7 @@ class AssociatedResourcesApplyRequest(StrictModel):
|
|
|
1620
1869
|
if not isinstance(value, dict):
|
|
1621
1870
|
return value
|
|
1622
1871
|
payload = dict(value)
|
|
1872
|
+
default_target_app_key = str(payload.get("app_key", payload.get("appKey", "")) or "").strip()
|
|
1623
1873
|
if "upsertResources" in payload and "upsert_resources" not in payload:
|
|
1624
1874
|
payload["upsert_resources"] = payload.pop("upsertResources")
|
|
1625
1875
|
if "patchResources" in payload and "patch_resources" not in payload:
|
|
@@ -1632,6 +1882,16 @@ class AssociatedResourcesApplyRequest(StrictModel):
|
|
|
1632
1882
|
payload["reorder_associated_item_ids"] = payload.pop("reorderAssociatedItemIds")
|
|
1633
1883
|
if "viewConfigs" in payload and "view_configs" not in payload:
|
|
1634
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
|
|
1635
1895
|
return payload
|
|
1636
1896
|
|
|
1637
1897
|
@model_validator(mode="after")
|
|
@@ -1655,7 +1915,7 @@ class ViewButtonBindingPatch(StrictModel):
|
|
|
1655
1915
|
being_main: bool = Field(default=False, validation_alias=AliasChoices("being_main", "beingMain"))
|
|
1656
1916
|
button_limit: list[list[ViewFilterRulePatch]] = Field(
|
|
1657
1917
|
default_factory=list,
|
|
1658
|
-
validation_alias=AliasChoices("button_limit", "buttonLimit"
|
|
1918
|
+
validation_alias=AliasChoices("button_limit", "buttonLimit"),
|
|
1659
1919
|
)
|
|
1660
1920
|
button_formula: str | None = Field(default=None, validation_alias=AliasChoices("button_formula", "buttonFormula"))
|
|
1661
1921
|
button_formula_type: int = Field(default=1, validation_alias=AliasChoices("button_formula_type", "buttonFormulaType"))
|
|
@@ -1684,12 +1944,9 @@ class ViewButtonBindingPatch(StrictModel):
|
|
|
1684
1944
|
payload["config_type"] = "DETAIL"
|
|
1685
1945
|
elif normalized_config in {"inside", "list", "row", "row_action"}:
|
|
1686
1946
|
payload["config_type"] = "INSIDE"
|
|
1687
|
-
raw_limits = payload.get("button_limit", payload.get("buttonLimit"
|
|
1947
|
+
raw_limits = payload.get("button_limit", payload.get("buttonLimit"))
|
|
1688
1948
|
if isinstance(raw_limits, list) and raw_limits and all(isinstance(item, dict) for item in raw_limits):
|
|
1689
1949
|
payload["button_limit"] = [raw_limits]
|
|
1690
|
-
for alias in ("buttonLimit", "visible_when", "visibleWhen"):
|
|
1691
|
-
if alias in payload and alias != "button_limit":
|
|
1692
|
-
payload.pop(alias, None)
|
|
1693
1950
|
return payload
|
|
1694
1951
|
|
|
1695
1952
|
@model_validator(mode="after")
|
|
@@ -1729,45 +1986,13 @@ class ViewButtonBindingPatch(StrictModel):
|
|
|
1729
1986
|
|
|
1730
1987
|
class ChartFilterRulePatch(StrictModel):
|
|
1731
1988
|
field_name: str = Field(validation_alias=AliasChoices("field_name", "fieldName", "field", "name"))
|
|
1732
|
-
operator: ViewFilterOperator = Field(validation_alias=AliasChoices("operator", "op"))
|
|
1989
|
+
operator: ViewFilterOperator = Field(default=ViewFilterOperator.eq, validation_alias=AliasChoices("operator", "op"))
|
|
1733
1990
|
values: list[Any] = Field(default_factory=list)
|
|
1734
1991
|
|
|
1735
1992
|
@model_validator(mode="before")
|
|
1736
1993
|
@classmethod
|
|
1737
1994
|
def normalize_aliases(cls, value: Any) -> Any:
|
|
1738
|
-
|
|
1739
|
-
return value
|
|
1740
|
-
payload = dict(value)
|
|
1741
|
-
if "value" in payload and "values" not in payload:
|
|
1742
|
-
raw_value = payload.pop("value")
|
|
1743
|
-
payload["values"] = list(raw_value) if isinstance(raw_value, list) else [raw_value]
|
|
1744
|
-
raw_operator = payload.get("operator", payload.get("op"))
|
|
1745
|
-
if isinstance(raw_operator, str):
|
|
1746
|
-
normalized = raw_operator.strip().lower()
|
|
1747
|
-
operator_aliases = {
|
|
1748
|
-
"equals": ViewFilterOperator.eq.value,
|
|
1749
|
-
"equal": ViewFilterOperator.eq.value,
|
|
1750
|
-
"=": ViewFilterOperator.eq.value,
|
|
1751
|
-
"not_equals": ViewFilterOperator.neq.value,
|
|
1752
|
-
"not_equal": ViewFilterOperator.neq.value,
|
|
1753
|
-
"!=": ViewFilterOperator.neq.value,
|
|
1754
|
-
">=": ViewFilterOperator.gte.value,
|
|
1755
|
-
"<=": ViewFilterOperator.lte.value,
|
|
1756
|
-
"any_of": ViewFilterOperator.in_.value,
|
|
1757
|
-
"one_of": ViewFilterOperator.in_.value,
|
|
1758
|
-
"between_any": ViewFilterOperator.in_.value,
|
|
1759
|
-
"empty": ViewFilterOperator.is_empty.value,
|
|
1760
|
-
"is blank": ViewFilterOperator.is_empty.value,
|
|
1761
|
-
"blank": ViewFilterOperator.is_empty.value,
|
|
1762
|
-
"not_empty": ViewFilterOperator.not_empty.value,
|
|
1763
|
-
"not blank": ViewFilterOperator.not_empty.value,
|
|
1764
|
-
}
|
|
1765
|
-
if normalized in operator_aliases:
|
|
1766
|
-
payload["operator"] = operator_aliases[normalized]
|
|
1767
|
-
elif "operator" not in payload:
|
|
1768
|
-
payload["operator"] = normalized
|
|
1769
|
-
payload.pop("op", None)
|
|
1770
|
-
return payload
|
|
1995
|
+
return _normalize_public_condition_payload(value)
|
|
1771
1996
|
|
|
1772
1997
|
@model_validator(mode="after")
|
|
1773
1998
|
def validate_shape(self) -> "ChartFilterRulePatch":
|
|
@@ -2056,6 +2281,22 @@ class ChartApplyRequest(StrictModel):
|
|
|
2056
2281
|
return self
|
|
2057
2282
|
|
|
2058
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
|
+
|
|
2059
2300
|
class PortalComponentPositionPatch(StrictModel):
|
|
2060
2301
|
pc_x: int = Field(default=0, validation_alias=AliasChoices("pc_x", "pcX", "x"))
|
|
2061
2302
|
pc_y: int = Field(default=0, validation_alias=AliasChoices("pc_y", "pcY", "y"))
|
|
@@ -2102,7 +2343,6 @@ class PortalComponentPositionPatch(StrictModel):
|
|
|
2102
2343
|
class PortalChartRefPatch(StrictModel):
|
|
2103
2344
|
app_key: str | None = None
|
|
2104
2345
|
chart_id: str | None = None
|
|
2105
|
-
chart_key: str | None = None
|
|
2106
2346
|
chart_name: str | None = None
|
|
2107
2347
|
|
|
2108
2348
|
@model_validator(mode="before")
|
|
@@ -2111,24 +2351,26 @@ class PortalChartRefPatch(StrictModel):
|
|
|
2111
2351
|
if not isinstance(value, dict):
|
|
2112
2352
|
return value
|
|
2113
2353
|
payload = dict(value)
|
|
2114
|
-
if "chartKey" in payload and "chart_key" not in payload:
|
|
2115
|
-
payload["chart_key"] = payload.pop("chartKey")
|
|
2116
2354
|
if "chartId" in payload and "chart_id" not in payload:
|
|
2117
2355
|
payload["chart_id"] = payload.pop("chartId")
|
|
2118
|
-
if "
|
|
2119
|
-
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")
|
|
2120
2360
|
if "chartName" in payload and "chart_name" not in payload:
|
|
2121
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")
|
|
2122
2364
|
if "appKey" in payload and "app_key" not in payload:
|
|
2123
2365
|
payload["app_key"] = payload.pop("appKey")
|
|
2124
2366
|
return payload
|
|
2125
2367
|
|
|
2126
2368
|
@model_validator(mode="after")
|
|
2127
2369
|
def validate_target(self) -> "PortalChartRefPatch":
|
|
2128
|
-
if not (self.chart_id or self.
|
|
2129
|
-
raise ValueError("chart_ref requires chart_id
|
|
2130
|
-
if self.chart_name and not self.
|
|
2131
|
-
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")
|
|
2132
2374
|
return self
|
|
2133
2375
|
|
|
2134
2376
|
|
|
@@ -2154,12 +2396,12 @@ class PortalViewRefPatch(StrictModel):
|
|
|
2154
2396
|
class PortalSectionPatch(StrictModel):
|
|
2155
2397
|
title: str
|
|
2156
2398
|
source_type: str = Field(validation_alias=AliasChoices("source_type", "sourceType"))
|
|
2157
|
-
role: str | None = None
|
|
2399
|
+
role: str | None = Field(default=None, validation_alias=AliasChoices("role", "zone", "section_role", "sectionRole"))
|
|
2158
2400
|
position: PortalComponentPositionPatch | None = None
|
|
2159
2401
|
dash_style_config: dict[str, Any] | None = Field(default=None, validation_alias=AliasChoices("dash_style_config", "dashStyleConfigBO"))
|
|
2160
2402
|
config: dict[str, Any] = Field(default_factory=dict)
|
|
2161
|
-
chart: dict[str, Any] | None = None
|
|
2162
2403
|
chart_ref: PortalChartRefPatch | None = None
|
|
2404
|
+
chart: PortalInlineChartPatch | None = Field(default=None, validation_alias=AliasChoices("chart", "inline_chart", "inlineChart", "create_chart", "createChart"))
|
|
2163
2405
|
view_ref: PortalViewRefPatch | None = None
|
|
2164
2406
|
text: str | None = None
|
|
2165
2407
|
url: str | None = None
|
|
@@ -2173,8 +2415,32 @@ class PortalSectionPatch(StrictModel):
|
|
|
2173
2415
|
raw_type = payload.get("source_type", payload.get("sourceType"))
|
|
2174
2416
|
if isinstance(raw_type, str):
|
|
2175
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()
|
|
2176
2421
|
if "chartRef" in payload and "chart_ref" not in payload:
|
|
2177
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)
|
|
2178
2444
|
if "viewRef" in payload and "view_ref" not in payload:
|
|
2179
2445
|
payload["view_ref"] = payload.pop("viewRef")
|
|
2180
2446
|
if "dashStyleConfigBO" in payload and "dash_style_config" not in payload:
|
|
@@ -2186,18 +2452,8 @@ class PortalSectionPatch(StrictModel):
|
|
|
2186
2452
|
supported = {"chart", "view", "grid", "filter", "text", "link"}
|
|
2187
2453
|
if self.source_type not in supported:
|
|
2188
2454
|
raise ValueError(f"unsupported portal source_type '{self.source_type}'")
|
|
2189
|
-
if self.
|
|
2190
|
-
|
|
2191
|
-
if normalized_role not in {"metric"}:
|
|
2192
|
-
raise ValueError(f"unsupported portal section role '{self.role}'")
|
|
2193
|
-
self.role = normalized_role
|
|
2194
|
-
if self.source_type != "chart":
|
|
2195
|
-
raise ValueError("portal section role is only supported for chart sections")
|
|
2196
|
-
if self.source_type == "chart":
|
|
2197
|
-
if self.chart is not None and self.chart_ref is not None:
|
|
2198
|
-
raise ValueError("chart section accepts either chart or chart_ref, not both")
|
|
2199
|
-
if self.chart is None and self.chart_ref is None:
|
|
2200
|
-
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")
|
|
2201
2457
|
if self.source_type == "view" and self.view_ref is None:
|
|
2202
2458
|
raise ValueError("view section requires view_ref")
|
|
2203
2459
|
if self.source_type == "text" and self.text is None:
|
|
@@ -2334,6 +2590,7 @@ AppReadSummaryResponse = AppGetResponse
|
|
|
2334
2590
|
AppFieldsReadResponse = AppGetFieldsResponse
|
|
2335
2591
|
AppLayoutReadResponse = AppGetLayoutResponse
|
|
2336
2592
|
AppViewsReadResponse = AppGetViewsResponse
|
|
2593
|
+
AppFlowReadResponse = AppGetFlowResponse
|
|
2337
2594
|
AppChartsReadResponse = AppGetChartsResponse
|
|
2338
2595
|
|
|
2339
2596
|
|
|
@@ -2400,7 +2657,6 @@ class SchemaPlanRequest(StrictModel):
|
|
|
2400
2657
|
icon: str | None = None
|
|
2401
2658
|
color: str | None = None
|
|
2402
2659
|
visibility: VisibilityPatch | None = None
|
|
2403
|
-
create_if_missing: bool = False
|
|
2404
2660
|
add_fields: list[FieldPatch] = Field(default_factory=list)
|
|
2405
2661
|
update_fields: list[FieldUpdatePatch] = Field(default_factory=list)
|
|
2406
2662
|
remove_fields: list[FieldRemovePatch] = Field(default_factory=list)
|
|
@@ -2423,6 +2679,40 @@ class LayoutPlanRequest(StrictModel):
|
|
|
2423
2679
|
return payload
|
|
2424
2680
|
|
|
2425
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
|
+
|
|
2426
2716
|
class ViewsPlanRequest(StrictModel):
|
|
2427
2717
|
app_key: str
|
|
2428
2718
|
upsert_views: list[ViewUpsertPatch] = Field(default_factory=list)
|
|
@@ -2450,8 +2740,18 @@ def _normalize_field_payload(value: Any) -> Any:
|
|
|
2450
2740
|
if not isinstance(value, dict):
|
|
2451
2741
|
return value
|
|
2452
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")
|
|
2453
2751
|
if "fields" in payload and "subfields" not in payload:
|
|
2454
2752
|
payload["subfields"] = payload.pop("fields")
|
|
2753
|
+
if "options" in payload:
|
|
2754
|
+
payload["options"] = _normalize_field_options(payload.get("options"))
|
|
2455
2755
|
raw_type = payload.get("type")
|
|
2456
2756
|
if isinstance(raw_type, int):
|
|
2457
2757
|
normalized_from_id = FIELD_TYPE_ID_ALIASES.get(raw_type)
|
|
@@ -2484,6 +2784,34 @@ def _normalize_field_payload(value: Any) -> Any:
|
|
|
2484
2784
|
return payload
|
|
2485
2785
|
|
|
2486
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
|
+
|
|
2487
2815
|
def _slugify_title(title: str) -> str:
|
|
2488
2816
|
normalized = "".join(ch.lower() if ch.isalnum() else "_" for ch in str(title or ""))
|
|
2489
2817
|
collapsed = "_".join(part for part in normalized.split("_") if part)
|