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