@josephyan/qingflow-cli 0.2.0-beta.55
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 +30 -0
- package/docs/local-agent-install.md +235 -0
- package/entry_point.py +13 -0
- package/npm/bin/qingflow.mjs +5 -0
- package/npm/lib/runtime.mjs +204 -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 +5 -0
- package/src/qingflow_mcp/__main__.py +5 -0
- package/src/qingflow_mcp/backend_client.py +547 -0
- package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
- package/src/qingflow_mcp/builder_facade/models.py +985 -0
- package/src/qingflow_mcp/builder_facade/service.py +8243 -0
- package/src/qingflow_mcp/cli/__init__.py +1 -0
- package/src/qingflow_mcp/cli/commands/__init__.py +15 -0
- package/src/qingflow_mcp/cli/commands/app.py +40 -0
- package/src/qingflow_mcp/cli/commands/auth.py +78 -0
- package/src/qingflow_mcp/cli/commands/builder.py +184 -0
- package/src/qingflow_mcp/cli/commands/common.py +47 -0
- package/src/qingflow_mcp/cli/commands/imports.py +86 -0
- package/src/qingflow_mcp/cli/commands/record.py +202 -0
- package/src/qingflow_mcp/cli/commands/task.py +87 -0
- package/src/qingflow_mcp/cli/commands/workspace.py +33 -0
- package/src/qingflow_mcp/cli/context.py +48 -0
- package/src/qingflow_mcp/cli/formatters.py +269 -0
- package/src/qingflow_mcp/cli/json_io.py +50 -0
- package/src/qingflow_mcp/cli/main.py +147 -0
- package/src/qingflow_mcp/config.py +221 -0
- package/src/qingflow_mcp/errors.py +66 -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/server.py +211 -0
- package/src/qingflow_mcp/server_app_builder.py +387 -0
- package/src/qingflow_mcp/server_app_user.py +317 -0
- package/src/qingflow_mcp/session_store.py +289 -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 +466 -0
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +113 -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 +2339 -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 +853 -0
- package/src/qingflow_mcp/tools/__init__.py +1 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +2063 -0
- package/src/qingflow_mcp/tools/app_tools.py +850 -0
- package/src/qingflow_mcp/tools/approval_tools.py +833 -0
- package/src/qingflow_mcp/tools/auth_tools.py +697 -0
- package/src/qingflow_mcp/tools/base.py +81 -0
- package/src/qingflow_mcp/tools/code_block_tools.py +679 -0
- package/src/qingflow_mcp/tools/directory_tools.py +648 -0
- package/src/qingflow_mcp/tools/feedback_tools.py +230 -0
- package/src/qingflow_mcp/tools/file_tools.py +385 -0
- package/src/qingflow_mcp/tools/import_tools.py +1971 -0
- package/src/qingflow_mcp/tools/navigation_tools.py +177 -0
- package/src/qingflow_mcp/tools/package_tools.py +240 -0
- package/src/qingflow_mcp/tools/portal_tools.py +131 -0
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +269 -0
- package/src/qingflow_mcp/tools/record_tools.py +12739 -0
- package/src/qingflow_mcp/tools/role_tools.py +94 -0
- package/src/qingflow_mcp/tools/solution_tools.py +3887 -0
- package/src/qingflow_mcp/tools/task_context_tools.py +1423 -0
- package/src/qingflow_mcp/tools/task_tools.py +843 -0
- package/src/qingflow_mcp/tools/view_tools.py +280 -0
- package/src/qingflow_mcp/tools/workflow_tools.py +312 -0
- package/src/qingflow_mcp/tools/workspace_tools.py +219 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from ..spec_models import EntitySpec, FieldType, FormLayoutRowSpec, FormLayoutSectionSpec
|
|
7
|
+
from .icon_utils import encode_workspace_icon
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
QUESTION_TYPE_MAP = {
|
|
11
|
+
FieldType.text: 2,
|
|
12
|
+
FieldType.long_text: 3,
|
|
13
|
+
FieldType.number: 8,
|
|
14
|
+
FieldType.amount: 8,
|
|
15
|
+
FieldType.date: 4,
|
|
16
|
+
FieldType.datetime: 4,
|
|
17
|
+
FieldType.member: 5,
|
|
18
|
+
FieldType.department: 22,
|
|
19
|
+
FieldType.single_select: 11,
|
|
20
|
+
FieldType.multi_select: 12,
|
|
21
|
+
FieldType.phone: 7,
|
|
22
|
+
FieldType.email: 6,
|
|
23
|
+
FieldType.address: 21,
|
|
24
|
+
FieldType.attachment: 13,
|
|
25
|
+
FieldType.boolean: 10,
|
|
26
|
+
FieldType.relation: 25,
|
|
27
|
+
FieldType.subtable: 18,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
OPTION_THEME_PALETTE = [
|
|
31
|
+
{
|
|
32
|
+
"hoverBorderColor": "#D16243",
|
|
33
|
+
"optionColor": "#FCE5DE",
|
|
34
|
+
"textColor": "#571C0C",
|
|
35
|
+
"themeId": 6,
|
|
36
|
+
"tickColor": "#D16243",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"hoverBorderColor": "#63AD0E",
|
|
40
|
+
"optionColor": "#E6F7D2",
|
|
41
|
+
"textColor": "#244201",
|
|
42
|
+
"themeId": 8,
|
|
43
|
+
"tickColor": "#63AD0E",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"hoverBorderColor": "#26ACD1",
|
|
47
|
+
"optionColor": "#DCF5FC",
|
|
48
|
+
"textColor": "#054557",
|
|
49
|
+
"themeId": 10,
|
|
50
|
+
"tickColor": "#26ACD1",
|
|
51
|
+
},
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def compile_entity_form(
|
|
56
|
+
entity: EntitySpec,
|
|
57
|
+
*,
|
|
58
|
+
include_package: bool,
|
|
59
|
+
) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any] | None, dict[str, dict[str, Any]], dict[str, str]]:
|
|
60
|
+
field_specs = {field.field_id: field.model_dump(mode="json") for field in entity.fields}
|
|
61
|
+
field_labels = {field.field_id: field.label for field in entity.fields}
|
|
62
|
+
app_create_payload = {
|
|
63
|
+
"appName": entity.display_name,
|
|
64
|
+
"appIcon": encode_workspace_icon(
|
|
65
|
+
icon=entity.icon,
|
|
66
|
+
color=entity.color,
|
|
67
|
+
title=entity.display_name,
|
|
68
|
+
fallback_icon_name="template",
|
|
69
|
+
),
|
|
70
|
+
"auth": default_member_auth(),
|
|
71
|
+
# Reserve the first slot inside a package for the package portal/homepage.
|
|
72
|
+
"ordinal": (entity.ordinal or 1) + (1 if include_package else 0),
|
|
73
|
+
}
|
|
74
|
+
if include_package:
|
|
75
|
+
app_create_payload["tagIds"] = ["__PACKAGE_TAG_ID__"]
|
|
76
|
+
|
|
77
|
+
questions_by_field_id: dict[str, dict[str, Any]] = {}
|
|
78
|
+
temp_id = -10000
|
|
79
|
+
for field in entity.fields:
|
|
80
|
+
question, next_temp_id = build_question(field.model_dump(mode="json"), temp_id)
|
|
81
|
+
questions_by_field_id[field.field_id] = question
|
|
82
|
+
temp_id = next_temp_id
|
|
83
|
+
|
|
84
|
+
base_questions = build_form_questions(entity, questions_by_field_id, include_reference=False)
|
|
85
|
+
base_payload = default_form_payload(entity.display_name, base_questions)
|
|
86
|
+
|
|
87
|
+
reference_payload = None
|
|
88
|
+
if any(field.type == FieldType.relation for field in entity.fields):
|
|
89
|
+
reference_questions = build_form_questions(entity, questions_by_field_id, include_reference=True)
|
|
90
|
+
reference_payload = default_form_payload(entity.display_name, reference_questions)
|
|
91
|
+
return app_create_payload, base_payload, reference_payload, field_specs, field_labels
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def default_member_auth() -> dict[str, Any]:
|
|
95
|
+
return {
|
|
96
|
+
"type": "WORKSPACE",
|
|
97
|
+
"contactAuth": {
|
|
98
|
+
"type": "WORKSPACE_ALL",
|
|
99
|
+
"authMembers": {
|
|
100
|
+
"member": [],
|
|
101
|
+
"depart": [],
|
|
102
|
+
"role": [],
|
|
103
|
+
"dynamic": [],
|
|
104
|
+
"includeSubDeparts": None,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
"externalMemberAuth": {
|
|
108
|
+
"type": "NOT",
|
|
109
|
+
"authMembers": {
|
|
110
|
+
"member": [],
|
|
111
|
+
"depart": [],
|
|
112
|
+
"role": [],
|
|
113
|
+
"dynamic": [],
|
|
114
|
+
"includeSubDeparts": None,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def default_form_payload(form_title: str, form_questions: list[list[dict[str, Any]]]) -> dict[str, Any]:
|
|
121
|
+
return {
|
|
122
|
+
"formTitle": form_title,
|
|
123
|
+
"serialNumType": 1,
|
|
124
|
+
"formTheme": 0,
|
|
125
|
+
"editVersionNo": 1,
|
|
126
|
+
"questionRelations": [],
|
|
127
|
+
"formQues": form_questions,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def build_form_questions(
|
|
132
|
+
entity: EntitySpec,
|
|
133
|
+
questions_by_field_id: dict[str, dict[str, Any]],
|
|
134
|
+
*,
|
|
135
|
+
include_reference: bool,
|
|
136
|
+
) -> list[list[dict[str, Any]]]:
|
|
137
|
+
used_field_ids: set[str] = set()
|
|
138
|
+
lines: list[list[dict[str, Any]]] = []
|
|
139
|
+
layout = entity.form_layout
|
|
140
|
+
if layout is not None:
|
|
141
|
+
for row in layout.rows:
|
|
142
|
+
row_questions = _row_questions(row, questions_by_field_id, include_reference=include_reference, used_field_ids=used_field_ids)
|
|
143
|
+
if row_questions:
|
|
144
|
+
lines.append(row_questions)
|
|
145
|
+
for section in layout.sections:
|
|
146
|
+
section_question = _section_question(
|
|
147
|
+
section,
|
|
148
|
+
questions_by_field_id,
|
|
149
|
+
include_reference=include_reference,
|
|
150
|
+
used_field_ids=used_field_ids,
|
|
151
|
+
)
|
|
152
|
+
if section_question is not None:
|
|
153
|
+
lines.append([section_question])
|
|
154
|
+
else:
|
|
155
|
+
for field in entity.fields:
|
|
156
|
+
if field.type == FieldType.relation and not include_reference:
|
|
157
|
+
continue
|
|
158
|
+
lines.append([deepcopy(questions_by_field_id[field.field_id])])
|
|
159
|
+
used_field_ids.add(field.field_id)
|
|
160
|
+
|
|
161
|
+
for field in entity.fields:
|
|
162
|
+
if field.field_id in used_field_ids:
|
|
163
|
+
continue
|
|
164
|
+
if field.type == FieldType.relation and not include_reference:
|
|
165
|
+
continue
|
|
166
|
+
lines.append([deepcopy(questions_by_field_id[field.field_id])])
|
|
167
|
+
return lines
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def build_question(field: dict[str, Any], temp_id: int) -> tuple[dict[str, Any], int]:
|
|
171
|
+
que_type = QUESTION_TYPE_MAP[field["type"]]
|
|
172
|
+
config = field.get("config") or {}
|
|
173
|
+
question = {
|
|
174
|
+
"queId": 0,
|
|
175
|
+
"queTempId": temp_id,
|
|
176
|
+
"queType": que_type,
|
|
177
|
+
"queTitle": field["label"],
|
|
178
|
+
"queWidth": int(config.get("que_width") or config.get("width") or 100),
|
|
179
|
+
"scanType": 1,
|
|
180
|
+
"status": 1,
|
|
181
|
+
"required": field["required"],
|
|
182
|
+
"queHint": field.get("description") or "",
|
|
183
|
+
"linkedQuestions": {},
|
|
184
|
+
"logicalShow": True,
|
|
185
|
+
"queDefaultValue": None,
|
|
186
|
+
"queDefaultType": 1,
|
|
187
|
+
"subQueWidth": 2,
|
|
188
|
+
"innerQuestions": [],
|
|
189
|
+
"beingHide": False,
|
|
190
|
+
"beingDesensitized": False,
|
|
191
|
+
}
|
|
192
|
+
next_temp_id = temp_id - 1
|
|
193
|
+
field_type = FieldType(field["type"])
|
|
194
|
+
if field_type in (FieldType.number, FieldType.amount, FieldType.text, FieldType.long_text, FieldType.phone, FieldType.email):
|
|
195
|
+
question.update(
|
|
196
|
+
{
|
|
197
|
+
"minWordCount": -1,
|
|
198
|
+
"maxWordCount": -1,
|
|
199
|
+
"canDecimal": field_type == FieldType.amount,
|
|
200
|
+
"numberFormat": 1,
|
|
201
|
+
"currencyType": 1,
|
|
202
|
+
}
|
|
203
|
+
)
|
|
204
|
+
if field_type == FieldType.date:
|
|
205
|
+
question["dateType"] = 0
|
|
206
|
+
if field_type == FieldType.datetime:
|
|
207
|
+
question["dateType"] = 1
|
|
208
|
+
if field_type == FieldType.member:
|
|
209
|
+
question.update({"memberDftValue": None, "memberSelectScopeType": 1, "memberSelectScope": {"member": []}})
|
|
210
|
+
if field_type == FieldType.department:
|
|
211
|
+
question.update({"deptSelectScopeType": 1, "deptSelectScope": {"depart": []}})
|
|
212
|
+
if field_type in (FieldType.single_select, FieldType.multi_select, FieldType.boolean):
|
|
213
|
+
options = field.get("options") or (["是", "否"] if field_type == FieldType.boolean else ["未命名1", "未命名2", "未命名3"])
|
|
214
|
+
question.update(
|
|
215
|
+
{
|
|
216
|
+
"options": build_options(options, start_temp_id=next_temp_id - len(options) + 1),
|
|
217
|
+
"unnamedOptions": options,
|
|
218
|
+
"optDirection": 0,
|
|
219
|
+
"beingShowColor": True,
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
if field_type == FieldType.multi_select:
|
|
223
|
+
question["optSelectMode"] = 1
|
|
224
|
+
next_temp_id -= len(options)
|
|
225
|
+
if field_type == FieldType.attachment:
|
|
226
|
+
question.update(
|
|
227
|
+
{
|
|
228
|
+
"fileBeingCaptureOnly": 0,
|
|
229
|
+
"fileSize": 20,
|
|
230
|
+
"fileType": [],
|
|
231
|
+
"pluginStatus": False,
|
|
232
|
+
"imageCompress": False,
|
|
233
|
+
"queDefaultValues": {"queId": temp_id, "queTitle": field["label"], "queType": que_type, "values": []},
|
|
234
|
+
}
|
|
235
|
+
)
|
|
236
|
+
if field_type == FieldType.address:
|
|
237
|
+
question.update({"addressPrecision": 1, "queDefaultValues": {"queId": temp_id, "queTitle": field["label"], "queType": que_type, "values": [], "tableValues": []}})
|
|
238
|
+
if field_type == FieldType.relation:
|
|
239
|
+
question.update(
|
|
240
|
+
{
|
|
241
|
+
"referenceConfig": build_reference_config(field, temp_id),
|
|
242
|
+
"queOriginType": 25,
|
|
243
|
+
"queDefaultType": 2,
|
|
244
|
+
}
|
|
245
|
+
)
|
|
246
|
+
if field_type == FieldType.subtable:
|
|
247
|
+
sub_questions: list[dict[str, Any]] = []
|
|
248
|
+
for subfield in field.get("subfields", []):
|
|
249
|
+
sub_question, next_temp_id = build_question(subfield, next_temp_id)
|
|
250
|
+
sub_questions.append(sub_question)
|
|
251
|
+
question["subQuestions"] = sub_questions
|
|
252
|
+
question["innerQuestions"] = [deepcopy(sub_questions)]
|
|
253
|
+
question["queDefaultValues"] = {"queId": temp_id, "queTitle": field["label"], "queType": que_type, "values": [], "tableValues": []}
|
|
254
|
+
return question, next_temp_id
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def build_reference_config(field: dict[str, Any], temp_id: int) -> dict[str, Any]:
|
|
258
|
+
config = field.get("config") or {}
|
|
259
|
+
display_field_id = field.get("target_field_id") or "title"
|
|
260
|
+
display_field_que_id = field.get("target_field_que_id") or config.get("target_field_que_id") or 0
|
|
261
|
+
display_field_label = config.get("target_field_label") or "__TARGET_FIELD_LABEL__"
|
|
262
|
+
refer_field_ids = list(config.get("refer_field_ids") or [display_field_id])
|
|
263
|
+
refer_field_que_ids = list(config.get("refer_field_que_ids") or [])
|
|
264
|
+
refer_field_labels = config.get("refer_field_labels") or [display_field_label]
|
|
265
|
+
refer_field_types = config.get("refer_field_types")
|
|
266
|
+
refer_questions = []
|
|
267
|
+
for ordinal, field_id in enumerate(refer_field_ids, start=1):
|
|
268
|
+
label = refer_field_labels[ordinal - 1] if ordinal - 1 < len(refer_field_labels) else field_id
|
|
269
|
+
que_id = refer_field_que_ids[ordinal - 1] if ordinal - 1 < len(refer_field_que_ids) else 0
|
|
270
|
+
raw_type = _reference_field_type_value(
|
|
271
|
+
refer_field_types,
|
|
272
|
+
field_id=field_id,
|
|
273
|
+
ordinal=ordinal,
|
|
274
|
+
default=config.get("target_field_type") if field_id == display_field_id else None,
|
|
275
|
+
)
|
|
276
|
+
refer_questions.append(
|
|
277
|
+
{
|
|
278
|
+
"queId": que_id,
|
|
279
|
+
"queTitle": label,
|
|
280
|
+
"queType": _normalize_reference_que_type(raw_type) or "2",
|
|
281
|
+
"queAuth": 1,
|
|
282
|
+
"ordinal": ordinal,
|
|
283
|
+
"quoteId": temp_id,
|
|
284
|
+
"_field_id": field_id,
|
|
285
|
+
}
|
|
286
|
+
)
|
|
287
|
+
auth_field_ids = list(config.get("auth_field_ids") or refer_field_ids)
|
|
288
|
+
auth_field_que_ids = list(config.get("auth_field_que_ids") or [])
|
|
289
|
+
auth_ques = deepcopy(config.get("refer_auth_ques") or [])
|
|
290
|
+
if not auth_ques:
|
|
291
|
+
auth_ques = []
|
|
292
|
+
for ordinal, field_id in enumerate(auth_field_ids, start=1):
|
|
293
|
+
que_id = auth_field_que_ids[ordinal - 1] if ordinal - 1 < len(auth_field_que_ids) else 0
|
|
294
|
+
auth_ques.append({"queId": que_id, "queAuth": 1, "_field_id": field_id})
|
|
295
|
+
return {
|
|
296
|
+
"referAppKey": "__TARGET_APP_KEY__",
|
|
297
|
+
"referQueId": display_field_que_id,
|
|
298
|
+
"customButtonText": config.get("custom_button_text") or "选择数据",
|
|
299
|
+
"beingTableSource": False,
|
|
300
|
+
"referQuestions": refer_questions,
|
|
301
|
+
"referMatchRules": deepcopy(config.get("refer_match_rules") or []),
|
|
302
|
+
"referFillRules": deepcopy(config.get("refer_fill_rules") or []),
|
|
303
|
+
"referAuthQues": auth_ques,
|
|
304
|
+
"canAddData": bool(config.get("can_add_data", False)),
|
|
305
|
+
"dataAdditionButtonText": config.get("data_addition_button_text") or "新增数据",
|
|
306
|
+
"canViewProcessLog": bool(config.get("can_view_process_log", True)),
|
|
307
|
+
"optionalDataNum": int(config.get("optional_data_num", 1)),
|
|
308
|
+
"beingDataLogVisible": bool(config.get("being_data_log_visible", True)),
|
|
309
|
+
"beingDefaultFormulaAutoFillEnabled": bool(config.get("being_default_formula_auto_fill_enabled", False)),
|
|
310
|
+
"defaultValueMatchRules": deepcopy(config.get("default_value_match_rules") or []),
|
|
311
|
+
"configShowForm": config.get("config_show_form") or "TABLE",
|
|
312
|
+
"configSortFieldId": None,
|
|
313
|
+
"configAsc": config.get("config_asc", True),
|
|
314
|
+
"dataShowForm": config.get("data_show_form") or "CARD",
|
|
315
|
+
"defaultRow": config.get("default_row"),
|
|
316
|
+
"fieldNameShow": config.get("field_name_show", True),
|
|
317
|
+
"dataSortFieldId": None,
|
|
318
|
+
"dataSortAsc": config.get("data_sort_asc"),
|
|
319
|
+
"_targetFieldId": display_field_id,
|
|
320
|
+
"_targetEntityId": field.get("target_entity_id"),
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def build_options(options: list[str], *, start_temp_id: int) -> list[dict[str, Any]]:
|
|
325
|
+
items: list[dict[str, Any]] = []
|
|
326
|
+
current = start_temp_id
|
|
327
|
+
for index, option in enumerate(options):
|
|
328
|
+
items.append(
|
|
329
|
+
{
|
|
330
|
+
"otherValue": None,
|
|
331
|
+
"beingDefault": False,
|
|
332
|
+
"optId": 0,
|
|
333
|
+
"optValue": option,
|
|
334
|
+
"optUrl": "",
|
|
335
|
+
"beingOtherOpt": False,
|
|
336
|
+
"beingRequired": False,
|
|
337
|
+
"applyLimit": -1,
|
|
338
|
+
"currentApply": 0,
|
|
339
|
+
"linkQueIds": [],
|
|
340
|
+
"emptyText": "",
|
|
341
|
+
"tempId": current,
|
|
342
|
+
"optionThemeColor": deepcopy(OPTION_THEME_PALETTE[index % len(OPTION_THEME_PALETTE)]),
|
|
343
|
+
}
|
|
344
|
+
)
|
|
345
|
+
current += 1
|
|
346
|
+
return items
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _row_questions(
|
|
350
|
+
row: FormLayoutRowSpec,
|
|
351
|
+
questions_by_field_id: dict[str, dict[str, Any]],
|
|
352
|
+
*,
|
|
353
|
+
include_reference: bool,
|
|
354
|
+
used_field_ids: set[str],
|
|
355
|
+
) -> list[dict[str, Any]]:
|
|
356
|
+
questions: list[dict[str, Any]] = []
|
|
357
|
+
for field_id in row.field_ids:
|
|
358
|
+
question = deepcopy(questions_by_field_id[field_id])
|
|
359
|
+
if question.get("queType") == 25 and not include_reference:
|
|
360
|
+
continue
|
|
361
|
+
used_field_ids.add(field_id)
|
|
362
|
+
questions.append(question)
|
|
363
|
+
widths = _resolve_row_widths(row, questions)
|
|
364
|
+
for question, width in zip(questions, widths):
|
|
365
|
+
question["queWidth"] = width
|
|
366
|
+
return questions
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _section_question(
|
|
370
|
+
section: FormLayoutSectionSpec,
|
|
371
|
+
questions_by_field_id: dict[str, dict[str, Any]],
|
|
372
|
+
*,
|
|
373
|
+
include_reference: bool,
|
|
374
|
+
used_field_ids: set[str],
|
|
375
|
+
) -> dict[str, Any] | None:
|
|
376
|
+
inner_rows: list[list[dict[str, Any]]] = []
|
|
377
|
+
for row in section.rows:
|
|
378
|
+
row_questions = _row_questions(row, questions_by_field_id, include_reference=include_reference, used_field_ids=used_field_ids)
|
|
379
|
+
if row_questions:
|
|
380
|
+
inner_rows.append(row_questions)
|
|
381
|
+
if not inner_rows:
|
|
382
|
+
return None
|
|
383
|
+
config = section.config or {}
|
|
384
|
+
return {
|
|
385
|
+
"queId": 0,
|
|
386
|
+
"queTempId": -(20000 + sum(ord(ch) for ch in section.section_id)),
|
|
387
|
+
"queType": 24,
|
|
388
|
+
"queTitle": section.title,
|
|
389
|
+
"queWidth": int(config.get("que_width") or 100),
|
|
390
|
+
"scanType": 1,
|
|
391
|
+
"status": 1,
|
|
392
|
+
"required": False,
|
|
393
|
+
"queHint": config.get("description") or "",
|
|
394
|
+
"linkedQuestions": {},
|
|
395
|
+
"logicalShow": True,
|
|
396
|
+
"queDefaultValue": None,
|
|
397
|
+
"queDefaultType": 1,
|
|
398
|
+
"subQueWidth": 2,
|
|
399
|
+
"innerQuestions": inner_rows,
|
|
400
|
+
"beingHide": False,
|
|
401
|
+
"beingDesensitized": False,
|
|
402
|
+
"sectionId": config.get("section_id"),
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _resolve_row_widths(row: FormLayoutRowSpec, questions: list[dict[str, Any]]) -> list[int]:
|
|
407
|
+
if not questions:
|
|
408
|
+
return []
|
|
409
|
+
configured_widths = row.config.get("field_widths") if isinstance(row.config.get("field_widths"), dict) else {}
|
|
410
|
+
widths: list[int] = []
|
|
411
|
+
remaining_slots = 0
|
|
412
|
+
assigned_total = 0
|
|
413
|
+
for field_id, question in zip(row.field_ids, questions):
|
|
414
|
+
width = configured_widths.get(field_id)
|
|
415
|
+
if width is None:
|
|
416
|
+
widths.append(-1)
|
|
417
|
+
remaining_slots += 1
|
|
418
|
+
continue
|
|
419
|
+
resolved_width = int(width)
|
|
420
|
+
widths.append(resolved_width)
|
|
421
|
+
assigned_total += resolved_width
|
|
422
|
+
if remaining_slots <= 0:
|
|
423
|
+
return widths
|
|
424
|
+
remaining_total = max(0, 100 - assigned_total)
|
|
425
|
+
auto_widths = _auto_row_widths(remaining_slots, total=remaining_total)
|
|
426
|
+
auto_index = 0
|
|
427
|
+
for index, width in enumerate(widths):
|
|
428
|
+
if width >= 0:
|
|
429
|
+
continue
|
|
430
|
+
widths[index] = auto_widths[auto_index]
|
|
431
|
+
auto_index += 1
|
|
432
|
+
return widths
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _auto_row_widths(count: int, *, total: int = 100) -> list[int]:
|
|
436
|
+
if count <= 0:
|
|
437
|
+
return []
|
|
438
|
+
base = total // count
|
|
439
|
+
remainder = total % count
|
|
440
|
+
return [base + (1 if index < remainder else 0) for index in range(count)]
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _reference_field_type_value(raw_types: Any, *, field_id: str, ordinal: int, default: Any) -> Any:
|
|
444
|
+
if isinstance(raw_types, dict):
|
|
445
|
+
return raw_types.get(field_id, default)
|
|
446
|
+
if isinstance(raw_types, list):
|
|
447
|
+
index = ordinal - 1
|
|
448
|
+
if 0 <= index < len(raw_types):
|
|
449
|
+
return raw_types[index]
|
|
450
|
+
return default
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _normalize_reference_que_type(raw_type: Any) -> str | None:
|
|
454
|
+
if raw_type is None:
|
|
455
|
+
return None
|
|
456
|
+
if isinstance(raw_type, int):
|
|
457
|
+
return str(raw_type)
|
|
458
|
+
value = str(raw_type).strip()
|
|
459
|
+
if not value:
|
|
460
|
+
return None
|
|
461
|
+
if value.isdigit():
|
|
462
|
+
return value
|
|
463
|
+
try:
|
|
464
|
+
return str(QUESTION_TYPE_MAP[FieldType(value)])
|
|
465
|
+
except ValueError:
|
|
466
|
+
return None
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
DEFAULT_ICON_COLOR = "qing-orange"
|
|
7
|
+
SUPPORTED_EX_ICONS = {
|
|
8
|
+
"ex-alert-outlined",
|
|
9
|
+
"ex-clock-outlined",
|
|
10
|
+
"ex-folder-outlined",
|
|
11
|
+
"ex-form-outlined",
|
|
12
|
+
"ex-home-outlined",
|
|
13
|
+
"ex-user-outlined",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
ICON_ALIAS_MAP = {
|
|
17
|
+
"archive-box": "ex-form-outlined",
|
|
18
|
+
"beaker": "ex-form-outlined",
|
|
19
|
+
"briefcase": "ex-folder-outlined",
|
|
20
|
+
"building-office": "ex-form-outlined",
|
|
21
|
+
"calendar": "ex-clock-outlined",
|
|
22
|
+
"chat": "ex-clock-outlined",
|
|
23
|
+
"chat-bubble-left-right": "ex-clock-outlined",
|
|
24
|
+
"chart-bar": "ex-form-outlined",
|
|
25
|
+
"chart-square-bar": "ex-form-outlined",
|
|
26
|
+
"check-badge": "ex-alert-outlined",
|
|
27
|
+
"check-circle": "ex-alert-outlined",
|
|
28
|
+
"clipboard-check": "ex-form-outlined",
|
|
29
|
+
"clock": "ex-clock-outlined",
|
|
30
|
+
"cube": "ex-form-outlined",
|
|
31
|
+
"document-text": "ex-form-outlined",
|
|
32
|
+
"folder": "ex-folder-outlined",
|
|
33
|
+
"home": "ex-home-outlined",
|
|
34
|
+
"office-building": "ex-form-outlined",
|
|
35
|
+
"shield-check": "ex-alert-outlined",
|
|
36
|
+
"template": "ex-form-outlined",
|
|
37
|
+
"user": "ex-user-outlined",
|
|
38
|
+
"user-group": "ex-user-outlined",
|
|
39
|
+
"users": "ex-user-outlined",
|
|
40
|
+
"view-grid": "ex-home-outlined",
|
|
41
|
+
"warning": "ex-alert-outlined",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def encode_workspace_icon(
|
|
46
|
+
*,
|
|
47
|
+
icon: str | None,
|
|
48
|
+
color: str | None,
|
|
49
|
+
title: str,
|
|
50
|
+
fallback_icon_name: str | None = None,
|
|
51
|
+
) -> str:
|
|
52
|
+
if icon and _looks_like_icon_json(icon):
|
|
53
|
+
return icon
|
|
54
|
+
normalized_icon_name = normalize_workspace_icon_name(icon or fallback_icon_name)
|
|
55
|
+
if normalized_icon_name and normalized_icon_name.startswith("ex-") and not color:
|
|
56
|
+
return normalized_icon_name
|
|
57
|
+
icon_name = normalized_icon_name
|
|
58
|
+
icon_color = color or DEFAULT_ICON_COLOR
|
|
59
|
+
if icon_name:
|
|
60
|
+
return json.dumps(
|
|
61
|
+
{
|
|
62
|
+
"icon": None,
|
|
63
|
+
"iconColor": icon_color,
|
|
64
|
+
"iconName": icon_name,
|
|
65
|
+
"iconText": None,
|
|
66
|
+
},
|
|
67
|
+
ensure_ascii=False,
|
|
68
|
+
separators=(",", ":"),
|
|
69
|
+
)
|
|
70
|
+
icon_text = (title or "未")[:1]
|
|
71
|
+
return json.dumps(
|
|
72
|
+
{
|
|
73
|
+
"iconColor": icon_color,
|
|
74
|
+
"iconText": icon_text,
|
|
75
|
+
},
|
|
76
|
+
ensure_ascii=False,
|
|
77
|
+
separators=(",", ":"),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def normalize_workspace_icon_name(icon: str | None) -> str | None:
|
|
82
|
+
if not icon:
|
|
83
|
+
return None
|
|
84
|
+
if _looks_like_icon_json(icon):
|
|
85
|
+
try:
|
|
86
|
+
payload = json.loads(icon)
|
|
87
|
+
except Exception:
|
|
88
|
+
return "ex-form-outlined"
|
|
89
|
+
return normalize_workspace_icon_name(payload.get("iconName")) or "ex-form-outlined"
|
|
90
|
+
if icon in SUPPORTED_EX_ICONS:
|
|
91
|
+
return icon
|
|
92
|
+
normalized = icon.strip().lower()
|
|
93
|
+
if normalized in ICON_ALIAS_MAP:
|
|
94
|
+
return ICON_ALIAS_MAP[normalized]
|
|
95
|
+
for token in normalized.replace("_", "-").split("-"):
|
|
96
|
+
if token in ICON_ALIAS_MAP:
|
|
97
|
+
return ICON_ALIAS_MAP[token]
|
|
98
|
+
if "folder" in normalized or "package" in normalized:
|
|
99
|
+
return "ex-folder-outlined"
|
|
100
|
+
if "home" in normalized or "portal" in normalized or "dashboard" in normalized:
|
|
101
|
+
return "ex-home-outlined"
|
|
102
|
+
if "user" in normalized or "member" in normalized or "contact" in normalized:
|
|
103
|
+
return "ex-user-outlined"
|
|
104
|
+
if "clock" in normalized or "time" in normalized or "schedule" in normalized or "log" in normalized:
|
|
105
|
+
return "ex-clock-outlined"
|
|
106
|
+
if "risk" in normalized or "alert" in normalized or "warn" in normalized or "safety" in normalized:
|
|
107
|
+
return "ex-alert-outlined"
|
|
108
|
+
return "ex-form-outlined"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _looks_like_icon_json(value: str) -> bool:
|
|
112
|
+
stripped = value.strip()
|
|
113
|
+
return stripped.startswith("{") and stripped.endswith("}")
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ..spec_models import NavigationItemSpec, NavigationTargetType, SolutionSpec
|
|
6
|
+
from .icon_utils import normalize_workspace_icon_name
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def compile_navigation(spec: SolutionSpec) -> list[dict[str, Any]]:
|
|
10
|
+
if not spec.preferences.create_navigation or not spec.navigation.enabled:
|
|
11
|
+
return []
|
|
12
|
+
compiled_items = [_compile_item(item) for item in spec.navigation.items]
|
|
13
|
+
if not compiled_items:
|
|
14
|
+
return []
|
|
15
|
+
package_item = next(
|
|
16
|
+
(
|
|
17
|
+
item
|
|
18
|
+
for item in compiled_items
|
|
19
|
+
if item["payload"].get("targetType") == "TAG"
|
|
20
|
+
),
|
|
21
|
+
None,
|
|
22
|
+
)
|
|
23
|
+
if package_item is None:
|
|
24
|
+
return compiled_items[:1]
|
|
25
|
+
package_item["children"] = []
|
|
26
|
+
return [package_item]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _compile_item(item: NavigationItemSpec) -> dict[str, Any]:
|
|
30
|
+
default_icon = _default_navigation_icon(item.target_type)
|
|
31
|
+
payload: dict[str, Any] = {
|
|
32
|
+
"item_id": item.item_id,
|
|
33
|
+
"payload": {
|
|
34
|
+
"name": item.title,
|
|
35
|
+
"navigationIcon": normalize_workspace_icon_name(item.icon or default_icon) or default_icon,
|
|
36
|
+
"auth": {"type": "WORKSPACE"},
|
|
37
|
+
**item.config,
|
|
38
|
+
},
|
|
39
|
+
"children": [_compile_item(child) for child in item.children],
|
|
40
|
+
}
|
|
41
|
+
if item.target_type == NavigationTargetType.package:
|
|
42
|
+
payload["payload"].update({"targetType": "TAG", "tagId": "__PACKAGE_TAG_ID__"})
|
|
43
|
+
elif item.target_type == NavigationTargetType.app:
|
|
44
|
+
payload["payload"].update({"targetType": "APP", "appRef": {"entity_id": item.entity_id}})
|
|
45
|
+
elif item.target_type == NavigationTargetType.portal:
|
|
46
|
+
payload["payload"].update({"targetType": "DASH", "dashKey": "__PORTAL_DASH_KEY__"})
|
|
47
|
+
else:
|
|
48
|
+
raise ValueError("navigation target_type currently supports only: package, app, portal")
|
|
49
|
+
return payload
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _default_navigation_icon(target_type: NavigationTargetType) -> str:
|
|
53
|
+
if target_type == NavigationTargetType.package:
|
|
54
|
+
return "ex-folder-outlined"
|
|
55
|
+
if target_type == NavigationTargetType.portal:
|
|
56
|
+
return "ex-home-outlined"
|
|
57
|
+
return "ex-form-outlined"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ..spec_models import SolutionSpec
|
|
4
|
+
from .icon_utils import encode_workspace_icon
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def compile_package(spec: SolutionSpec) -> dict | None:
|
|
8
|
+
if not spec.preferences.create_package or not spec.package.enabled:
|
|
9
|
+
return None
|
|
10
|
+
return {
|
|
11
|
+
"tagName": spec.package.name or spec.solution_name,
|
|
12
|
+
"tagIcon": encode_workspace_icon(
|
|
13
|
+
icon=spec.package.icon,
|
|
14
|
+
color=spec.package.color,
|
|
15
|
+
title=spec.package.name or spec.solution_name,
|
|
16
|
+
fallback_icon_name="files-folder",
|
|
17
|
+
),
|
|
18
|
+
"ordinal": spec.package.ordinal,
|
|
19
|
+
}
|