@josephyan/qingflow-cli 0.2.0-beta.1000
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 +31 -0
- package/docs/local-agent-install.md +309 -0
- package/entry_point.py +13 -0
- package/npm/bin/qingflow.mjs +5 -0
- package/npm/lib/runtime.mjs +346 -0
- package/npm/scripts/postinstall.mjs +16 -0
- package/package.json +34 -0
- package/pyproject.toml +67 -0
- package/qingflow +15 -0
- package/src/qingflow_mcp/__init__.py +37 -0
- package/src/qingflow_mcp/__main__.py +5 -0
- package/src/qingflow_mcp/backend_client.py +649 -0
- package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
- package/src/qingflow_mcp/builder_facade/models.py +1846 -0
- package/src/qingflow_mcp/builder_facade/service.py +16502 -0
- package/src/qingflow_mcp/cli/__init__.py +1 -0
- package/src/qingflow_mcp/cli/commands/__init__.py +18 -0
- package/src/qingflow_mcp/cli/commands/app.py +40 -0
- package/src/qingflow_mcp/cli/commands/auth.py +112 -0
- package/src/qingflow_mcp/cli/commands/builder.py +539 -0
- package/src/qingflow_mcp/cli/commands/chart.py +18 -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/portal.py +25 -0
- package/src/qingflow_mcp/cli/commands/record.py +331 -0
- package/src/qingflow_mcp/cli/commands/repo.py +80 -0
- package/src/qingflow_mcp/cli/commands/task.py +141 -0
- package/src/qingflow_mcp/cli/commands/view.py +18 -0
- package/src/qingflow_mcp/cli/commands/workspace.py +110 -0
- package/src/qingflow_mcp/cli/context.py +60 -0
- package/src/qingflow_mcp/cli/formatters.py +573 -0
- package/src/qingflow_mcp/cli/json_io.py +50 -0
- package/src/qingflow_mcp/cli/main.py +186 -0
- package/src/qingflow_mcp/cli/qingflow_login.py +116 -0
- package/src/qingflow_mcp/cli/terminal_ui.py +173 -0
- package/src/qingflow_mcp/config.py +407 -0
- package/src/qingflow_mcp/errors.py +66 -0
- package/src/qingflow_mcp/id_utils.py +49 -0
- package/src/qingflow_mcp/import_store.py +121 -0
- package/src/qingflow_mcp/json_types.py +18 -0
- package/src/qingflow_mcp/list_type_labels.py +76 -0
- package/src/qingflow_mcp/public_surface.py +243 -0
- package/src/qingflow_mcp/repository_store.py +71 -0
- package/src/qingflow_mcp/response_trim.py +841 -0
- package/src/qingflow_mcp/server.py +216 -0
- package/src/qingflow_mcp/server_app_builder.py +543 -0
- package/src/qingflow_mcp/server_app_user.py +386 -0
- package/src/qingflow_mcp/session_store.py +369 -0
- package/src/qingflow_mcp/solution/__init__.py +6 -0
- package/src/qingflow_mcp/solution/build_assembly_store.py +181 -0
- package/src/qingflow_mcp/solution/compiler/__init__.py +282 -0
- package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +495 -0
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +187 -0
- package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
- package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
- package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
- package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
- package/src/qingflow_mcp/solution/design_session.py +222 -0
- package/src/qingflow_mcp/solution/design_store.py +100 -0
- package/src/qingflow_mcp/solution/executor.py +2398 -0
- package/src/qingflow_mcp/solution/normalizer.py +23 -0
- package/src/qingflow_mcp/solution/requirements_builder.py +536 -0
- package/src/qingflow_mcp/solution/run_store.py +244 -0
- package/src/qingflow_mcp/solution/spec_models.py +855 -0
- package/src/qingflow_mcp/tools/__init__.py +1 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +3449 -0
- package/src/qingflow_mcp/tools/app_tools.py +926 -0
- package/src/qingflow_mcp/tools/approval_tools.py +1062 -0
- package/src/qingflow_mcp/tools/auth_tools.py +1133 -0
- package/src/qingflow_mcp/tools/base.py +281 -0
- package/src/qingflow_mcp/tools/code_block_tools.py +777 -0
- package/src/qingflow_mcp/tools/custom_button_tools.py +202 -0
- package/src/qingflow_mcp/tools/directory_tools.py +675 -0
- package/src/qingflow_mcp/tools/feedback_tools.py +238 -0
- package/src/qingflow_mcp/tools/file_tools.py +409 -0
- package/src/qingflow_mcp/tools/import_tools.py +2223 -0
- package/src/qingflow_mcp/tools/navigation_tools.py +210 -0
- package/src/qingflow_mcp/tools/package_tools.py +326 -0
- package/src/qingflow_mcp/tools/portal_tools.py +158 -0
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +374 -0
- package/src/qingflow_mcp/tools/record_tools.py +14291 -0
- package/src/qingflow_mcp/tools/repository_dev_tools.py +552 -0
- package/src/qingflow_mcp/tools/resource_read_tools.py +503 -0
- package/src/qingflow_mcp/tools/role_tools.py +112 -0
- package/src/qingflow_mcp/tools/solution_tools.py +4054 -0
- package/src/qingflow_mcp/tools/task_context_tools.py +2986 -0
- package/src/qingflow_mcp/tools/task_tools.py +889 -0
- package/src/qingflow_mcp/tools/view_tools.py +335 -0
- package/src/qingflow_mcp/tools/workflow_tools.py +376 -0
- package/src/qingflow_mcp/tools/workspace_tools.py +266 -0
|
@@ -0,0 +1,1846 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pydantic import AliasChoices, Field, model_validator
|
|
7
|
+
|
|
8
|
+
from ..solution.spec_models import StrictModel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PublicFieldType(str, Enum):
|
|
12
|
+
text = "text"
|
|
13
|
+
long_text = "long_text"
|
|
14
|
+
number = "number"
|
|
15
|
+
amount = "amount"
|
|
16
|
+
date = "date"
|
|
17
|
+
datetime = "datetime"
|
|
18
|
+
member = "member"
|
|
19
|
+
department = "department"
|
|
20
|
+
single_select = "single_select"
|
|
21
|
+
multi_select = "multi_select"
|
|
22
|
+
phone = "phone"
|
|
23
|
+
email = "email"
|
|
24
|
+
address = "address"
|
|
25
|
+
attachment = "attachment"
|
|
26
|
+
boolean = "boolean"
|
|
27
|
+
q_linker = "q_linker"
|
|
28
|
+
code_block = "code_block"
|
|
29
|
+
relation = "relation"
|
|
30
|
+
subtable = "subtable"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class PublicRelationMode(str, Enum):
|
|
34
|
+
single = "single"
|
|
35
|
+
multiple = "multiple"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class PublicDepartmentScopeMode(str, Enum):
|
|
39
|
+
all = "all"
|
|
40
|
+
custom = "custom"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class PublicVisibilityMode(str, Enum):
|
|
44
|
+
workspace = "workspace"
|
|
45
|
+
everyone = "everyone"
|
|
46
|
+
specific = "specific"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class PublicExternalVisibilityMode(str, Enum):
|
|
50
|
+
not_ = "not"
|
|
51
|
+
workspace = "workspace"
|
|
52
|
+
specific = "specific"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
FIELD_TYPE_ALIASES: dict[str, PublicFieldType] = {
|
|
56
|
+
"textarea": PublicFieldType.long_text,
|
|
57
|
+
"amount": PublicFieldType.amount,
|
|
58
|
+
"currency": PublicFieldType.amount,
|
|
59
|
+
"mobile": PublicFieldType.phone,
|
|
60
|
+
"user": PublicFieldType.member,
|
|
61
|
+
"users": PublicFieldType.member,
|
|
62
|
+
"select": PublicFieldType.single_select,
|
|
63
|
+
"radio": PublicFieldType.single_select,
|
|
64
|
+
"checkbox": PublicFieldType.multi_select,
|
|
65
|
+
"multi_select": PublicFieldType.multi_select,
|
|
66
|
+
"multi-select": PublicFieldType.multi_select,
|
|
67
|
+
"departments": PublicFieldType.department,
|
|
68
|
+
"qlinker": PublicFieldType.q_linker,
|
|
69
|
+
"q_linker": PublicFieldType.q_linker,
|
|
70
|
+
"codeblock": PublicFieldType.code_block,
|
|
71
|
+
"code_block": PublicFieldType.code_block,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
FIELD_TYPE_ID_ALIASES: dict[int, PublicFieldType] = {
|
|
75
|
+
2: PublicFieldType.text,
|
|
76
|
+
3: PublicFieldType.long_text,
|
|
77
|
+
4: PublicFieldType.date,
|
|
78
|
+
5: PublicFieldType.member,
|
|
79
|
+
6: PublicFieldType.email,
|
|
80
|
+
7: PublicFieldType.phone,
|
|
81
|
+
8: PublicFieldType.number,
|
|
82
|
+
10: PublicFieldType.boolean,
|
|
83
|
+
11: PublicFieldType.single_select,
|
|
84
|
+
12: PublicFieldType.multi_select,
|
|
85
|
+
13: PublicFieldType.attachment,
|
|
86
|
+
20: PublicFieldType.q_linker,
|
|
87
|
+
26: PublicFieldType.code_block,
|
|
88
|
+
18: PublicFieldType.subtable,
|
|
89
|
+
21: PublicFieldType.address,
|
|
90
|
+
22: PublicFieldType.department,
|
|
91
|
+
25: PublicFieldType.relation,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class PublicViewType(str, Enum):
|
|
96
|
+
table = "table"
|
|
97
|
+
card = "card"
|
|
98
|
+
board = "board"
|
|
99
|
+
gantt = "gantt"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class PublicButtonTriggerAction(str, Enum):
|
|
103
|
+
add_data = "addData"
|
|
104
|
+
link = "link"
|
|
105
|
+
qrobot = "qRobot"
|
|
106
|
+
wings = "wings"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class PublicViewButtonType(str, Enum):
|
|
110
|
+
system = "SYSTEM"
|
|
111
|
+
custom = "CUSTOM"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class PublicViewButtonConfigType(str, Enum):
|
|
115
|
+
top = "TOP"
|
|
116
|
+
detail = "DETAIL"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class PublicChartType(str, Enum):
|
|
120
|
+
target = "target"
|
|
121
|
+
pie = "pie"
|
|
122
|
+
bar = "bar"
|
|
123
|
+
line = "line"
|
|
124
|
+
table = "table"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class LayoutApplyMode(str, Enum):
|
|
128
|
+
merge = "merge"
|
|
129
|
+
replace = "replace"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class LayoutPreset(str, Enum):
|
|
133
|
+
balanced = "balanced"
|
|
134
|
+
compact = "compact"
|
|
135
|
+
single_section = "single_section"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class FlowPreset(str, Enum):
|
|
139
|
+
basic_approval = "basic_approval"
|
|
140
|
+
basic_fill_then_approve = "basic_fill_then_approve"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class ViewsPreset(str, Enum):
|
|
144
|
+
default_table = "default_table"
|
|
145
|
+
status_board = "status_board"
|
|
146
|
+
default_gantt = "default_gantt"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class PublicFlowNodeType(str, Enum):
|
|
150
|
+
start = "start"
|
|
151
|
+
approve = "approve"
|
|
152
|
+
fill = "fill"
|
|
153
|
+
copy = "copy"
|
|
154
|
+
branch = "branch"
|
|
155
|
+
condition = "condition"
|
|
156
|
+
webhook = "webhook"
|
|
157
|
+
end = "end"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class FlowConditionOperator(str, Enum):
|
|
161
|
+
eq = "eq"
|
|
162
|
+
neq = "neq"
|
|
163
|
+
in_ = "in"
|
|
164
|
+
contains = "contains"
|
|
165
|
+
gte = "gte"
|
|
166
|
+
lte = "lte"
|
|
167
|
+
is_empty = "is_empty"
|
|
168
|
+
not_empty = "not_empty"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class ViewFilterOperator(str, Enum):
|
|
172
|
+
eq = "eq"
|
|
173
|
+
neq = "neq"
|
|
174
|
+
in_ = "in"
|
|
175
|
+
contains = "contains"
|
|
176
|
+
gte = "gte"
|
|
177
|
+
lte = "lte"
|
|
178
|
+
is_empty = "is_empty"
|
|
179
|
+
not_empty = "not_empty"
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class FlowAssigneePatch(StrictModel):
|
|
183
|
+
role_ids: list[int] = Field(default_factory=list)
|
|
184
|
+
role_names: list[str] = Field(default_factory=list)
|
|
185
|
+
member_uids: list[int] = Field(default_factory=list)
|
|
186
|
+
member_emails: list[str] = Field(default_factory=list)
|
|
187
|
+
member_names: list[str] = Field(default_factory=list)
|
|
188
|
+
include_sub_departs: bool | None = None
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class FlowNodePermissionsPatch(StrictModel):
|
|
192
|
+
editable_fields: list[str] = Field(default_factory=list)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class FlowConditionRulePatch(StrictModel):
|
|
196
|
+
field_name: str = Field(validation_alias=AliasChoices("field_name", "fieldName", "field", "name"))
|
|
197
|
+
operator: FlowConditionOperator = Field(validation_alias=AliasChoices("operator", "op"))
|
|
198
|
+
values: list[Any] = Field(default_factory=list)
|
|
199
|
+
|
|
200
|
+
@model_validator(mode="before")
|
|
201
|
+
@classmethod
|
|
202
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
203
|
+
if not isinstance(value, dict):
|
|
204
|
+
return value
|
|
205
|
+
payload = dict(value)
|
|
206
|
+
if "value" in payload and "values" not in payload:
|
|
207
|
+
raw_value = payload.pop("value")
|
|
208
|
+
payload["values"] = list(raw_value) if isinstance(raw_value, list) else [raw_value]
|
|
209
|
+
raw_operator = payload.get("operator", payload.get("op"))
|
|
210
|
+
if isinstance(raw_operator, str):
|
|
211
|
+
normalized = raw_operator.strip().lower()
|
|
212
|
+
operator_aliases = {
|
|
213
|
+
"equals": FlowConditionOperator.eq.value,
|
|
214
|
+
"equal": FlowConditionOperator.eq.value,
|
|
215
|
+
"=": FlowConditionOperator.eq.value,
|
|
216
|
+
"not_equals": FlowConditionOperator.neq.value,
|
|
217
|
+
"not_equal": FlowConditionOperator.neq.value,
|
|
218
|
+
"!=": FlowConditionOperator.neq.value,
|
|
219
|
+
">=": FlowConditionOperator.gte.value,
|
|
220
|
+
"<=": FlowConditionOperator.lte.value,
|
|
221
|
+
"any_of": FlowConditionOperator.in_.value,
|
|
222
|
+
"one_of": FlowConditionOperator.in_.value,
|
|
223
|
+
"between_any": FlowConditionOperator.in_.value,
|
|
224
|
+
"empty": FlowConditionOperator.is_empty.value,
|
|
225
|
+
"is blank": FlowConditionOperator.is_empty.value,
|
|
226
|
+
"blank": FlowConditionOperator.is_empty.value,
|
|
227
|
+
"not_empty": FlowConditionOperator.not_empty.value,
|
|
228
|
+
"not blank": FlowConditionOperator.not_empty.value,
|
|
229
|
+
}
|
|
230
|
+
if normalized in operator_aliases:
|
|
231
|
+
payload["operator"] = operator_aliases[normalized]
|
|
232
|
+
elif "operator" not in payload:
|
|
233
|
+
payload["operator"] = normalized
|
|
234
|
+
payload.pop("op", None)
|
|
235
|
+
return payload
|
|
236
|
+
|
|
237
|
+
@model_validator(mode="after")
|
|
238
|
+
def validate_shape(self) -> "FlowConditionRulePatch":
|
|
239
|
+
if self.operator in {FlowConditionOperator.is_empty, FlowConditionOperator.not_empty}:
|
|
240
|
+
self.values = []
|
|
241
|
+
return self
|
|
242
|
+
if not self.values:
|
|
243
|
+
raise ValueError("condition rule requires values")
|
|
244
|
+
return self
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class ViewFilterRulePatch(StrictModel):
|
|
248
|
+
field_name: str = Field(validation_alias=AliasChoices("field_name", "fieldName", "field", "name"))
|
|
249
|
+
operator: ViewFilterOperator = Field(validation_alias=AliasChoices("operator", "op"))
|
|
250
|
+
values: list[Any] = Field(default_factory=list)
|
|
251
|
+
|
|
252
|
+
@model_validator(mode="before")
|
|
253
|
+
@classmethod
|
|
254
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
255
|
+
if not isinstance(value, dict):
|
|
256
|
+
return value
|
|
257
|
+
payload = dict(value)
|
|
258
|
+
if "value" in payload and "values" not in payload:
|
|
259
|
+
raw_value = payload.pop("value")
|
|
260
|
+
payload["values"] = list(raw_value) if isinstance(raw_value, list) else [raw_value]
|
|
261
|
+
raw_operator = payload.get("operator", payload.get("op"))
|
|
262
|
+
if isinstance(raw_operator, str):
|
|
263
|
+
normalized = raw_operator.strip().lower()
|
|
264
|
+
operator_aliases = {
|
|
265
|
+
"equals": ViewFilterOperator.eq.value,
|
|
266
|
+
"equal": ViewFilterOperator.eq.value,
|
|
267
|
+
"=": ViewFilterOperator.eq.value,
|
|
268
|
+
"not_equals": ViewFilterOperator.neq.value,
|
|
269
|
+
"not_equal": ViewFilterOperator.neq.value,
|
|
270
|
+
"!=": ViewFilterOperator.neq.value,
|
|
271
|
+
">=": ViewFilterOperator.gte.value,
|
|
272
|
+
"<=": ViewFilterOperator.lte.value,
|
|
273
|
+
"any_of": ViewFilterOperator.in_.value,
|
|
274
|
+
"one_of": ViewFilterOperator.in_.value,
|
|
275
|
+
"between_any": ViewFilterOperator.in_.value,
|
|
276
|
+
"empty": ViewFilterOperator.is_empty.value,
|
|
277
|
+
"is blank": ViewFilterOperator.is_empty.value,
|
|
278
|
+
"blank": ViewFilterOperator.is_empty.value,
|
|
279
|
+
"not_empty": ViewFilterOperator.not_empty.value,
|
|
280
|
+
"not blank": ViewFilterOperator.not_empty.value,
|
|
281
|
+
}
|
|
282
|
+
if normalized in operator_aliases:
|
|
283
|
+
payload["operator"] = operator_aliases[normalized]
|
|
284
|
+
elif "operator" not in payload:
|
|
285
|
+
payload["operator"] = normalized
|
|
286
|
+
payload.pop("op", None)
|
|
287
|
+
return payload
|
|
288
|
+
|
|
289
|
+
@model_validator(mode="after")
|
|
290
|
+
def validate_shape(self) -> "ViewFilterRulePatch":
|
|
291
|
+
if self.operator in {ViewFilterOperator.is_empty, ViewFilterOperator.not_empty}:
|
|
292
|
+
self.values = []
|
|
293
|
+
return self
|
|
294
|
+
if not self.values:
|
|
295
|
+
raise ValueError("view filter rule requires values")
|
|
296
|
+
return self
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class FieldSelector(StrictModel):
|
|
300
|
+
field_id: str | None = None
|
|
301
|
+
que_id: int | None = None
|
|
302
|
+
name: str | None = Field(default=None, validation_alias=AliasChoices("name", "title", "label"))
|
|
303
|
+
|
|
304
|
+
@model_validator(mode="after")
|
|
305
|
+
def validate_selector(self) -> "FieldSelector":
|
|
306
|
+
if self.field_id is None and self.que_id is None and self.name is None:
|
|
307
|
+
raise ValueError("selector must include field_id, que_id, or name")
|
|
308
|
+
return self
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class DepartmentSelectorPatch(StrictModel):
|
|
312
|
+
dept_id: int | None = Field(default=None, validation_alias=AliasChoices("dept_id", "deptId", "id"))
|
|
313
|
+
dept_name: str | None = Field(default=None, validation_alias=AliasChoices("dept_name", "deptName", "name", "value"))
|
|
314
|
+
|
|
315
|
+
@model_validator(mode="after")
|
|
316
|
+
def validate_selector(self) -> "DepartmentSelectorPatch":
|
|
317
|
+
if self.dept_id is None and not str(self.dept_name or "").strip():
|
|
318
|
+
raise ValueError("department selector must include dept_id or dept_name")
|
|
319
|
+
return self
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
class DepartmentScopePatch(StrictModel):
|
|
323
|
+
mode: PublicDepartmentScopeMode | None = None
|
|
324
|
+
departments: list[DepartmentSelectorPatch] = Field(
|
|
325
|
+
default_factory=list,
|
|
326
|
+
validation_alias=AliasChoices("departments", "departs", "depart"),
|
|
327
|
+
)
|
|
328
|
+
include_sub_departs: bool | None = Field(
|
|
329
|
+
default=None,
|
|
330
|
+
validation_alias=AliasChoices("include_sub_departs", "includeSubDeparts"),
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
@model_validator(mode="before")
|
|
334
|
+
@classmethod
|
|
335
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
336
|
+
if not isinstance(value, dict):
|
|
337
|
+
return value
|
|
338
|
+
payload = dict(value)
|
|
339
|
+
if "depart" in payload and "departments" not in payload:
|
|
340
|
+
payload["departments"] = payload.pop("depart")
|
|
341
|
+
if "departs" in payload and "departments" not in payload:
|
|
342
|
+
payload["departments"] = payload.pop("departs")
|
|
343
|
+
normalized_mode = _normalize_public_department_scope_mode(payload.get("mode"))
|
|
344
|
+
if normalized_mode is None:
|
|
345
|
+
if "departments" in payload:
|
|
346
|
+
normalized_mode = PublicDepartmentScopeMode.custom.value
|
|
347
|
+
else:
|
|
348
|
+
normalized_mode = PublicDepartmentScopeMode.all.value
|
|
349
|
+
payload["mode"] = normalized_mode
|
|
350
|
+
return payload
|
|
351
|
+
|
|
352
|
+
@model_validator(mode="after")
|
|
353
|
+
def validate_shape(self) -> "DepartmentScopePatch":
|
|
354
|
+
if self.mode == PublicDepartmentScopeMode.custom and not self.departments:
|
|
355
|
+
raise ValueError("custom department scope requires departments")
|
|
356
|
+
if self.mode == PublicDepartmentScopeMode.custom and any(item.dept_id is None for item in self.departments):
|
|
357
|
+
raise ValueError("custom department scope requires departments[].dept_id")
|
|
358
|
+
return self
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
class VisibilitySelectorsPatch(StrictModel):
|
|
362
|
+
member_uids: list[int] = Field(default_factory=list, validation_alias=AliasChoices("member_uids", "memberUids"))
|
|
363
|
+
member_emails: list[str] = Field(default_factory=list, validation_alias=AliasChoices("member_emails", "memberEmails"))
|
|
364
|
+
member_names: list[str] = Field(default_factory=list, validation_alias=AliasChoices("member_names", "memberNames"))
|
|
365
|
+
dept_ids: list[int] = Field(default_factory=list, validation_alias=AliasChoices("dept_ids", "deptIds"))
|
|
366
|
+
dept_names: list[str] = Field(default_factory=list, validation_alias=AliasChoices("dept_names", "deptNames"))
|
|
367
|
+
role_ids: list[int] = Field(default_factory=list, validation_alias=AliasChoices("role_ids", "roleIds"))
|
|
368
|
+
role_names: list[str] = Field(default_factory=list, validation_alias=AliasChoices("role_names", "roleNames"))
|
|
369
|
+
include_sub_departs: bool | None = Field(
|
|
370
|
+
default=None,
|
|
371
|
+
validation_alias=AliasChoices("include_sub_departs", "includeSubDeparts"),
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
def has_any_selector(self) -> bool:
|
|
375
|
+
return any(
|
|
376
|
+
(
|
|
377
|
+
self.member_uids,
|
|
378
|
+
self.member_emails,
|
|
379
|
+
self.member_names,
|
|
380
|
+
self.dept_ids,
|
|
381
|
+
self.dept_names,
|
|
382
|
+
self.role_ids,
|
|
383
|
+
self.role_names,
|
|
384
|
+
)
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
class ExternalVisibilitySelectorsPatch(StrictModel):
|
|
389
|
+
member_ids: list[int] = Field(default_factory=list, validation_alias=AliasChoices("member_ids", "memberIds"))
|
|
390
|
+
member_emails: list[str] = Field(default_factory=list, validation_alias=AliasChoices("member_emails", "memberEmails"))
|
|
391
|
+
dept_ids: list[int] = Field(default_factory=list, validation_alias=AliasChoices("dept_ids", "deptIds"))
|
|
392
|
+
|
|
393
|
+
def has_any_selector(self) -> bool:
|
|
394
|
+
return any((self.member_ids, self.member_emails, self.dept_ids))
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
class VisibilityPatch(StrictModel):
|
|
398
|
+
mode: PublicVisibilityMode | None = None
|
|
399
|
+
selectors: VisibilitySelectorsPatch = Field(default_factory=VisibilitySelectorsPatch)
|
|
400
|
+
external_mode: PublicExternalVisibilityMode | None = Field(
|
|
401
|
+
default=None,
|
|
402
|
+
validation_alias=AliasChoices("external_mode", "externalMode"),
|
|
403
|
+
)
|
|
404
|
+
external_selectors: ExternalVisibilitySelectorsPatch = Field(
|
|
405
|
+
default_factory=ExternalVisibilitySelectorsPatch,
|
|
406
|
+
validation_alias=AliasChoices("external_selectors", "externalSelectors"),
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
@model_validator(mode="before")
|
|
410
|
+
@classmethod
|
|
411
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
412
|
+
if not isinstance(value, dict):
|
|
413
|
+
return value
|
|
414
|
+
payload = dict(value)
|
|
415
|
+
selectors = payload.get("selectors")
|
|
416
|
+
external_selectors = payload.get("external_selectors", payload.get("externalSelectors"))
|
|
417
|
+
if payload.get("mode") is None:
|
|
418
|
+
if isinstance(selectors, dict) and selectors:
|
|
419
|
+
payload["mode"] = PublicVisibilityMode.specific.value
|
|
420
|
+
else:
|
|
421
|
+
payload["mode"] = PublicVisibilityMode.workspace.value
|
|
422
|
+
if payload.get("mode") == PublicVisibilityMode.everyone.value and payload.get("external_mode", payload.get("externalMode")) is None:
|
|
423
|
+
payload["external_mode"] = PublicExternalVisibilityMode.workspace.value
|
|
424
|
+
if payload.get("external_mode", payload.get("externalMode")) is None:
|
|
425
|
+
if isinstance(external_selectors, dict) and external_selectors:
|
|
426
|
+
payload["external_mode"] = PublicExternalVisibilityMode.specific.value
|
|
427
|
+
else:
|
|
428
|
+
payload["external_mode"] = PublicExternalVisibilityMode.not_.value
|
|
429
|
+
return payload
|
|
430
|
+
|
|
431
|
+
@model_validator(mode="after")
|
|
432
|
+
def validate_shape(self) -> "VisibilityPatch":
|
|
433
|
+
if self.mode == PublicVisibilityMode.specific and not self.selectors.has_any_selector():
|
|
434
|
+
raise ValueError("specific visibility requires selectors")
|
|
435
|
+
if self.mode != PublicVisibilityMode.specific and self.selectors.has_any_selector():
|
|
436
|
+
raise ValueError("selectors are only allowed when mode=specific")
|
|
437
|
+
if self.mode == PublicVisibilityMode.everyone and self.external_mode != PublicExternalVisibilityMode.workspace:
|
|
438
|
+
raise ValueError("mode=everyone requires external_mode=workspace")
|
|
439
|
+
if self.mode == PublicVisibilityMode.everyone and self.external_selectors.has_any_selector():
|
|
440
|
+
raise ValueError("external_selectors are not allowed when mode=everyone")
|
|
441
|
+
if self.external_mode == PublicExternalVisibilityMode.specific and not self.external_selectors.has_any_selector():
|
|
442
|
+
raise ValueError("external_mode=specific requires external_selectors")
|
|
443
|
+
if self.external_mode != PublicExternalVisibilityMode.specific and self.external_selectors.has_any_selector():
|
|
444
|
+
raise ValueError("external_selectors are only allowed when external_mode=specific")
|
|
445
|
+
return self
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
class CodeBlockAliasPathPatch(StrictModel):
|
|
449
|
+
alias_name: str = Field(validation_alias=AliasChoices("alias_name", "aliasName"))
|
|
450
|
+
alias_path: str = Field(validation_alias=AliasChoices("alias_path", "aliasPath"))
|
|
451
|
+
alias_type: int = Field(default=1, validation_alias=AliasChoices("alias_type", "aliasType"))
|
|
452
|
+
alias_id: int | None = Field(default=None, validation_alias=AliasChoices("alias_id", "aliasId"))
|
|
453
|
+
sub_alias: list["CodeBlockAliasPathPatch"] = Field(
|
|
454
|
+
default_factory=list,
|
|
455
|
+
validation_alias=AliasChoices("sub_alias", "subAlias"),
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
class CodeBlockConfigPatch(StrictModel):
|
|
460
|
+
config_mode: int = Field(default=1, validation_alias=AliasChoices("config_mode", "configMode"))
|
|
461
|
+
code_content: str = Field(default="", validation_alias=AliasChoices("code_content", "codeContent"))
|
|
462
|
+
being_hide_on_form: bool = Field(
|
|
463
|
+
default=False,
|
|
464
|
+
validation_alias=AliasChoices("being_hide_on_form", "beingHideOnForm"),
|
|
465
|
+
)
|
|
466
|
+
result_alias_path: list[CodeBlockAliasPathPatch] = Field(
|
|
467
|
+
default_factory=list,
|
|
468
|
+
validation_alias=AliasChoices("result_alias_path", "resultAliasPath", "alias_config", "aliasConfig"),
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
class CodeBlockInputBindingPatch(StrictModel):
|
|
473
|
+
field: FieldSelector
|
|
474
|
+
var: str | None = None
|
|
475
|
+
|
|
476
|
+
@model_validator(mode="before")
|
|
477
|
+
@classmethod
|
|
478
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
479
|
+
if isinstance(value, str):
|
|
480
|
+
return {"field": {"name": value}}
|
|
481
|
+
if not isinstance(value, dict):
|
|
482
|
+
return value
|
|
483
|
+
payload = dict(value)
|
|
484
|
+
raw_field = payload.get("field")
|
|
485
|
+
if isinstance(raw_field, str):
|
|
486
|
+
payload["field"] = {"name": raw_field}
|
|
487
|
+
elif raw_field is None:
|
|
488
|
+
for key in ("field_name", "fieldName", "name", "title", "label"):
|
|
489
|
+
raw_value = payload.get(key)
|
|
490
|
+
if isinstance(raw_value, str) and raw_value.strip():
|
|
491
|
+
payload["field"] = {"name": raw_value}
|
|
492
|
+
break
|
|
493
|
+
return payload
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
class QLinkerInputSource(str, Enum):
|
|
497
|
+
query_param = "query_param"
|
|
498
|
+
header = "header"
|
|
499
|
+
url_encoded = "url_encoded"
|
|
500
|
+
json_path = "json_path"
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
class QLinkerKeyValuePatch(StrictModel):
|
|
504
|
+
key: str
|
|
505
|
+
value: str | None = None
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
class QLinkerAliasPathPatch(StrictModel):
|
|
509
|
+
alias_name: str = Field(validation_alias=AliasChoices("alias_name", "aliasName"))
|
|
510
|
+
alias_path: str = Field(validation_alias=AliasChoices("alias_path", "aliasPath"))
|
|
511
|
+
alias_id: int | None = Field(default=None, validation_alias=AliasChoices("alias_id", "aliasId"))
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
class RemoteLookupConfigPatch(StrictModel):
|
|
515
|
+
config_mode: int = Field(default=1, validation_alias=AliasChoices("config_mode", "configMode"))
|
|
516
|
+
url: str = ""
|
|
517
|
+
method: str = "GET"
|
|
518
|
+
headers: list[QLinkerKeyValuePatch] = Field(default_factory=list)
|
|
519
|
+
body_type: int = Field(default=1, validation_alias=AliasChoices("body_type", "bodyType"))
|
|
520
|
+
url_encoded_value: list[QLinkerKeyValuePatch] = Field(
|
|
521
|
+
default_factory=list,
|
|
522
|
+
validation_alias=AliasChoices("url_encoded_value", "urlEncodedValue"),
|
|
523
|
+
)
|
|
524
|
+
json_value: str | None = Field(default=None, validation_alias=AliasChoices("json_value", "jsonValue"))
|
|
525
|
+
xml_value: str | None = Field(default=None, validation_alias=AliasChoices("xml_value", "xmlValue"))
|
|
526
|
+
result_type: int = Field(default=1, validation_alias=AliasChoices("result_type", "resultType"))
|
|
527
|
+
result_format_path: list[QLinkerAliasPathPatch] = Field(
|
|
528
|
+
default_factory=list,
|
|
529
|
+
validation_alias=AliasChoices("result_format_path", "resultFormatPath"),
|
|
530
|
+
)
|
|
531
|
+
query_params: list[QLinkerKeyValuePatch] = Field(
|
|
532
|
+
default_factory=list,
|
|
533
|
+
validation_alias=AliasChoices("query_params", "queryParams"),
|
|
534
|
+
)
|
|
535
|
+
auto_trigger: bool | None = Field(default=None, validation_alias=AliasChoices("auto_trigger", "autoTrigger"))
|
|
536
|
+
custom_button_text_enabled: bool | None = Field(
|
|
537
|
+
default=None,
|
|
538
|
+
validation_alias=AliasChoices("custom_button_text_enabled", "customButtonTextEnabled", "custom_btn_text_status", "customBtnTextStatus"),
|
|
539
|
+
)
|
|
540
|
+
custom_button_text: str | None = Field(
|
|
541
|
+
default=None,
|
|
542
|
+
validation_alias=AliasChoices("custom_button_text", "customButtonText", "custom_btn_text", "customBtnText"),
|
|
543
|
+
)
|
|
544
|
+
being_insert_value_directly: bool | None = Field(
|
|
545
|
+
default=None,
|
|
546
|
+
validation_alias=AliasChoices("being_insert_value_directly", "beingInsertValueDirectly"),
|
|
547
|
+
)
|
|
548
|
+
being_hide_on_form: bool | None = Field(
|
|
549
|
+
default=None,
|
|
550
|
+
validation_alias=AliasChoices("being_hide_on_form", "beingHideOnForm"),
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
class QLinkerInputBindingPatch(StrictModel):
|
|
555
|
+
field: FieldSelector
|
|
556
|
+
key: str
|
|
557
|
+
source: QLinkerInputSource
|
|
558
|
+
|
|
559
|
+
@model_validator(mode="before")
|
|
560
|
+
@classmethod
|
|
561
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
562
|
+
if isinstance(value, str):
|
|
563
|
+
return {"field": {"name": value}, "key": value, "source": QLinkerInputSource.query_param.value}
|
|
564
|
+
if not isinstance(value, dict):
|
|
565
|
+
return value
|
|
566
|
+
payload = dict(value)
|
|
567
|
+
raw_field = payload.get("field")
|
|
568
|
+
if isinstance(raw_field, str):
|
|
569
|
+
payload["field"] = {"name": raw_field}
|
|
570
|
+
elif raw_field is None:
|
|
571
|
+
for key in ("field_name", "fieldName", "name", "title", "label"):
|
|
572
|
+
raw_value = payload.get(key)
|
|
573
|
+
if isinstance(raw_value, str) and raw_value.strip():
|
|
574
|
+
payload["field"] = {"name": raw_value}
|
|
575
|
+
break
|
|
576
|
+
raw_source = payload.get("source")
|
|
577
|
+
if isinstance(raw_source, str):
|
|
578
|
+
payload["source"] = raw_source.strip().lower()
|
|
579
|
+
return payload
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
class QLinkerOutputBindingPatch(StrictModel):
|
|
583
|
+
alias: str
|
|
584
|
+
path: str
|
|
585
|
+
target_field: FieldSelector = Field(validation_alias=AliasChoices("target_field", "targetField"))
|
|
586
|
+
|
|
587
|
+
@model_validator(mode="before")
|
|
588
|
+
@classmethod
|
|
589
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
590
|
+
if not isinstance(value, dict):
|
|
591
|
+
return value
|
|
592
|
+
payload = dict(value)
|
|
593
|
+
if "alias_name" in payload and "alias" not in payload:
|
|
594
|
+
payload["alias"] = payload.pop("alias_name")
|
|
595
|
+
if "aliasName" in payload and "alias" not in payload:
|
|
596
|
+
payload["alias"] = payload.pop("aliasName")
|
|
597
|
+
if "alias_path" in payload and "path" not in payload:
|
|
598
|
+
payload["path"] = payload.pop("alias_path")
|
|
599
|
+
if "aliasPath" in payload and "path" not in payload:
|
|
600
|
+
payload["path"] = payload.pop("aliasPath")
|
|
601
|
+
raw_target = payload.get("target_field", payload.get("targetField"))
|
|
602
|
+
if isinstance(raw_target, str):
|
|
603
|
+
payload["target_field"] = {"name": raw_target}
|
|
604
|
+
return payload
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
class QLinkerRequestPatch(StrictModel):
|
|
608
|
+
url: str = ""
|
|
609
|
+
method: str = "GET"
|
|
610
|
+
headers: list[QLinkerKeyValuePatch] = Field(default_factory=list)
|
|
611
|
+
query_params: list[QLinkerKeyValuePatch] = Field(
|
|
612
|
+
default_factory=list,
|
|
613
|
+
validation_alias=AliasChoices("query_params", "queryParams"),
|
|
614
|
+
)
|
|
615
|
+
body_type: int = Field(default=1, validation_alias=AliasChoices("body_type", "bodyType"))
|
|
616
|
+
url_encoded_value: list[QLinkerKeyValuePatch] = Field(
|
|
617
|
+
default_factory=list,
|
|
618
|
+
validation_alias=AliasChoices("url_encoded_value", "urlEncodedValue"),
|
|
619
|
+
)
|
|
620
|
+
json_value: str | None = Field(default=None, validation_alias=AliasChoices("json_value", "jsonValue"))
|
|
621
|
+
xml_value: str | None = Field(default=None, validation_alias=AliasChoices("xml_value", "xmlValue"))
|
|
622
|
+
result_type: int = Field(default=1, validation_alias=AliasChoices("result_type", "resultType"))
|
|
623
|
+
auto_trigger: bool | None = Field(default=None, validation_alias=AliasChoices("auto_trigger", "autoTrigger"))
|
|
624
|
+
custom_button_text_enabled: bool | None = Field(
|
|
625
|
+
default=None,
|
|
626
|
+
validation_alias=AliasChoices("custom_button_text_enabled", "customButtonTextEnabled", "custom_btn_text_status", "customBtnTextStatus"),
|
|
627
|
+
)
|
|
628
|
+
custom_button_text: str | None = Field(
|
|
629
|
+
default=None,
|
|
630
|
+
validation_alias=AliasChoices("custom_button_text", "customButtonText", "custom_btn_text", "customBtnText"),
|
|
631
|
+
)
|
|
632
|
+
being_insert_value_directly: bool | None = Field(
|
|
633
|
+
default=None,
|
|
634
|
+
validation_alias=AliasChoices("being_insert_value_directly", "beingInsertValueDirectly"),
|
|
635
|
+
)
|
|
636
|
+
being_hide_on_form: bool | None = Field(
|
|
637
|
+
default=None,
|
|
638
|
+
validation_alias=AliasChoices("being_hide_on_form", "beingHideOnForm"),
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
class QLinkerBindingPatch(StrictModel):
|
|
643
|
+
inputs: list[QLinkerInputBindingPatch] = Field(default_factory=list)
|
|
644
|
+
request: QLinkerRequestPatch
|
|
645
|
+
outputs: list[QLinkerOutputBindingPatch] = Field(default_factory=list)
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
class CodeBlockOutputBindingPatch(StrictModel):
|
|
649
|
+
alias: str
|
|
650
|
+
path: str
|
|
651
|
+
target_field: FieldSelector = Field(
|
|
652
|
+
validation_alias=AliasChoices("target_field", "targetField"),
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
@model_validator(mode="before")
|
|
656
|
+
@classmethod
|
|
657
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
658
|
+
if not isinstance(value, dict):
|
|
659
|
+
return value
|
|
660
|
+
payload = dict(value)
|
|
661
|
+
if "alias_name" in payload and "alias" not in payload:
|
|
662
|
+
payload["alias"] = payload.pop("alias_name")
|
|
663
|
+
if "aliasName" in payload and "alias" not in payload:
|
|
664
|
+
payload["alias"] = payload.pop("aliasName")
|
|
665
|
+
if "alias_path" in payload and "path" not in payload:
|
|
666
|
+
payload["path"] = payload.pop("alias_path")
|
|
667
|
+
if "aliasPath" in payload and "path" not in payload:
|
|
668
|
+
payload["path"] = payload.pop("aliasPath")
|
|
669
|
+
raw_target = payload.get("target_field", payload.get("targetField"))
|
|
670
|
+
if isinstance(raw_target, str):
|
|
671
|
+
payload["target_field"] = {"name": raw_target}
|
|
672
|
+
return payload
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
class CodeBlockBindingPatch(StrictModel):
|
|
676
|
+
inputs: list[CodeBlockInputBindingPatch] = Field(default_factory=list)
|
|
677
|
+
code: str = ""
|
|
678
|
+
auto_trigger: bool | None = Field(default=None, validation_alias=AliasChoices("auto_trigger", "autoTrigger"))
|
|
679
|
+
custom_button_text_enabled: bool | None = Field(
|
|
680
|
+
default=None,
|
|
681
|
+
validation_alias=AliasChoices("custom_button_text_enabled", "customButtonTextEnabled", "custom_btn_text_status", "customBtnTextStatus"),
|
|
682
|
+
)
|
|
683
|
+
custom_button_text: str | None = Field(
|
|
684
|
+
default=None,
|
|
685
|
+
validation_alias=AliasChoices("custom_button_text", "customButtonText", "custom_btn_text", "customBtnText"),
|
|
686
|
+
)
|
|
687
|
+
outputs: list[CodeBlockOutputBindingPatch] = Field(default_factory=list)
|
|
688
|
+
|
|
689
|
+
@model_validator(mode="before")
|
|
690
|
+
@classmethod
|
|
691
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
692
|
+
if not isinstance(value, dict):
|
|
693
|
+
return value
|
|
694
|
+
payload = dict(value)
|
|
695
|
+
if "code_content" in payload and "code" not in payload:
|
|
696
|
+
payload["code"] = payload.pop("code_content")
|
|
697
|
+
if "codeContent" in payload and "code" not in payload:
|
|
698
|
+
payload["code"] = payload.pop("codeContent")
|
|
699
|
+
return payload
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
class FieldPatch(StrictModel):
|
|
703
|
+
name: str = Field(validation_alias=AliasChoices("name", "title", "label"))
|
|
704
|
+
type: PublicFieldType
|
|
705
|
+
required: bool = False
|
|
706
|
+
description: str | None = None
|
|
707
|
+
options: list[str] = Field(default_factory=list)
|
|
708
|
+
target_app_key: str | None = None
|
|
709
|
+
display_field: FieldSelector | None = None
|
|
710
|
+
visible_fields: list[FieldSelector] = Field(default_factory=list)
|
|
711
|
+
relation_mode: PublicRelationMode | None = Field(default=None, validation_alias=AliasChoices("relation_mode", "relationMode", "selection_mode", "selectionMode"))
|
|
712
|
+
department_scope: DepartmentScopePatch | None = Field(
|
|
713
|
+
default=None,
|
|
714
|
+
validation_alias=AliasChoices("department_scope", "departmentScope"),
|
|
715
|
+
)
|
|
716
|
+
remote_lookup_config: RemoteLookupConfigPatch | None = Field(
|
|
717
|
+
default=None,
|
|
718
|
+
validation_alias=AliasChoices("remote_lookup_config", "remoteLookupConfig"),
|
|
719
|
+
)
|
|
720
|
+
q_linker_binding: QLinkerBindingPatch | None = Field(
|
|
721
|
+
default=None,
|
|
722
|
+
validation_alias=AliasChoices("q_linker_binding", "qLinkerBinding"),
|
|
723
|
+
)
|
|
724
|
+
code_block_config: CodeBlockConfigPatch | None = Field(
|
|
725
|
+
default=None,
|
|
726
|
+
validation_alias=AliasChoices("code_block_config", "codeBlockConfig"),
|
|
727
|
+
)
|
|
728
|
+
code_block_binding: CodeBlockBindingPatch | None = Field(
|
|
729
|
+
default=None,
|
|
730
|
+
validation_alias=AliasChoices("code_block_binding", "codeBlockBinding"),
|
|
731
|
+
)
|
|
732
|
+
auto_trigger: bool | None = Field(default=None, validation_alias=AliasChoices("auto_trigger", "autoTrigger"))
|
|
733
|
+
custom_button_text_enabled: bool | None = Field(
|
|
734
|
+
default=None,
|
|
735
|
+
validation_alias=AliasChoices("custom_button_text_enabled", "customButtonTextEnabled", "custom_btn_text_status", "customBtnTextStatus"),
|
|
736
|
+
)
|
|
737
|
+
custom_button_text: str | None = Field(
|
|
738
|
+
default=None,
|
|
739
|
+
validation_alias=AliasChoices("custom_button_text", "customButtonText", "custom_btn_text", "customBtnText"),
|
|
740
|
+
)
|
|
741
|
+
subfields: list["FieldPatch"] = Field(default_factory=list)
|
|
742
|
+
|
|
743
|
+
@model_validator(mode="after")
|
|
744
|
+
def validate_shape(self) -> "FieldPatch":
|
|
745
|
+
if self.type == PublicFieldType.relation and not self.target_app_key:
|
|
746
|
+
raise ValueError("relation field requires target_app_key")
|
|
747
|
+
if self.type == PublicFieldType.relation and self.display_field is None:
|
|
748
|
+
raise ValueError("relation field requires display_field")
|
|
749
|
+
if self.type == PublicFieldType.relation and not self.visible_fields:
|
|
750
|
+
raise ValueError("relation field requires visible_fields")
|
|
751
|
+
if self.type != PublicFieldType.relation and self.target_app_key:
|
|
752
|
+
raise ValueError("target_app_key is only allowed for relation fields")
|
|
753
|
+
if self.type != PublicFieldType.relation and (self.display_field is not None or self.visible_fields or self.relation_mode is not None):
|
|
754
|
+
raise ValueError("display_field, visible_fields, and relation_mode are only allowed for relation fields")
|
|
755
|
+
if self.type != PublicFieldType.department and self.department_scope is not None:
|
|
756
|
+
raise ValueError("department_scope is only allowed for department fields")
|
|
757
|
+
if self.type != PublicFieldType.q_linker and (
|
|
758
|
+
self.remote_lookup_config is not None
|
|
759
|
+
or self.q_linker_binding is not None
|
|
760
|
+
):
|
|
761
|
+
raise ValueError("remote_lookup_config and q_linker_binding are only allowed for q_linker fields")
|
|
762
|
+
if self.type != PublicFieldType.code_block and (
|
|
763
|
+
self.code_block_config is not None
|
|
764
|
+
or self.code_block_binding is not None
|
|
765
|
+
or self.auto_trigger is not None
|
|
766
|
+
or self.custom_button_text_enabled is not None
|
|
767
|
+
or self.custom_button_text is not None
|
|
768
|
+
):
|
|
769
|
+
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")
|
|
770
|
+
if self.type == PublicFieldType.subtable and not self.subfields:
|
|
771
|
+
raise ValueError("subtable field requires subfields")
|
|
772
|
+
if self.type != PublicFieldType.subtable and self.subfields:
|
|
773
|
+
raise ValueError("subfields are only allowed for subtable fields")
|
|
774
|
+
return self
|
|
775
|
+
|
|
776
|
+
@model_validator(mode="before")
|
|
777
|
+
@classmethod
|
|
778
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
779
|
+
return _normalize_field_payload(value)
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
class FieldMutation(StrictModel):
|
|
783
|
+
name: str | None = Field(default=None, validation_alias=AliasChoices("name", "title", "label"))
|
|
784
|
+
type: PublicFieldType | None = None
|
|
785
|
+
required: bool | None = None
|
|
786
|
+
description: str | None = None
|
|
787
|
+
options: list[str] | None = None
|
|
788
|
+
target_app_key: str | None = None
|
|
789
|
+
display_field: FieldSelector | None = None
|
|
790
|
+
visible_fields: list[FieldSelector] | None = None
|
|
791
|
+
relation_mode: PublicRelationMode | None = Field(default=None, validation_alias=AliasChoices("relation_mode", "relationMode", "selection_mode", "selectionMode"))
|
|
792
|
+
department_scope: DepartmentScopePatch | None = Field(
|
|
793
|
+
default=None,
|
|
794
|
+
validation_alias=AliasChoices("department_scope", "departmentScope"),
|
|
795
|
+
)
|
|
796
|
+
remote_lookup_config: RemoteLookupConfigPatch | None = Field(
|
|
797
|
+
default=None,
|
|
798
|
+
validation_alias=AliasChoices("remote_lookup_config", "remoteLookupConfig"),
|
|
799
|
+
)
|
|
800
|
+
q_linker_binding: QLinkerBindingPatch | None = Field(
|
|
801
|
+
default=None,
|
|
802
|
+
validation_alias=AliasChoices("q_linker_binding", "qLinkerBinding"),
|
|
803
|
+
)
|
|
804
|
+
code_block_config: CodeBlockConfigPatch | None = Field(
|
|
805
|
+
default=None,
|
|
806
|
+
validation_alias=AliasChoices("code_block_config", "codeBlockConfig"),
|
|
807
|
+
)
|
|
808
|
+
code_block_binding: CodeBlockBindingPatch | None = Field(
|
|
809
|
+
default=None,
|
|
810
|
+
validation_alias=AliasChoices("code_block_binding", "codeBlockBinding"),
|
|
811
|
+
)
|
|
812
|
+
auto_trigger: bool | None = Field(default=None, validation_alias=AliasChoices("auto_trigger", "autoTrigger"))
|
|
813
|
+
custom_button_text_enabled: bool | None = Field(
|
|
814
|
+
default=None,
|
|
815
|
+
validation_alias=AliasChoices("custom_button_text_enabled", "customButtonTextEnabled", "custom_btn_text_status", "customBtnTextStatus"),
|
|
816
|
+
)
|
|
817
|
+
custom_button_text: str | None = Field(
|
|
818
|
+
default=None,
|
|
819
|
+
validation_alias=AliasChoices("custom_button_text", "customButtonText", "custom_btn_text", "customBtnText"),
|
|
820
|
+
)
|
|
821
|
+
subfields: list[FieldPatch] | None = None
|
|
822
|
+
subfield_updates: list["FieldUpdatePatch"] | None = Field(
|
|
823
|
+
default=None,
|
|
824
|
+
validation_alias=AliasChoices("subfield_updates", "subfieldUpdates"),
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
@model_validator(mode="after")
|
|
828
|
+
def validate_shape(self) -> "FieldMutation":
|
|
829
|
+
if self.type == PublicFieldType.relation and not self.target_app_key:
|
|
830
|
+
raise ValueError("relation field requires target_app_key")
|
|
831
|
+
relation_patch = (
|
|
832
|
+
self.type == PublicFieldType.relation
|
|
833
|
+
or self.target_app_key is not None
|
|
834
|
+
or self.display_field is not None
|
|
835
|
+
or self.visible_fields is not None
|
|
836
|
+
or self.relation_mode is not None
|
|
837
|
+
)
|
|
838
|
+
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):
|
|
839
|
+
raise ValueError("display_field, visible_fields, and relation_mode are only allowed for relation fields")
|
|
840
|
+
if self.type is not None and self.type != PublicFieldType.department and self.department_scope is not None:
|
|
841
|
+
raise ValueError("department_scope is only allowed for department fields")
|
|
842
|
+
if self.type is not None and self.type != PublicFieldType.q_linker and (
|
|
843
|
+
self.remote_lookup_config is not None
|
|
844
|
+
or self.q_linker_binding is not None
|
|
845
|
+
):
|
|
846
|
+
raise ValueError("remote_lookup_config and q_linker_binding are only allowed for q_linker fields")
|
|
847
|
+
if self.type is not None and self.type != PublicFieldType.code_block and (
|
|
848
|
+
self.code_block_config is not None
|
|
849
|
+
or self.code_block_binding is not None
|
|
850
|
+
or self.auto_trigger is not None
|
|
851
|
+
or self.custom_button_text_enabled is not None
|
|
852
|
+
or self.custom_button_text is not None
|
|
853
|
+
):
|
|
854
|
+
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")
|
|
855
|
+
if self.type == PublicFieldType.subtable and not self.subfields and not self.subfield_updates:
|
|
856
|
+
raise ValueError("subtable field requires subfields or subfield_updates")
|
|
857
|
+
if self.type is not None and self.type != PublicFieldType.subtable and self.subfield_updates:
|
|
858
|
+
raise ValueError("subfield_updates are only allowed for subtable fields")
|
|
859
|
+
if self.subfields and self.subfield_updates:
|
|
860
|
+
raise ValueError("subfields and subfield_updates cannot be used together")
|
|
861
|
+
return self
|
|
862
|
+
|
|
863
|
+
@model_validator(mode="before")
|
|
864
|
+
@classmethod
|
|
865
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
866
|
+
return _normalize_field_payload(value)
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
class FieldUpdatePatch(StrictModel):
|
|
870
|
+
selector: FieldSelector
|
|
871
|
+
set: FieldMutation
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
class FieldRemovePatch(StrictModel):
|
|
875
|
+
field_id: str | None = None
|
|
876
|
+
que_id: int | None = None
|
|
877
|
+
name: str | None = Field(default=None, validation_alias=AliasChoices("name", "title", "label"))
|
|
878
|
+
|
|
879
|
+
@model_validator(mode="after")
|
|
880
|
+
def validate_shape(self) -> "FieldRemovePatch":
|
|
881
|
+
if not any((self.field_id, self.que_id, self.name)):
|
|
882
|
+
raise ValueError("remove patch must include field_id, que_id, or name")
|
|
883
|
+
return self
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
def _coerce_layout_columns(value: Any) -> int | None:
|
|
887
|
+
if isinstance(value, bool):
|
|
888
|
+
return None
|
|
889
|
+
if isinstance(value, int):
|
|
890
|
+
return value if value > 0 else None
|
|
891
|
+
if isinstance(value, str):
|
|
892
|
+
stripped = value.strip()
|
|
893
|
+
if stripped.isdigit():
|
|
894
|
+
parsed = int(stripped)
|
|
895
|
+
return parsed if parsed > 0 else None
|
|
896
|
+
return None
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
def _normalize_layout_rows(value: Any, *, columns: int | None = None) -> Any:
|
|
900
|
+
if not isinstance(value, list):
|
|
901
|
+
return value
|
|
902
|
+
if value and all(isinstance(item, list) for item in value):
|
|
903
|
+
return value
|
|
904
|
+
if not value:
|
|
905
|
+
return []
|
|
906
|
+
width = columns if columns and columns > 0 else None
|
|
907
|
+
if width is None:
|
|
908
|
+
return [list(value)]
|
|
909
|
+
return [list(value[index : index + width]) for index in range(0, len(value), width) if value[index : index + width]]
|
|
910
|
+
|
|
911
|
+
|
|
912
|
+
class LayoutSectionPatch(StrictModel):
|
|
913
|
+
type: str | None = Field(default=None, validation_alias=AliasChoices("type", "kind", "block_type", "blockType"))
|
|
914
|
+
section_id: str | None = Field(default=None, validation_alias=AliasChoices("section_id", "sectionId", "paragraph_id", "paragraphId"))
|
|
915
|
+
title: str = Field(validation_alias=AliasChoices("title", "name", "paragraph_title", "paragraphTitle"))
|
|
916
|
+
rows: list[list[Any]] = Field(default_factory=list)
|
|
917
|
+
|
|
918
|
+
@model_validator(mode="before")
|
|
919
|
+
@classmethod
|
|
920
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
921
|
+
if not isinstance(value, dict):
|
|
922
|
+
return value
|
|
923
|
+
payload = dict(value)
|
|
924
|
+
if "name" in payload and "title" not in payload:
|
|
925
|
+
payload["title"] = payload.pop("name")
|
|
926
|
+
if "paragraph_title" in payload and "title" not in payload:
|
|
927
|
+
payload["title"] = payload.pop("paragraph_title")
|
|
928
|
+
if "paragraphTitle" in payload and "title" not in payload:
|
|
929
|
+
payload["title"] = payload.pop("paragraphTitle")
|
|
930
|
+
shorthand: Any | None = None
|
|
931
|
+
if "rows" not in payload:
|
|
932
|
+
if "fields" in payload:
|
|
933
|
+
shorthand = payload.pop("fields")
|
|
934
|
+
elif "field_ids" in payload:
|
|
935
|
+
shorthand = payload.pop("field_ids")
|
|
936
|
+
if shorthand is not None:
|
|
937
|
+
payload["rows"] = _normalize_layout_rows(
|
|
938
|
+
shorthand,
|
|
939
|
+
columns=_coerce_layout_columns(payload.pop("columns", None)),
|
|
940
|
+
)
|
|
941
|
+
return payload
|
|
942
|
+
|
|
943
|
+
@model_validator(mode="after")
|
|
944
|
+
def validate_rows(self) -> "LayoutSectionPatch":
|
|
945
|
+
if self.type is not None and str(self.type).strip().lower() != "paragraph":
|
|
946
|
+
raise ValueError("layout section type must be 'paragraph'")
|
|
947
|
+
if not self.rows:
|
|
948
|
+
raise ValueError("section rows must be a non-empty list")
|
|
949
|
+
for row in self.rows:
|
|
950
|
+
if not isinstance(row, list) or not row:
|
|
951
|
+
raise ValueError("section rows must be a non-empty list")
|
|
952
|
+
if not self.section_id:
|
|
953
|
+
self.section_id = _slugify_title(self.title)
|
|
954
|
+
return self
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
class FlowNodePatch(StrictModel):
|
|
958
|
+
id: str
|
|
959
|
+
type: PublicFlowNodeType
|
|
960
|
+
name: str
|
|
961
|
+
assignees: FlowAssigneePatch = Field(default_factory=FlowAssigneePatch)
|
|
962
|
+
permissions: FlowNodePermissionsPatch = Field(default_factory=FlowNodePermissionsPatch)
|
|
963
|
+
conditions: list[FlowConditionRulePatch] = Field(default_factory=list)
|
|
964
|
+
condition_groups: list[list[FlowConditionRulePatch]] = Field(default_factory=list)
|
|
965
|
+
config: dict[str, Any] = Field(default_factory=dict)
|
|
966
|
+
|
|
967
|
+
@model_validator(mode="before")
|
|
968
|
+
@classmethod
|
|
969
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
970
|
+
if not isinstance(value, dict):
|
|
971
|
+
return value
|
|
972
|
+
payload = dict(value)
|
|
973
|
+
assignees = dict(payload.get("assignees") or {})
|
|
974
|
+
permissions = dict(payload.get("permissions") or {})
|
|
975
|
+
|
|
976
|
+
for key in ("role_ids", "role_names", "member_uids", "member_emails", "member_names", "include_sub_departs"):
|
|
977
|
+
if key in payload and key not in assignees:
|
|
978
|
+
assignees[key] = payload.pop(key)
|
|
979
|
+
for key in ("editable_fields",):
|
|
980
|
+
if key in payload and key not in permissions:
|
|
981
|
+
permissions[key] = payload.pop(key)
|
|
982
|
+
if "filters" in payload and "conditions" not in payload:
|
|
983
|
+
payload["conditions"] = payload.pop("filters")
|
|
984
|
+
if "rules" in payload and "conditions" not in payload:
|
|
985
|
+
payload["conditions"] = payload.pop("rules")
|
|
986
|
+
if "conditionRules" in payload and "condition_groups" not in payload:
|
|
987
|
+
payload["condition_groups"] = payload.pop("conditionRules")
|
|
988
|
+
if "conditionGroups" in payload and "condition_groups" not in payload:
|
|
989
|
+
payload["condition_groups"] = payload.pop("conditionGroups")
|
|
990
|
+
if "owners" in payload and "member_names" not in assignees:
|
|
991
|
+
assignees["member_names"] = payload.pop("owners")
|
|
992
|
+
if "approvers" in payload and "role_names" not in assignees:
|
|
993
|
+
assignees["role_names"] = payload.pop("approvers")
|
|
994
|
+
if assignees:
|
|
995
|
+
payload["assignees"] = assignees
|
|
996
|
+
if permissions:
|
|
997
|
+
payload["permissions"] = permissions
|
|
998
|
+
return payload
|
|
999
|
+
|
|
1000
|
+
@model_validator(mode="after")
|
|
1001
|
+
def validate_branch_conditions(self) -> "FlowNodePatch":
|
|
1002
|
+
if self.conditions:
|
|
1003
|
+
self.condition_groups = [list(self.conditions), *self.condition_groups]
|
|
1004
|
+
self.conditions = []
|
|
1005
|
+
if self.type != PublicFlowNodeType.condition and self.condition_groups:
|
|
1006
|
+
raise ValueError("condition_groups are only allowed on condition nodes")
|
|
1007
|
+
return self
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
class FlowTransitionPatch(StrictModel):
|
|
1011
|
+
source: str = Field(alias="from")
|
|
1012
|
+
target: str = Field(alias="to")
|
|
1013
|
+
|
|
1014
|
+
|
|
1015
|
+
class ViewUpsertPatch(StrictModel):
|
|
1016
|
+
name: str
|
|
1017
|
+
view_key: str | None = Field(default=None, validation_alias=AliasChoices("view_key", "viewKey"))
|
|
1018
|
+
type: PublicViewType
|
|
1019
|
+
columns: list[str] = Field(default_factory=list)
|
|
1020
|
+
group_by: str | None = None
|
|
1021
|
+
filters: list[ViewFilterRulePatch] = Field(default_factory=list)
|
|
1022
|
+
start_field: str | None = Field(default=None, validation_alias=AliasChoices("start_field", "startField"))
|
|
1023
|
+
end_field: str | None = Field(default=None, validation_alias=AliasChoices("end_field", "endField"))
|
|
1024
|
+
title_field: str | None = Field(default=None, validation_alias=AliasChoices("title_field", "titleField"))
|
|
1025
|
+
buttons: list["ViewButtonBindingPatch"] | None = None
|
|
1026
|
+
visibility: VisibilityPatch | None = None
|
|
1027
|
+
|
|
1028
|
+
@model_validator(mode="before")
|
|
1029
|
+
@classmethod
|
|
1030
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
1031
|
+
if not isinstance(value, dict):
|
|
1032
|
+
return value
|
|
1033
|
+
payload = dict(value)
|
|
1034
|
+
if "fields" in payload and "columns" not in payload:
|
|
1035
|
+
payload["columns"] = payload.pop("fields")
|
|
1036
|
+
if "column_names" in payload and "columns" not in payload:
|
|
1037
|
+
payload["columns"] = payload.pop("column_names")
|
|
1038
|
+
if "columnNames" in payload and "columns" not in payload:
|
|
1039
|
+
payload["columns"] = payload.pop("columnNames")
|
|
1040
|
+
if "filter_rules" in payload and "filters" not in payload:
|
|
1041
|
+
payload["filters"] = payload.pop("filter_rules")
|
|
1042
|
+
if "filterRules" in payload and "filters" not in payload:
|
|
1043
|
+
payload["filters"] = payload.pop("filterRules")
|
|
1044
|
+
raw_type = payload.get("type")
|
|
1045
|
+
if isinstance(raw_type, str):
|
|
1046
|
+
normalized = raw_type.strip().lower()
|
|
1047
|
+
if normalized == "tableview":
|
|
1048
|
+
payload["type"] = "table"
|
|
1049
|
+
elif normalized == "cardview":
|
|
1050
|
+
payload["type"] = "card"
|
|
1051
|
+
elif normalized == "kanban":
|
|
1052
|
+
payload["type"] = "board"
|
|
1053
|
+
elif normalized == "ganttview":
|
|
1054
|
+
payload["type"] = "gantt"
|
|
1055
|
+
return payload
|
|
1056
|
+
|
|
1057
|
+
@model_validator(mode="after")
|
|
1058
|
+
def validate_shape(self) -> "ViewUpsertPatch":
|
|
1059
|
+
if self.type in {PublicViewType.table, PublicViewType.card} and not self.columns:
|
|
1060
|
+
raise ValueError("table/card views require columns")
|
|
1061
|
+
if self.type == PublicViewType.board and not self.group_by:
|
|
1062
|
+
raise ValueError("board view requires group_by")
|
|
1063
|
+
if self.type == PublicViewType.gantt and not (self.start_field and self.end_field):
|
|
1064
|
+
raise ValueError("gantt view requires start_field and end_field")
|
|
1065
|
+
return self
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
class CustomButtonJudgeValuePatch(StrictModel):
|
|
1069
|
+
id: int | str | None = None
|
|
1070
|
+
value: Any | None = None
|
|
1071
|
+
|
|
1072
|
+
@model_validator(mode="before")
|
|
1073
|
+
@classmethod
|
|
1074
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
1075
|
+
if not isinstance(value, dict):
|
|
1076
|
+
return value
|
|
1077
|
+
payload = dict(value)
|
|
1078
|
+
if "opt_id" in payload and "id" not in payload:
|
|
1079
|
+
payload["id"] = payload.pop("opt_id")
|
|
1080
|
+
if "member_id" in payload and "id" not in payload:
|
|
1081
|
+
payload["id"] = payload.pop("member_id")
|
|
1082
|
+
return payload
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
class CustomButtonQuestionRefPatch(StrictModel):
|
|
1086
|
+
que_id: int = Field(validation_alias=AliasChoices("que_id", "queId"))
|
|
1087
|
+
que_title: str | None = Field(default=None, validation_alias=AliasChoices("que_title", "queTitle"))
|
|
1088
|
+
que_type: int | None = Field(default=None, validation_alias=AliasChoices("que_type", "queType"))
|
|
1089
|
+
|
|
1090
|
+
|
|
1091
|
+
class CustomButtonMatchRulePatch(StrictModel):
|
|
1092
|
+
que_id: int = Field(validation_alias=AliasChoices("que_id", "queId"))
|
|
1093
|
+
que_title: str | None = Field(default=None, validation_alias=AliasChoices("que_title", "queTitle"))
|
|
1094
|
+
que_type: int | None = Field(default=None, validation_alias=AliasChoices("que_type", "queType"))
|
|
1095
|
+
date_type: int | None = Field(default=None, validation_alias=AliasChoices("date_type", "dateType"))
|
|
1096
|
+
judge_type: int | None = Field(default=None, validation_alias=AliasChoices("judge_type", "judgeType"))
|
|
1097
|
+
match_type: int | None = Field(default=None, validation_alias=AliasChoices("match_type", "matchType"))
|
|
1098
|
+
judge_values: list[str] = Field(default_factory=list, validation_alias=AliasChoices("judge_values", "judgeValues"))
|
|
1099
|
+
judge_que_type: int | None = Field(default=None, validation_alias=AliasChoices("judge_que_type", "judgeQueType"))
|
|
1100
|
+
judge_que_id: int | None = Field(default=None, validation_alias=AliasChoices("judge_que_id", "judgeQueId"))
|
|
1101
|
+
judge_que_detail: CustomButtonQuestionRefPatch | None = Field(
|
|
1102
|
+
default=None,
|
|
1103
|
+
validation_alias=AliasChoices("judge_que_detail", "judgeQueDetail"),
|
|
1104
|
+
)
|
|
1105
|
+
judge_value_details: list[CustomButtonJudgeValuePatch] = Field(
|
|
1106
|
+
default_factory=list,
|
|
1107
|
+
validation_alias=AliasChoices("judge_value_details", "judgeValueDetails"),
|
|
1108
|
+
)
|
|
1109
|
+
path_value: str | None = Field(default=None, validation_alias=AliasChoices("path_value", "pathValue"))
|
|
1110
|
+
table_update_type: int | None = Field(default=None, validation_alias=AliasChoices("table_update_type", "tableUpdateType"))
|
|
1111
|
+
multi_value: bool | None = Field(default=None, validation_alias=AliasChoices("multi_value", "multiValue"))
|
|
1112
|
+
add_rule: str | None = Field(default=None, validation_alias=AliasChoices("add_rule", "addRule"))
|
|
1113
|
+
filter_condition: list[list["CustomButtonMatchRulePatch"]] = Field(
|
|
1114
|
+
default_factory=list,
|
|
1115
|
+
validation_alias=AliasChoices("filter_condition", "filterCondition"),
|
|
1116
|
+
)
|
|
1117
|
+
field_id_prefix: str | None = Field(default=None, validation_alias=AliasChoices("field_id_prefix", "fieldIdPrefix"))
|
|
1118
|
+
|
|
1119
|
+
@model_validator(mode="before")
|
|
1120
|
+
@classmethod
|
|
1121
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
1122
|
+
if not isinstance(value, dict):
|
|
1123
|
+
return value
|
|
1124
|
+
payload = dict(value)
|
|
1125
|
+
if "judgeValue" in payload and "judge_values" not in payload and "judgeValues" not in payload:
|
|
1126
|
+
payload["judge_values"] = [payload.pop("judgeValue")]
|
|
1127
|
+
return payload
|
|
1128
|
+
|
|
1129
|
+
|
|
1130
|
+
class CustomButtonAddDataConfigPatch(StrictModel):
|
|
1131
|
+
related_app_key: str | None = Field(default=None, validation_alias=AliasChoices("related_app_key", "relatedAppKey"))
|
|
1132
|
+
related_app_name: str | None = Field(default=None, validation_alias=AliasChoices("related_app_name", "relatedAppName"))
|
|
1133
|
+
que_relation: list[CustomButtonMatchRulePatch] = Field(
|
|
1134
|
+
default_factory=list,
|
|
1135
|
+
validation_alias=AliasChoices("que_relation", "queRelation"),
|
|
1136
|
+
)
|
|
1137
|
+
|
|
1138
|
+
|
|
1139
|
+
class CustomButtonExternalQRobotConfigPatch(StrictModel):
|
|
1140
|
+
external_qrobot_config_id: int | None = Field(
|
|
1141
|
+
default=None,
|
|
1142
|
+
validation_alias=AliasChoices("external_qrobot_config_id", "externalQRobotConfigId"),
|
|
1143
|
+
)
|
|
1144
|
+
triggered_text: str | None = Field(default=None, validation_alias=AliasChoices("triggered_text", "triggeredText"))
|
|
1145
|
+
|
|
1146
|
+
|
|
1147
|
+
class CustomButtonWingsConfigPatch(StrictModel):
|
|
1148
|
+
wings_agent_id: int | None = Field(default=None, validation_alias=AliasChoices("wings_agent_id", "wingsAgentId"))
|
|
1149
|
+
wings_agent_name: str | None = Field(default=None, validation_alias=AliasChoices("wings_agent_name", "wingsAgentName"))
|
|
1150
|
+
bind_que_id_list: list[int] = Field(default_factory=list, validation_alias=AliasChoices("bind_que_id_list", "bindQueIdList"))
|
|
1151
|
+
bind_file_que_id_list: list[int] = Field(
|
|
1152
|
+
default_factory=list,
|
|
1153
|
+
validation_alias=AliasChoices("bind_file_que_id_list", "bindFileQueIdList"),
|
|
1154
|
+
)
|
|
1155
|
+
default_prompt: str | None = Field(default=None, validation_alias=AliasChoices("default_prompt", "defaultPrompt"))
|
|
1156
|
+
being_auto_send: bool | None = Field(default=None, validation_alias=AliasChoices("being_auto_send", "beingAutoSend"))
|
|
1157
|
+
|
|
1158
|
+
@model_validator(mode="before")
|
|
1159
|
+
@classmethod
|
|
1160
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
1161
|
+
if not isinstance(value, dict):
|
|
1162
|
+
return value
|
|
1163
|
+
payload = dict(value)
|
|
1164
|
+
raw_agent_id = payload.get("wings_agent_id", payload.get("wingsAgentId"))
|
|
1165
|
+
if isinstance(raw_agent_id, str) and raw_agent_id.strip().isdigit():
|
|
1166
|
+
payload["wings_agent_id"] = int(raw_agent_id.strip())
|
|
1167
|
+
return payload
|
|
1168
|
+
|
|
1169
|
+
|
|
1170
|
+
def _is_white_button_color(value: str | None) -> bool:
|
|
1171
|
+
normalized = str(value or "").strip().lower().replace(" ", "")
|
|
1172
|
+
return normalized in {
|
|
1173
|
+
"#fff",
|
|
1174
|
+
"#ffffff",
|
|
1175
|
+
"white",
|
|
1176
|
+
"rgb(255,255,255)",
|
|
1177
|
+
"rgba(255,255,255,1)",
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
class CustomButtonPatch(StrictModel):
|
|
1182
|
+
button_text: str = Field(validation_alias=AliasChoices("button_text", "buttonText"))
|
|
1183
|
+
background_color: str = Field(validation_alias=AliasChoices("background_color", "backgroundColor"))
|
|
1184
|
+
text_color: str = Field(validation_alias=AliasChoices("text_color", "textColor"))
|
|
1185
|
+
button_icon: str = Field(validation_alias=AliasChoices("button_icon", "buttonIcon"))
|
|
1186
|
+
icon_color: str | None = Field(default=None, validation_alias=AliasChoices("icon_color", "iconColor"))
|
|
1187
|
+
trigger_action: PublicButtonTriggerAction = Field(validation_alias=AliasChoices("trigger_action", "triggerAction"))
|
|
1188
|
+
trigger_link_url: str | None = Field(default=None, validation_alias=AliasChoices("trigger_link_url", "triggerLinkUrl"))
|
|
1189
|
+
trigger_add_data_config: CustomButtonAddDataConfigPatch | None = Field(
|
|
1190
|
+
default=None,
|
|
1191
|
+
validation_alias=AliasChoices("trigger_add_data_config", "triggerAddDataConfig"),
|
|
1192
|
+
)
|
|
1193
|
+
external_qrobot_config: CustomButtonExternalQRobotConfigPatch | None = Field(
|
|
1194
|
+
default=None,
|
|
1195
|
+
validation_alias=AliasChoices(
|
|
1196
|
+
"external_qrobot_config",
|
|
1197
|
+
"externalQrobotConfig",
|
|
1198
|
+
"custom_button_external_qrobot_relation_vo",
|
|
1199
|
+
"customButtonExternalQRobotRelationVO",
|
|
1200
|
+
),
|
|
1201
|
+
)
|
|
1202
|
+
trigger_wings_config: CustomButtonWingsConfigPatch | None = Field(
|
|
1203
|
+
default=None,
|
|
1204
|
+
validation_alias=AliasChoices("trigger_wings_config", "triggerWingsConfig"),
|
|
1205
|
+
)
|
|
1206
|
+
|
|
1207
|
+
@model_validator(mode="after")
|
|
1208
|
+
def validate_shape(self) -> "CustomButtonPatch":
|
|
1209
|
+
if self.trigger_action == PublicButtonTriggerAction.link and not str(self.trigger_link_url or "").strip():
|
|
1210
|
+
raise ValueError("link buttons require trigger_link_url")
|
|
1211
|
+
if self.trigger_action == PublicButtonTriggerAction.add_data and self.trigger_add_data_config is None:
|
|
1212
|
+
raise ValueError("addData buttons require trigger_add_data_config")
|
|
1213
|
+
if self.trigger_action == PublicButtonTriggerAction.qrobot and self.external_qrobot_config is None:
|
|
1214
|
+
raise ValueError("qRobot buttons require external_qrobot_config")
|
|
1215
|
+
if self.trigger_action == PublicButtonTriggerAction.wings and self.trigger_wings_config is None:
|
|
1216
|
+
raise ValueError("wings buttons require trigger_wings_config")
|
|
1217
|
+
if _is_white_button_color(self.background_color) and _is_white_button_color(self.text_color):
|
|
1218
|
+
raise ValueError("background_color and text_color cannot both be white")
|
|
1219
|
+
return self
|
|
1220
|
+
|
|
1221
|
+
|
|
1222
|
+
class ViewButtonBindingPatch(StrictModel):
|
|
1223
|
+
button_type: PublicViewButtonType = Field(validation_alias=AliasChoices("button_type", "buttonType"))
|
|
1224
|
+
config_type: PublicViewButtonConfigType = Field(validation_alias=AliasChoices("config_type", "configType"))
|
|
1225
|
+
button_id: int = Field(validation_alias=AliasChoices("button_id", "buttonId", "id"))
|
|
1226
|
+
button_text: str | None = Field(default=None, validation_alias=AliasChoices("button_text", "buttonText"))
|
|
1227
|
+
button_icon: str | None = Field(default=None, validation_alias=AliasChoices("button_icon", "buttonIcon"))
|
|
1228
|
+
icon_color: str | None = Field(default=None, validation_alias=AliasChoices("icon_color", "iconColor"))
|
|
1229
|
+
background_color: str | None = Field(default=None, validation_alias=AliasChoices("background_color", "backgroundColor"))
|
|
1230
|
+
text_color: str | None = Field(default=None, validation_alias=AliasChoices("text_color", "textColor"))
|
|
1231
|
+
trigger_action: str | None = Field(default=None, validation_alias=AliasChoices("trigger_action", "triggerAction"))
|
|
1232
|
+
print_tpls: list[Any] = Field(default_factory=list, validation_alias=AliasChoices("print_tpls", "printTpls"))
|
|
1233
|
+
being_main: bool = Field(default=False, validation_alias=AliasChoices("being_main", "beingMain"))
|
|
1234
|
+
button_limit: list[list[ViewFilterRulePatch]] = Field(
|
|
1235
|
+
default_factory=list,
|
|
1236
|
+
validation_alias=AliasChoices("button_limit", "buttonLimit"),
|
|
1237
|
+
)
|
|
1238
|
+
button_formula: str | None = Field(default=None, validation_alias=AliasChoices("button_formula", "buttonFormula"))
|
|
1239
|
+
button_formula_type: int = Field(default=1, validation_alias=AliasChoices("button_formula_type", "buttonFormulaType"))
|
|
1240
|
+
|
|
1241
|
+
@model_validator(mode="before")
|
|
1242
|
+
@classmethod
|
|
1243
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
1244
|
+
if not isinstance(value, dict):
|
|
1245
|
+
return value
|
|
1246
|
+
payload = dict(value)
|
|
1247
|
+
raw_button_type = payload.get("button_type", payload.get("buttonType"))
|
|
1248
|
+
if isinstance(raw_button_type, str):
|
|
1249
|
+
normalized_type = raw_button_type.strip().lower()
|
|
1250
|
+
if normalized_type == "system":
|
|
1251
|
+
payload["button_type"] = "SYSTEM"
|
|
1252
|
+
elif normalized_type == "custom":
|
|
1253
|
+
payload["button_type"] = "CUSTOM"
|
|
1254
|
+
raw_config_type = payload.get("config_type", payload.get("configType"))
|
|
1255
|
+
if isinstance(raw_config_type, str):
|
|
1256
|
+
normalized_config = raw_config_type.strip().lower()
|
|
1257
|
+
if normalized_config == "top":
|
|
1258
|
+
payload["config_type"] = "TOP"
|
|
1259
|
+
elif normalized_config == "detail":
|
|
1260
|
+
payload["config_type"] = "DETAIL"
|
|
1261
|
+
raw_limits = payload.get("button_limit", payload.get("buttonLimit"))
|
|
1262
|
+
if isinstance(raw_limits, list) and raw_limits and all(isinstance(item, dict) for item in raw_limits):
|
|
1263
|
+
payload["button_limit"] = [raw_limits]
|
|
1264
|
+
return payload
|
|
1265
|
+
|
|
1266
|
+
@model_validator(mode="after")
|
|
1267
|
+
def validate_shape(self) -> "ViewButtonBindingPatch":
|
|
1268
|
+
if self.button_type == PublicViewButtonType.system:
|
|
1269
|
+
missing = [
|
|
1270
|
+
field_name
|
|
1271
|
+
for field_name, value in (
|
|
1272
|
+
("button_icon", self.button_icon),
|
|
1273
|
+
("background_color", self.background_color),
|
|
1274
|
+
("text_color", self.text_color),
|
|
1275
|
+
("trigger_action", self.trigger_action),
|
|
1276
|
+
)
|
|
1277
|
+
if not str(value or "").strip()
|
|
1278
|
+
]
|
|
1279
|
+
if missing:
|
|
1280
|
+
raise ValueError(f"system button bindings require {', '.join(missing)}")
|
|
1281
|
+
if _is_white_button_color(self.background_color) and _is_white_button_color(self.text_color):
|
|
1282
|
+
raise ValueError("background_color and text_color cannot both be white")
|
|
1283
|
+
return self
|
|
1284
|
+
|
|
1285
|
+
|
|
1286
|
+
class ChartFilterRulePatch(StrictModel):
|
|
1287
|
+
field_name: str = Field(validation_alias=AliasChoices("field_name", "fieldName", "field", "name"))
|
|
1288
|
+
operator: ViewFilterOperator = Field(validation_alias=AliasChoices("operator", "op"))
|
|
1289
|
+
values: list[Any] = Field(default_factory=list)
|
|
1290
|
+
|
|
1291
|
+
@model_validator(mode="before")
|
|
1292
|
+
@classmethod
|
|
1293
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
1294
|
+
if not isinstance(value, dict):
|
|
1295
|
+
return value
|
|
1296
|
+
payload = dict(value)
|
|
1297
|
+
if "value" in payload and "values" not in payload:
|
|
1298
|
+
raw_value = payload.pop("value")
|
|
1299
|
+
payload["values"] = list(raw_value) if isinstance(raw_value, list) else [raw_value]
|
|
1300
|
+
raw_operator = payload.get("operator", payload.get("op"))
|
|
1301
|
+
if isinstance(raw_operator, str):
|
|
1302
|
+
normalized = raw_operator.strip().lower()
|
|
1303
|
+
operator_aliases = {
|
|
1304
|
+
"equals": ViewFilterOperator.eq.value,
|
|
1305
|
+
"equal": ViewFilterOperator.eq.value,
|
|
1306
|
+
"=": ViewFilterOperator.eq.value,
|
|
1307
|
+
"not_equals": ViewFilterOperator.neq.value,
|
|
1308
|
+
"not_equal": ViewFilterOperator.neq.value,
|
|
1309
|
+
"!=": ViewFilterOperator.neq.value,
|
|
1310
|
+
">=": ViewFilterOperator.gte.value,
|
|
1311
|
+
"<=": ViewFilterOperator.lte.value,
|
|
1312
|
+
"any_of": ViewFilterOperator.in_.value,
|
|
1313
|
+
"one_of": ViewFilterOperator.in_.value,
|
|
1314
|
+
"between_any": ViewFilterOperator.in_.value,
|
|
1315
|
+
"empty": ViewFilterOperator.is_empty.value,
|
|
1316
|
+
"is blank": ViewFilterOperator.is_empty.value,
|
|
1317
|
+
"blank": ViewFilterOperator.is_empty.value,
|
|
1318
|
+
"not_empty": ViewFilterOperator.not_empty.value,
|
|
1319
|
+
"not blank": ViewFilterOperator.not_empty.value,
|
|
1320
|
+
}
|
|
1321
|
+
if normalized in operator_aliases:
|
|
1322
|
+
payload["operator"] = operator_aliases[normalized]
|
|
1323
|
+
elif "operator" not in payload:
|
|
1324
|
+
payload["operator"] = normalized
|
|
1325
|
+
payload.pop("op", None)
|
|
1326
|
+
return payload
|
|
1327
|
+
|
|
1328
|
+
@model_validator(mode="after")
|
|
1329
|
+
def validate_shape(self) -> "ChartFilterRulePatch":
|
|
1330
|
+
if self.operator in {ViewFilterOperator.is_empty, ViewFilterOperator.not_empty}:
|
|
1331
|
+
self.values = []
|
|
1332
|
+
return self
|
|
1333
|
+
if not self.values:
|
|
1334
|
+
raise ValueError("chart filter rule requires values")
|
|
1335
|
+
return self
|
|
1336
|
+
|
|
1337
|
+
|
|
1338
|
+
class ChartUpsertPatch(StrictModel):
|
|
1339
|
+
chart_id: str | None = None
|
|
1340
|
+
name: str
|
|
1341
|
+
chart_type: PublicChartType
|
|
1342
|
+
dimension_field_ids: list[str] = Field(default_factory=list)
|
|
1343
|
+
indicator_field_ids: list[str] = Field(default_factory=list)
|
|
1344
|
+
filters: list[ChartFilterRulePatch] = Field(default_factory=list)
|
|
1345
|
+
question_config: list[dict[str, Any]] = Field(default_factory=list)
|
|
1346
|
+
user_config: list[dict[str, Any]] = Field(default_factory=list)
|
|
1347
|
+
config: dict[str, Any] = Field(default_factory=dict)
|
|
1348
|
+
visibility: VisibilityPatch | None = None
|
|
1349
|
+
|
|
1350
|
+
@model_validator(mode="before")
|
|
1351
|
+
@classmethod
|
|
1352
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
1353
|
+
if not isinstance(value, dict):
|
|
1354
|
+
return value
|
|
1355
|
+
payload = dict(value)
|
|
1356
|
+
if "id" in payload and "chart_id" not in payload:
|
|
1357
|
+
payload["chart_id"] = payload.pop("id")
|
|
1358
|
+
if "type" in payload and "chart_type" not in payload:
|
|
1359
|
+
payload["chart_type"] = payload.pop("type")
|
|
1360
|
+
if "dimension_fields" in payload and "dimension_field_ids" not in payload:
|
|
1361
|
+
payload["dimension_field_ids"] = payload.pop("dimension_fields")
|
|
1362
|
+
if "indicator_fields" in payload and "indicator_field_ids" not in payload:
|
|
1363
|
+
payload["indicator_field_ids"] = payload.pop("indicator_fields")
|
|
1364
|
+
if "metric_field_ids" in payload and "indicator_field_ids" not in payload:
|
|
1365
|
+
payload["indicator_field_ids"] = payload.pop("metric_field_ids")
|
|
1366
|
+
raw_type = payload.get("chart_type")
|
|
1367
|
+
if isinstance(raw_type, str):
|
|
1368
|
+
normalized = raw_type.strip().lower()
|
|
1369
|
+
aliases = {
|
|
1370
|
+
"targetchart": PublicChartType.target.value,
|
|
1371
|
+
"piechart": PublicChartType.pie.value,
|
|
1372
|
+
"barchart": PublicChartType.bar.value,
|
|
1373
|
+
"linechart": PublicChartType.line.value,
|
|
1374
|
+
"tablechart": PublicChartType.table.value,
|
|
1375
|
+
}
|
|
1376
|
+
if normalized in aliases:
|
|
1377
|
+
payload["chart_type"] = aliases[normalized]
|
|
1378
|
+
if isinstance(payload.get("chart_id"), int):
|
|
1379
|
+
payload["chart_id"] = str(payload["chart_id"])
|
|
1380
|
+
if isinstance(payload.get("dimension_field_ids"), list):
|
|
1381
|
+
payload["dimension_field_ids"] = [str(item) for item in payload["dimension_field_ids"] if item is not None and str(item).strip()]
|
|
1382
|
+
if isinstance(payload.get("indicator_field_ids"), list):
|
|
1383
|
+
payload["indicator_field_ids"] = [str(item) for item in payload["indicator_field_ids"] if item is not None and str(item).strip()]
|
|
1384
|
+
return payload
|
|
1385
|
+
|
|
1386
|
+
|
|
1387
|
+
class ChartApplyRequest(StrictModel):
|
|
1388
|
+
app_key: str
|
|
1389
|
+
upsert_charts: list[ChartUpsertPatch] = Field(default_factory=list)
|
|
1390
|
+
remove_chart_ids: list[str] = Field(default_factory=list)
|
|
1391
|
+
reorder_chart_ids: list[str] = Field(default_factory=list)
|
|
1392
|
+
|
|
1393
|
+
@model_validator(mode="before")
|
|
1394
|
+
@classmethod
|
|
1395
|
+
def normalize_ids(cls, value: Any) -> Any:
|
|
1396
|
+
if not isinstance(value, dict):
|
|
1397
|
+
return value
|
|
1398
|
+
payload = dict(value)
|
|
1399
|
+
for key in ("remove_chart_ids", "reorder_chart_ids"):
|
|
1400
|
+
raw = payload.get(key)
|
|
1401
|
+
if isinstance(raw, list):
|
|
1402
|
+
payload[key] = [str(item) for item in raw if item is not None and str(item).strip()]
|
|
1403
|
+
return payload
|
|
1404
|
+
|
|
1405
|
+
@model_validator(mode="after")
|
|
1406
|
+
def validate_shape(self) -> "ChartApplyRequest":
|
|
1407
|
+
if not self.upsert_charts and not self.remove_chart_ids and not self.reorder_chart_ids:
|
|
1408
|
+
raise ValueError("chart apply requires at least one upsert, remove, or reorder operation")
|
|
1409
|
+
return self
|
|
1410
|
+
|
|
1411
|
+
|
|
1412
|
+
class PortalComponentPositionPatch(StrictModel):
|
|
1413
|
+
pc_x: int = Field(default=0, validation_alias=AliasChoices("pc_x", "pcX", "x"))
|
|
1414
|
+
pc_y: int = Field(default=0, validation_alias=AliasChoices("pc_y", "pcY", "y"))
|
|
1415
|
+
pc_w: int = Field(default=12, validation_alias=AliasChoices("pc_w", "pcW", "w"))
|
|
1416
|
+
pc_h: int = Field(default=8, validation_alias=AliasChoices("pc_h", "pcH", "h"))
|
|
1417
|
+
mobile_x: int = Field(default=0, validation_alias=AliasChoices("mobile_x", "mobileX"))
|
|
1418
|
+
mobile_y: int = Field(default=0, validation_alias=AliasChoices("mobile_y", "mobileY"))
|
|
1419
|
+
mobile_w: int = Field(default=12, validation_alias=AliasChoices("mobile_w", "mobileW"))
|
|
1420
|
+
mobile_h: int = Field(default=8, validation_alias=AliasChoices("mobile_h", "mobileH"))
|
|
1421
|
+
|
|
1422
|
+
@model_validator(mode="before")
|
|
1423
|
+
@classmethod
|
|
1424
|
+
def normalize_nested_layout(cls, value: Any) -> Any:
|
|
1425
|
+
if not isinstance(value, dict):
|
|
1426
|
+
return value
|
|
1427
|
+
payload = dict(value)
|
|
1428
|
+
pc = payload.pop("pc", None)
|
|
1429
|
+
mobile = payload.pop("mobile", None)
|
|
1430
|
+
if isinstance(pc, dict):
|
|
1431
|
+
if "pc_x" not in payload and "x" in pc:
|
|
1432
|
+
payload["pc_x"] = pc.get("x")
|
|
1433
|
+
if "pc_y" not in payload and "y" in pc:
|
|
1434
|
+
payload["pc_y"] = pc.get("y")
|
|
1435
|
+
if "pc_w" not in payload and "cols" in pc:
|
|
1436
|
+
payload["pc_w"] = pc.get("cols")
|
|
1437
|
+
if "pc_h" not in payload and "rows" in pc:
|
|
1438
|
+
payload["pc_h"] = pc.get("rows")
|
|
1439
|
+
if isinstance(mobile, dict):
|
|
1440
|
+
if "mobile_x" not in payload and "x" in mobile:
|
|
1441
|
+
payload["mobile_x"] = mobile.get("x")
|
|
1442
|
+
if "mobile_y" not in payload and "y" in mobile:
|
|
1443
|
+
payload["mobile_y"] = mobile.get("y")
|
|
1444
|
+
if "mobile_w" not in payload and "cols" in mobile:
|
|
1445
|
+
payload["mobile_w"] = mobile.get("cols")
|
|
1446
|
+
if "mobile_h" not in payload and "rows" in mobile:
|
|
1447
|
+
payload["mobile_h"] = mobile.get("rows")
|
|
1448
|
+
return payload
|
|
1449
|
+
|
|
1450
|
+
|
|
1451
|
+
class PortalChartRefPatch(StrictModel):
|
|
1452
|
+
app_key: str
|
|
1453
|
+
chart_id: str | None = None
|
|
1454
|
+
chart_name: str | None = None
|
|
1455
|
+
|
|
1456
|
+
@model_validator(mode="after")
|
|
1457
|
+
def validate_target(self) -> "PortalChartRefPatch":
|
|
1458
|
+
if not (self.chart_id or self.chart_name):
|
|
1459
|
+
raise ValueError("chart_ref requires chart_id or chart_name")
|
|
1460
|
+
return self
|
|
1461
|
+
|
|
1462
|
+
|
|
1463
|
+
class PortalViewRefPatch(StrictModel):
|
|
1464
|
+
app_key: str
|
|
1465
|
+
view_key: str | None = None
|
|
1466
|
+
view_name: str | None = None
|
|
1467
|
+
|
|
1468
|
+
@model_validator(mode="after")
|
|
1469
|
+
def validate_target(self) -> "PortalViewRefPatch":
|
|
1470
|
+
if not (self.view_key or self.view_name):
|
|
1471
|
+
raise ValueError("view_ref requires view_key or view_name")
|
|
1472
|
+
return self
|
|
1473
|
+
|
|
1474
|
+
|
|
1475
|
+
class PortalSectionPatch(StrictModel):
|
|
1476
|
+
title: str
|
|
1477
|
+
source_type: str = Field(validation_alias=AliasChoices("source_type", "sourceType"))
|
|
1478
|
+
position: PortalComponentPositionPatch | None = None
|
|
1479
|
+
dash_style_config: dict[str, Any] | None = Field(default=None, validation_alias=AliasChoices("dash_style_config", "dashStyleConfigBO"))
|
|
1480
|
+
config: dict[str, Any] = Field(default_factory=dict)
|
|
1481
|
+
chart_ref: PortalChartRefPatch | None = None
|
|
1482
|
+
view_ref: PortalViewRefPatch | None = None
|
|
1483
|
+
text: str | None = None
|
|
1484
|
+
url: str | None = None
|
|
1485
|
+
|
|
1486
|
+
@model_validator(mode="before")
|
|
1487
|
+
@classmethod
|
|
1488
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
1489
|
+
if not isinstance(value, dict):
|
|
1490
|
+
return value
|
|
1491
|
+
payload = dict(value)
|
|
1492
|
+
raw_type = payload.get("source_type", payload.get("sourceType"))
|
|
1493
|
+
if isinstance(raw_type, str):
|
|
1494
|
+
payload["source_type"] = raw_type.strip().lower()
|
|
1495
|
+
if "chartRef" in payload and "chart_ref" not in payload:
|
|
1496
|
+
payload["chart_ref"] = payload.pop("chartRef")
|
|
1497
|
+
if "viewRef" in payload and "view_ref" not in payload:
|
|
1498
|
+
payload["view_ref"] = payload.pop("viewRef")
|
|
1499
|
+
if "dashStyleConfigBO" in payload and "dash_style_config" not in payload:
|
|
1500
|
+
payload["dash_style_config"] = payload.pop("dashStyleConfigBO")
|
|
1501
|
+
return payload
|
|
1502
|
+
|
|
1503
|
+
@model_validator(mode="after")
|
|
1504
|
+
def validate_shape(self) -> "PortalSectionPatch":
|
|
1505
|
+
supported = {"chart", "view", "grid", "filter", "text", "link"}
|
|
1506
|
+
if self.source_type not in supported:
|
|
1507
|
+
raise ValueError(f"unsupported portal source_type '{self.source_type}'")
|
|
1508
|
+
if self.source_type == "chart" and self.chart_ref is None:
|
|
1509
|
+
raise ValueError("chart section requires chart_ref")
|
|
1510
|
+
if self.source_type == "view" and self.view_ref is None:
|
|
1511
|
+
raise ValueError("view section requires view_ref")
|
|
1512
|
+
if self.source_type == "text" and self.text is None:
|
|
1513
|
+
raise ValueError("text section requires text")
|
|
1514
|
+
if self.source_type == "link" and self.url is None:
|
|
1515
|
+
raise ValueError("link section requires url")
|
|
1516
|
+
return self
|
|
1517
|
+
|
|
1518
|
+
|
|
1519
|
+
class PortalApplyRequest(StrictModel):
|
|
1520
|
+
dash_key: str | None = None
|
|
1521
|
+
dash_name: str | None = None
|
|
1522
|
+
package_tag_id: int | None = None
|
|
1523
|
+
publish: bool = True
|
|
1524
|
+
sections: list[PortalSectionPatch] = Field(default_factory=list)
|
|
1525
|
+
visibility: VisibilityPatch | None = None
|
|
1526
|
+
auth: dict[str, Any] | None = None
|
|
1527
|
+
icon: str | None = None
|
|
1528
|
+
color: str | None = None
|
|
1529
|
+
hide_copyright: bool | None = Field(default=None, validation_alias=AliasChoices("hide_copyright", "hideCopyright"))
|
|
1530
|
+
dash_global_config: dict[str, Any] | None = Field(default=None, validation_alias=AliasChoices("dash_global_config", "dashGlobalConfig"))
|
|
1531
|
+
config: dict[str, Any] = Field(default_factory=dict)
|
|
1532
|
+
|
|
1533
|
+
@model_validator(mode="after")
|
|
1534
|
+
def validate_shape(self) -> "PortalApplyRequest":
|
|
1535
|
+
if not self.dash_key and not self.package_tag_id:
|
|
1536
|
+
raise ValueError("package_tag_id is required when dash_key is empty")
|
|
1537
|
+
if not self.dash_key and not self.dash_name:
|
|
1538
|
+
raise ValueError("dash_name is required when creating a portal")
|
|
1539
|
+
if not self.dash_key and not self.sections:
|
|
1540
|
+
raise ValueError("portal apply requires a non-empty sections list when creating a portal")
|
|
1541
|
+
if self.visibility is not None and self.auth is not None:
|
|
1542
|
+
raise ValueError("visibility and auth cannot be provided together")
|
|
1543
|
+
return self
|
|
1544
|
+
|
|
1545
|
+
|
|
1546
|
+
FieldPatch.model_rebuild()
|
|
1547
|
+
FieldMutation.model_rebuild()
|
|
1548
|
+
FieldUpdatePatch.model_rebuild()
|
|
1549
|
+
|
|
1550
|
+
|
|
1551
|
+
class AppGetResponse(StrictModel):
|
|
1552
|
+
app_key: str
|
|
1553
|
+
title: str | None = None
|
|
1554
|
+
app_icon: str | None = None
|
|
1555
|
+
visibility: dict[str, Any] = Field(default_factory=dict)
|
|
1556
|
+
tag_ids: list[int] = Field(default_factory=list)
|
|
1557
|
+
publish_status: int | None = None
|
|
1558
|
+
field_count: int = 0
|
|
1559
|
+
layout_section_count: int = 0
|
|
1560
|
+
view_count: int = 0
|
|
1561
|
+
workflow_enabled: bool = False
|
|
1562
|
+
verification_hints: list[str] = Field(default_factory=list)
|
|
1563
|
+
editability: dict[str, bool | None] = Field(default_factory=dict)
|
|
1564
|
+
|
|
1565
|
+
|
|
1566
|
+
class AppGetFieldsResponse(StrictModel):
|
|
1567
|
+
app_key: str
|
|
1568
|
+
fields: list[dict[str, Any]] = Field(default_factory=list)
|
|
1569
|
+
field_count: int = 0
|
|
1570
|
+
|
|
1571
|
+
|
|
1572
|
+
class AppGetLayoutResponse(StrictModel):
|
|
1573
|
+
app_key: str
|
|
1574
|
+
sections: list[dict[str, Any]] = Field(default_factory=list)
|
|
1575
|
+
unplaced_fields: list[str] = Field(default_factory=list)
|
|
1576
|
+
layout_mode_detected: str = "empty"
|
|
1577
|
+
|
|
1578
|
+
|
|
1579
|
+
class AppGetViewsResponse(StrictModel):
|
|
1580
|
+
app_key: str
|
|
1581
|
+
views: list[dict[str, Any]] = Field(default_factory=list)
|
|
1582
|
+
|
|
1583
|
+
|
|
1584
|
+
class AppGetFlowResponse(StrictModel):
|
|
1585
|
+
app_key: str
|
|
1586
|
+
enabled: bool = False
|
|
1587
|
+
nodes: list[dict[str, Any]] = Field(default_factory=list)
|
|
1588
|
+
transitions: list[dict[str, Any]] = Field(default_factory=list)
|
|
1589
|
+
|
|
1590
|
+
|
|
1591
|
+
class AppGetChartsResponse(StrictModel):
|
|
1592
|
+
app_key: str
|
|
1593
|
+
charts: list[dict[str, Any]] = Field(default_factory=list)
|
|
1594
|
+
chart_count: int = 0
|
|
1595
|
+
|
|
1596
|
+
|
|
1597
|
+
AppReadSummaryResponse = AppGetResponse
|
|
1598
|
+
AppFieldsReadResponse = AppGetFieldsResponse
|
|
1599
|
+
AppLayoutReadResponse = AppGetLayoutResponse
|
|
1600
|
+
AppViewsReadResponse = AppGetViewsResponse
|
|
1601
|
+
AppFlowReadResponse = AppGetFlowResponse
|
|
1602
|
+
AppChartsReadResponse = AppGetChartsResponse
|
|
1603
|
+
|
|
1604
|
+
|
|
1605
|
+
class PortalListResponse(StrictModel):
|
|
1606
|
+
items: list[dict[str, Any]] = Field(default_factory=list)
|
|
1607
|
+
total: int = 0
|
|
1608
|
+
|
|
1609
|
+
|
|
1610
|
+
class PortalReadSummaryResponse(StrictModel):
|
|
1611
|
+
dash_key: str
|
|
1612
|
+
being_draft: bool = True
|
|
1613
|
+
dash_name: str | None = None
|
|
1614
|
+
package_tag_ids: list[int] = Field(default_factory=list)
|
|
1615
|
+
dash_icon: str | None = None
|
|
1616
|
+
hide_copyright: bool | None = None
|
|
1617
|
+
config_keys: list[str] = Field(default_factory=list)
|
|
1618
|
+
dash_global_config_keys: list[str] = Field(default_factory=list)
|
|
1619
|
+
section_count: int = 0
|
|
1620
|
+
sections: list[dict[str, Any]] = Field(default_factory=list)
|
|
1621
|
+
|
|
1622
|
+
|
|
1623
|
+
class PortalGetResponse(StrictModel):
|
|
1624
|
+
dash_key: str
|
|
1625
|
+
being_draft: bool = True
|
|
1626
|
+
dash_name: str | None = None
|
|
1627
|
+
package_tag_ids: list[int] = Field(default_factory=list)
|
|
1628
|
+
dash_icon: str | None = None
|
|
1629
|
+
hide_copyright: bool | None = None
|
|
1630
|
+
visibility: dict[str, Any] = Field(default_factory=dict)
|
|
1631
|
+
auth: dict[str, Any] = Field(default_factory=dict)
|
|
1632
|
+
config: dict[str, Any] = Field(default_factory=dict)
|
|
1633
|
+
dash_global_config: dict[str, Any] = Field(default_factory=dict)
|
|
1634
|
+
component_count: int = 0
|
|
1635
|
+
components: list[dict[str, Any]] = Field(default_factory=list)
|
|
1636
|
+
|
|
1637
|
+
|
|
1638
|
+
class ViewGetResponse(StrictModel):
|
|
1639
|
+
view_key: str
|
|
1640
|
+
base_info: dict[str, Any] = Field(default_factory=dict)
|
|
1641
|
+
visibility: dict[str, Any] = Field(default_factory=dict)
|
|
1642
|
+
config: dict[str, Any] = Field(default_factory=dict)
|
|
1643
|
+
questions: list[dict[str, Any]] = Field(default_factory=list)
|
|
1644
|
+
associations: list[dict[str, Any]] = Field(default_factory=list)
|
|
1645
|
+
|
|
1646
|
+
|
|
1647
|
+
class ChartGetResponse(StrictModel):
|
|
1648
|
+
chart_id: str
|
|
1649
|
+
base: dict[str, Any] = Field(default_factory=dict)
|
|
1650
|
+
visibility: dict[str, Any] = Field(default_factory=dict)
|
|
1651
|
+
config: dict[str, Any] = Field(default_factory=dict)
|
|
1652
|
+
|
|
1653
|
+
|
|
1654
|
+
class SchemaPlanRequest(StrictModel):
|
|
1655
|
+
app_key: str = ""
|
|
1656
|
+
package_tag_id: int | None = None
|
|
1657
|
+
app_name: str = Field(default="", validation_alias=AliasChoices("app_name", "app_title", "title"))
|
|
1658
|
+
icon: str | None = None
|
|
1659
|
+
color: str | None = None
|
|
1660
|
+
visibility: VisibilityPatch | None = None
|
|
1661
|
+
create_if_missing: bool = False
|
|
1662
|
+
add_fields: list[FieldPatch] = Field(default_factory=list)
|
|
1663
|
+
update_fields: list[FieldUpdatePatch] = Field(default_factory=list)
|
|
1664
|
+
remove_fields: list[FieldRemovePatch] = Field(default_factory=list)
|
|
1665
|
+
|
|
1666
|
+
|
|
1667
|
+
class LayoutPlanRequest(StrictModel):
|
|
1668
|
+
app_key: str
|
|
1669
|
+
mode: LayoutApplyMode = LayoutApplyMode.merge
|
|
1670
|
+
sections: list[LayoutSectionPatch] = Field(default_factory=list)
|
|
1671
|
+
preset: LayoutPreset | None = None
|
|
1672
|
+
|
|
1673
|
+
@model_validator(mode="before")
|
|
1674
|
+
@classmethod
|
|
1675
|
+
def normalize_mode_alias(cls, value: Any) -> Any:
|
|
1676
|
+
if not isinstance(value, dict):
|
|
1677
|
+
return value
|
|
1678
|
+
payload = dict(value)
|
|
1679
|
+
if str(payload.get("mode") or "").strip().lower() == "overwrite":
|
|
1680
|
+
payload["mode"] = "replace"
|
|
1681
|
+
return payload
|
|
1682
|
+
|
|
1683
|
+
|
|
1684
|
+
class FlowPlanRequest(StrictModel):
|
|
1685
|
+
app_key: str
|
|
1686
|
+
mode: str = "replace"
|
|
1687
|
+
nodes: list[FlowNodePatch] = Field(default_factory=list)
|
|
1688
|
+
transitions: list[FlowTransitionPatch] = Field(default_factory=list)
|
|
1689
|
+
preset: FlowPreset | None = None
|
|
1690
|
+
|
|
1691
|
+
@model_validator(mode="before")
|
|
1692
|
+
@classmethod
|
|
1693
|
+
def normalize_mode_alias(cls, value: Any) -> Any:
|
|
1694
|
+
if not isinstance(value, dict):
|
|
1695
|
+
return value
|
|
1696
|
+
payload = dict(value)
|
|
1697
|
+
if str(payload.get("mode") or "").strip().lower() == "overwrite":
|
|
1698
|
+
payload["mode"] = "replace"
|
|
1699
|
+
raw_preset = payload.get("preset")
|
|
1700
|
+
if raw_preset is None and isinstance(payload.get("base_preset"), str):
|
|
1701
|
+
raw_preset = payload["base_preset"]
|
|
1702
|
+
payload["preset"] = raw_preset
|
|
1703
|
+
if isinstance(raw_preset, str):
|
|
1704
|
+
normalized_preset = raw_preset.strip().lower()
|
|
1705
|
+
preset_aliases = {
|
|
1706
|
+
"default_approval": FlowPreset.basic_approval.value,
|
|
1707
|
+
"approval": FlowPreset.basic_approval.value,
|
|
1708
|
+
"basic approval": FlowPreset.basic_approval.value,
|
|
1709
|
+
"default_fill_then_approve": FlowPreset.basic_fill_then_approve.value,
|
|
1710
|
+
"default-fill-then-approve": FlowPreset.basic_fill_then_approve.value,
|
|
1711
|
+
"fill_then_approve": FlowPreset.basic_fill_then_approve.value,
|
|
1712
|
+
}
|
|
1713
|
+
if normalized_preset in preset_aliases:
|
|
1714
|
+
payload["preset"] = preset_aliases[normalized_preset]
|
|
1715
|
+
return payload
|
|
1716
|
+
|
|
1717
|
+
|
|
1718
|
+
class ViewsPlanRequest(StrictModel):
|
|
1719
|
+
app_key: str
|
|
1720
|
+
upsert_views: list[ViewUpsertPatch] = Field(default_factory=list)
|
|
1721
|
+
remove_views: list[str] = Field(default_factory=list)
|
|
1722
|
+
preset: ViewsPreset | None = None
|
|
1723
|
+
|
|
1724
|
+
|
|
1725
|
+
class OperationResultEnvelope(StrictModel):
|
|
1726
|
+
status: str
|
|
1727
|
+
error_code: str | None = None
|
|
1728
|
+
recoverable: bool = False
|
|
1729
|
+
message: str
|
|
1730
|
+
normalized_args: dict[str, Any] = Field(default_factory=dict)
|
|
1731
|
+
missing_fields: list[str] = Field(default_factory=list)
|
|
1732
|
+
allowed_values: dict[str, Any] = Field(default_factory=dict)
|
|
1733
|
+
details: dict[str, Any] = Field(default_factory=dict)
|
|
1734
|
+
request_id: str | None = None
|
|
1735
|
+
suggested_next_call: dict[str, Any] | None = None
|
|
1736
|
+
noop: bool = False
|
|
1737
|
+
verification: dict[str, Any] = Field(default_factory=dict)
|
|
1738
|
+
|
|
1739
|
+
|
|
1740
|
+
def _normalize_field_payload(value: Any) -> Any:
|
|
1741
|
+
if not isinstance(value, dict):
|
|
1742
|
+
return value
|
|
1743
|
+
payload = dict(value)
|
|
1744
|
+
if "fields" in payload and "subfields" not in payload:
|
|
1745
|
+
payload["subfields"] = payload.pop("fields")
|
|
1746
|
+
raw_type = payload.get("type")
|
|
1747
|
+
if isinstance(raw_type, int):
|
|
1748
|
+
normalized_from_id = FIELD_TYPE_ID_ALIASES.get(raw_type)
|
|
1749
|
+
if normalized_from_id is not None:
|
|
1750
|
+
payload["type"] = normalized_from_id.value
|
|
1751
|
+
if isinstance(raw_type, str):
|
|
1752
|
+
normalized = FIELD_TYPE_ALIASES.get(raw_type.strip().lower())
|
|
1753
|
+
if normalized is not None:
|
|
1754
|
+
payload["type"] = normalized.value
|
|
1755
|
+
normalized_relation_mode = _normalize_public_relation_mode(
|
|
1756
|
+
payload.get("relation_mode", payload.get("relationMode", payload.get("selection_mode", payload.get("selectionMode"))))
|
|
1757
|
+
)
|
|
1758
|
+
if normalized_relation_mode is None:
|
|
1759
|
+
for alias_key in ("optional_data_num", "optionalDataNum", "multiple", "allow_multiple"):
|
|
1760
|
+
if alias_key in payload:
|
|
1761
|
+
normalized_relation_mode = _normalize_public_relation_mode(payload.get(alias_key))
|
|
1762
|
+
break
|
|
1763
|
+
if normalized_relation_mode is not None:
|
|
1764
|
+
payload["relation_mode"] = normalized_relation_mode
|
|
1765
|
+
for alias_key in (
|
|
1766
|
+
"relationMode",
|
|
1767
|
+
"selection_mode",
|
|
1768
|
+
"selectionMode",
|
|
1769
|
+
"optional_data_num",
|
|
1770
|
+
"optionalDataNum",
|
|
1771
|
+
"multiple",
|
|
1772
|
+
"allow_multiple",
|
|
1773
|
+
):
|
|
1774
|
+
payload.pop(alias_key, None)
|
|
1775
|
+
return payload
|
|
1776
|
+
|
|
1777
|
+
|
|
1778
|
+
def _slugify_title(title: str) -> str:
|
|
1779
|
+
normalized = "".join(ch.lower() if ch.isalnum() else "_" for ch in str(title or ""))
|
|
1780
|
+
collapsed = "_".join(part for part in normalized.split("_") if part)
|
|
1781
|
+
return collapsed or "section"
|
|
1782
|
+
|
|
1783
|
+
|
|
1784
|
+
def _normalize_public_relation_mode(value: Any) -> str | None:
|
|
1785
|
+
if value is None:
|
|
1786
|
+
return None
|
|
1787
|
+
if isinstance(value, bool):
|
|
1788
|
+
return PublicRelationMode.multiple.value if value else PublicRelationMode.single.value
|
|
1789
|
+
if isinstance(value, int):
|
|
1790
|
+
if value == 0:
|
|
1791
|
+
return PublicRelationMode.multiple.value
|
|
1792
|
+
if value == 1:
|
|
1793
|
+
return PublicRelationMode.single.value
|
|
1794
|
+
return None
|
|
1795
|
+
if isinstance(value, str):
|
|
1796
|
+
normalized = value.strip().lower()
|
|
1797
|
+
aliases = {
|
|
1798
|
+
"single": PublicRelationMode.single.value,
|
|
1799
|
+
"single_select": PublicRelationMode.single.value,
|
|
1800
|
+
"single-select": PublicRelationMode.single.value,
|
|
1801
|
+
"one": PublicRelationMode.single.value,
|
|
1802
|
+
"1": PublicRelationMode.single.value,
|
|
1803
|
+
"multiple": PublicRelationMode.multiple.value,
|
|
1804
|
+
"multi": PublicRelationMode.multiple.value,
|
|
1805
|
+
"multi_select": PublicRelationMode.multiple.value,
|
|
1806
|
+
"multi-select": PublicRelationMode.multiple.value,
|
|
1807
|
+
"many": PublicRelationMode.multiple.value,
|
|
1808
|
+
"0": PublicRelationMode.multiple.value,
|
|
1809
|
+
}
|
|
1810
|
+
return aliases.get(normalized, normalized or None)
|
|
1811
|
+
return None
|
|
1812
|
+
|
|
1813
|
+
|
|
1814
|
+
def _normalize_public_department_scope_mode(value: Any) -> str | None:
|
|
1815
|
+
if value is None:
|
|
1816
|
+
return None
|
|
1817
|
+
if isinstance(value, int):
|
|
1818
|
+
if value == 1:
|
|
1819
|
+
return PublicDepartmentScopeMode.all.value
|
|
1820
|
+
if value == 2:
|
|
1821
|
+
return PublicDepartmentScopeMode.custom.value
|
|
1822
|
+
return None
|
|
1823
|
+
if isinstance(value, str):
|
|
1824
|
+
normalized = value.strip().lower()
|
|
1825
|
+
aliases = {
|
|
1826
|
+
"all": PublicDepartmentScopeMode.all.value,
|
|
1827
|
+
"workspace_all": PublicDepartmentScopeMode.all.value,
|
|
1828
|
+
"workspace-all": PublicDepartmentScopeMode.all.value,
|
|
1829
|
+
"default": PublicDepartmentScopeMode.all.value,
|
|
1830
|
+
"default_all": PublicDepartmentScopeMode.all.value,
|
|
1831
|
+
"default-all": PublicDepartmentScopeMode.all.value,
|
|
1832
|
+
"1": PublicDepartmentScopeMode.all.value,
|
|
1833
|
+
"custom": PublicDepartmentScopeMode.custom.value,
|
|
1834
|
+
"explicit": PublicDepartmentScopeMode.custom.value,
|
|
1835
|
+
"selected": PublicDepartmentScopeMode.custom.value,
|
|
1836
|
+
"2": PublicDepartmentScopeMode.custom.value,
|
|
1837
|
+
}
|
|
1838
|
+
return aliases.get(normalized, normalized or None)
|
|
1839
|
+
return None
|
|
1840
|
+
|
|
1841
|
+
|
|
1842
|
+
CustomButtonMatchRulePatch.model_rebuild()
|
|
1843
|
+
CustomButtonAddDataConfigPatch.model_rebuild()
|
|
1844
|
+
CodeBlockAliasPathPatch.model_rebuild()
|
|
1845
|
+
ViewButtonBindingPatch.model_rebuild()
|
|
1846
|
+
ViewUpsertPatch.model_rebuild()
|