@qingflow-tech/qingflow-cli 1.1.43-beta.1 → 1.1.43-beta.3
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 +12 -5
- package/entry_point.py +1 -1
- package/npm/bin/qingflow.mjs +1 -2
- package/npm/lib/runtime.mjs +3 -7
- package/package.json +1 -2
- package/pyproject.toml +1 -3
- package/skills/qingflow-app-form/SKILL.md +63 -0
- package/skills/qingflow-app-form/agents/openai.yaml +4 -0
- package/skills/qingflow-cli/SKILL.md +8 -6
- package/skills/qingflow-cli/manifest.yaml +1 -1
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md +4 -4
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_EXPLORATION_REPORT.md +1 -1
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_FIELD_DATA_TYPES.md +1 -1
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_SCHEMA_APPLY_FIELD_TYPES_AND_SCENARIOS.md +4 -4
- package/skills/qingflow-cli/reference/builder/10-build-single-app.md +5 -5
- package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +14 -14
- package/skills/qingflow-cli/reference/builder/30-schema-fields.md +98 -101
- package/skills/qingflow-cli/reference/builder/40-layout.md +39 -31
- package/skills/qingflow-cli/reference/builder/README.md +9 -9
- package/skills/qingflow-cli/reference/builder/code-integrations/README.md +4 -2
- package/skills/qingflow-cli/reference/builder/code-integrations/code-block.md +2 -0
- package/skills/qingflow-cli/reference/builder/code-integrations/q-linker.md +2 -0
- package/skills/qingflow-cli/reference/builder/reference/app-delivery-sop.md +45 -38
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_EXPLORATION_REPORT.md +1 -1
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_FIELD_DATA_TYPES.md +1 -1
- package/skills/qingflow-cli/reference/examples/README.md +3 -3
- package/skills/qingflow-common/SKILL.md +47 -0
- package/skills/qingflow-common/agents/openai.yaml +4 -0
- package/src/qingflow_mcp/__main__.py +2 -3
- package/src/qingflow_mcp/app_form_compat.py +6 -1
- package/src/qingflow_mcp/builder_facade/app_form.py +547 -18
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +1321 -178
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +268 -7
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +115 -10
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +304 -7
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +23 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +23 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +23 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +23 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +145 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +69 -10
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +69 -10
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +69 -10
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +360 -7
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +71 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +115 -10
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +78 -10
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +23 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +249 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +71 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +71 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +1041 -20
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +69 -10
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +23 -0
- package/src/qingflow_mcp/response_trim.py +41 -1
- package/src/qingflow_mcp/server.py +24 -3
- package/src/qingflow_mcp/server_app_builder.py +1 -1
- package/src/qingflow_mcp/tools/record_tools.py +3 -3
- package/src/qingflow_mcp/version.py +2 -4
- package/docs/local-agent-install.md +0 -370
- package/skills/qingflow-mcp-setup/SKILL.md +0 -115
- package/skills/qingflow-mcp-setup/agents/openai.yaml +0 -4
- package/skills/qingflow-mcp-setup/references/claude-desktop.md +0 -34
- package/skills/qingflow-mcp-setup/references/environments.md +0 -62
- package/skills/qingflow-mcp-setup/references/generic-stdio.md +0 -32
- package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +0 -38
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import base64
|
|
3
4
|
from copy import deepcopy
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
import json
|
|
6
7
|
import re
|
|
7
8
|
from typing import Any, Iterable
|
|
9
|
+
from urllib.parse import unquote_plus
|
|
8
10
|
|
|
9
11
|
from ..app_form_compat import normalize_app_form_declaration, validate_app_form_create_icon
|
|
10
12
|
from ..errors import QingflowApiError, is_auth_like_error
|
|
@@ -19,6 +21,8 @@ from .service import (
|
|
|
19
21
|
_compile_code_block_binding_fields,
|
|
20
22
|
_compile_q_linker_binding_fields,
|
|
21
23
|
_field_to_question,
|
|
24
|
+
_looks_like_backend_encoded_formula,
|
|
25
|
+
_normalize_formula_defaults_for_save,
|
|
22
26
|
_normalize_and_validate_code_block_fields_for_write,
|
|
23
27
|
)
|
|
24
28
|
|
|
@@ -159,6 +163,17 @@ def _positive_int(value: Any) -> int | None:
|
|
|
159
163
|
return result if result > 0 else None
|
|
160
164
|
|
|
161
165
|
|
|
166
|
+
def _decode_formula_default(value: Any) -> Any:
|
|
167
|
+
if not isinstance(value, str) or not _looks_like_backend_encoded_formula(value):
|
|
168
|
+
return value
|
|
169
|
+
encoded = value[16:-16]
|
|
170
|
+
try:
|
|
171
|
+
decoded = base64.b64decode(encoded, validate=True).decode("utf-8")
|
|
172
|
+
return unquote_plus(decoded)
|
|
173
|
+
except Exception:
|
|
174
|
+
return value
|
|
175
|
+
|
|
176
|
+
|
|
162
177
|
def _without_none(value: Any) -> Any:
|
|
163
178
|
if isinstance(value, dict):
|
|
164
179
|
return {key: _without_none(item) for key, item in value.items() if item is not None}
|
|
@@ -167,6 +182,117 @@ def _without_none(value: Any) -> Any:
|
|
|
167
182
|
return value
|
|
168
183
|
|
|
169
184
|
|
|
185
|
+
def _canonical_unlimited(value: Any) -> Any:
|
|
186
|
+
"""Expose the backend -1 sentinel as the declarative null/omitted form."""
|
|
187
|
+
return None if value == -1 else value
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _backend_unlimited(value: Any) -> Any:
|
|
191
|
+
"""Translate an explicit declarative null back to the backend -1 sentinel."""
|
|
192
|
+
return -1 if value is None else value
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _scope_items(scope: Any, key: str) -> list[JSONObject]:
|
|
196
|
+
value = scope.get(key) if isinstance(scope, dict) else None
|
|
197
|
+
return [item for item in value if isinstance(item, dict)] if isinstance(value, list) else []
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _backend_member_scope(scope: Any) -> JSONObject | None:
|
|
201
|
+
"""Translate the public member/depart scope to AuthMembersIncludeExternalBean JSON."""
|
|
202
|
+
if not isinstance(scope, dict):
|
|
203
|
+
return None
|
|
204
|
+
result: JSONObject = {
|
|
205
|
+
"member": [{"uid": item.get("uid"), "nickName": item.get("nickName")} for item in _scope_items(scope, "members")],
|
|
206
|
+
"depart": [{"deptId": item.get("deptId"), "deptName": item.get("deptName")} for item in _scope_items(scope, "departments")],
|
|
207
|
+
"role": [{"roleId": item.get("roleId"), "roleName": item.get("roleName")} for item in _scope_items(scope, "roles")],
|
|
208
|
+
"dynamic": deepcopy(_scope_items(scope, "dynamic")),
|
|
209
|
+
"externalMemberList": deepcopy(_scope_items(scope, "externalMembers")),
|
|
210
|
+
"externalDepartList": deepcopy(_scope_items(scope, "externalDepartments")),
|
|
211
|
+
}
|
|
212
|
+
if "includeSubDeparts" in scope:
|
|
213
|
+
result["includeSubDeparts"] = bool(scope.get("includeSubDeparts"))
|
|
214
|
+
return result
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _canonical_member_scope(scope: Any, scope_type: Any, *, department: bool = False) -> JSONObject:
|
|
218
|
+
normalized_type = _positive_int(scope_type) or 1
|
|
219
|
+
if normalized_type == 1:
|
|
220
|
+
return {"mode": "all"}
|
|
221
|
+
raw = scope if isinstance(scope, dict) else {}
|
|
222
|
+
result: JSONObject = {
|
|
223
|
+
"mode": "custom" if normalized_type == 2 else "relation",
|
|
224
|
+
"includeSubDeparts": bool(raw.get("includeSubDeparts")),
|
|
225
|
+
}
|
|
226
|
+
if department:
|
|
227
|
+
result["departments"] = [
|
|
228
|
+
_without_none({"deptId": _positive_int(item.get("deptId") or item.get("id")), "deptName": item.get("deptName") or item.get("name")})
|
|
229
|
+
for item in _scope_items(raw, "depart")
|
|
230
|
+
if _positive_int(item.get("deptId") or item.get("id")) is not None
|
|
231
|
+
]
|
|
232
|
+
else:
|
|
233
|
+
result["members"] = [
|
|
234
|
+
_without_none({"uid": _positive_int(item.get("uid")), "nickName": item.get("nickName")})
|
|
235
|
+
for item in _scope_items(raw, "member")
|
|
236
|
+
if _positive_int(item.get("uid")) is not None
|
|
237
|
+
]
|
|
238
|
+
result["departments"] = [
|
|
239
|
+
_without_none({"deptId": _positive_int(item.get("deptId")), "deptName": item.get("deptName")})
|
|
240
|
+
for item in _scope_items(raw, "depart")
|
|
241
|
+
if _positive_int(item.get("deptId")) is not None
|
|
242
|
+
]
|
|
243
|
+
result["roles"] = [
|
|
244
|
+
_without_none({"roleId": _positive_int(item.get("roleId")), "roleName": item.get("roleName")})
|
|
245
|
+
for item in _scope_items(raw, "role")
|
|
246
|
+
if _positive_int(item.get("roleId")) is not None
|
|
247
|
+
]
|
|
248
|
+
result["dynamic"] = [
|
|
249
|
+
_without_none({
|
|
250
|
+
"type": _positive_int(item.get("type")),
|
|
251
|
+
"queId": item.get("queId"),
|
|
252
|
+
"queTitle": item.get("queTitle"),
|
|
253
|
+
})
|
|
254
|
+
for item in _scope_items(raw, "dynamic")
|
|
255
|
+
if _positive_int(item.get("type")) is not None
|
|
256
|
+
]
|
|
257
|
+
result["externalMembers"] = [
|
|
258
|
+
_without_none({
|
|
259
|
+
"uid": _positive_int(item.get("uid")),
|
|
260
|
+
"nickName": item.get("nickName"),
|
|
261
|
+
"email": item.get("email"),
|
|
262
|
+
})
|
|
263
|
+
for item in _scope_items(raw, "externalMemberList")
|
|
264
|
+
if _positive_int(item.get("uid")) is not None
|
|
265
|
+
]
|
|
266
|
+
result["externalDepartments"] = [
|
|
267
|
+
_without_none({
|
|
268
|
+
"deptId": _positive_int(item.get("deptId")),
|
|
269
|
+
"deptName": item.get("deptName"),
|
|
270
|
+
})
|
|
271
|
+
for item in _scope_items(raw, "externalDepartList")
|
|
272
|
+
if _positive_int(item.get("deptId")) is not None
|
|
273
|
+
]
|
|
274
|
+
return _without_none(result)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _canonical_scope_relation(relation: Any) -> JSONObject | None:
|
|
278
|
+
if not isinstance(relation, dict):
|
|
279
|
+
return None
|
|
280
|
+
displayed = relation.get("displayedQueInfo") if isinstance(relation.get("displayedQueInfo"), dict) else {}
|
|
281
|
+
displayed_id = _positive_int(relation.get("displayedQueId") or displayed.get("queId"))
|
|
282
|
+
if displayed_id is None or not displayed.get("appKey"):
|
|
283
|
+
return None
|
|
284
|
+
result: JSONObject = {
|
|
285
|
+
"displayedField": _selector(displayed_id, displayed.get("queTitle")),
|
|
286
|
+
"targetAppKey": displayed.get("appKey"),
|
|
287
|
+
"targetAppTitle": displayed.get("appTitle"),
|
|
288
|
+
"displayedFieldType": displayed.get("queType"),
|
|
289
|
+
"matchRuleType": relation.get("matchRuleType"),
|
|
290
|
+
"matchRules": relation.get("matchRules"),
|
|
291
|
+
"matchRuleFormula": relation.get("matchRuleFormula"),
|
|
292
|
+
}
|
|
293
|
+
return _without_none(result)
|
|
294
|
+
|
|
295
|
+
|
|
170
296
|
def _selector(que_id: Any, name: Any) -> JSONObject:
|
|
171
297
|
result: JSONObject = {}
|
|
172
298
|
normalized_id = _positive_int(que_id)
|
|
@@ -221,6 +347,7 @@ def _semantic(value: Any) -> Any:
|
|
|
221
347
|
key: _semantic(item)
|
|
222
348
|
for key, item in sorted(value.items())
|
|
223
349
|
if key not in identity_keys
|
|
350
|
+
and not key.startswith("_")
|
|
224
351
|
and not (key == "sort" and isinstance(item, dict) and item.get("enabled") is False)
|
|
225
352
|
}
|
|
226
353
|
if isinstance(value, list):
|
|
@@ -308,6 +435,7 @@ def _validate_payload_limits(value: Any) -> None:
|
|
|
308
435
|
class AppFormNormalizer:
|
|
309
436
|
def from_backend(self, *, context: AppFormSchemaContext, app_key: str, form: JSONObject) -> JSONObject:
|
|
310
437
|
relations = [item for item in form.get("questionRelations") or [] if isinstance(item, dict)]
|
|
438
|
+
select_scope_relations = [item for item in form.get("selectScopeRelations") or [] if isinstance(item, dict)]
|
|
311
439
|
field_names = self._field_index(form)
|
|
312
440
|
title_id = _positive_int(form.get("dataTitleQueId"))
|
|
313
441
|
cover_id = _positive_int(form.get("dataCoverQueId"))
|
|
@@ -321,7 +449,7 @@ class AppFormNormalizer:
|
|
|
321
449
|
if section_id is None:
|
|
322
450
|
raise AppFormFailure("UNSUPPORTED_LAYOUT", "existing section has no stable ID", "NORMALIZE")
|
|
323
451
|
rows = [
|
|
324
|
-
{"fields": [self._field(question, relations, title_id, cover_id, field_names) for question in inner if isinstance(question, dict)]}
|
|
452
|
+
{"fields": [self._field(question, relations, select_scope_relations, title_id, cover_id, field_names) for question in inner if isinstance(question, dict)]}
|
|
325
453
|
for inner in section.get("innerQuestions") or []
|
|
326
454
|
if isinstance(inner, list) and inner
|
|
327
455
|
]
|
|
@@ -329,7 +457,7 @@ class AppFormNormalizer:
|
|
|
329
457
|
raise AppFormFailure("UNSUPPORTED_LAYOUT", "existing section is empty", "NORMALIZE")
|
|
330
458
|
body.append({"kind": "section", "sectionId": str(section_id), "title": str(section.get("queTitle") or ""), "rows": rows})
|
|
331
459
|
else:
|
|
332
|
-
body.append({"kind": "row", "fields": [self._field(question, relations, title_id, cover_id, field_names) for question in row if isinstance(question, dict)]})
|
|
460
|
+
body.append({"kind": "row", "fields": [self._field(question, relations, select_scope_relations, title_id, cover_id, field_names) for question in row if isinstance(question, dict)]})
|
|
333
461
|
resource = {
|
|
334
462
|
"apiVersion": context.api_version,
|
|
335
463
|
"appKey": app_key,
|
|
@@ -349,6 +477,7 @@ class AppFormNormalizer:
|
|
|
349
477
|
self,
|
|
350
478
|
question: JSONObject,
|
|
351
479
|
relations: list[JSONObject],
|
|
480
|
+
select_scope_relations: list[JSONObject],
|
|
352
481
|
title_id: int | None,
|
|
353
482
|
cover_id: int | None,
|
|
354
483
|
field_names: dict[int, str],
|
|
@@ -381,6 +510,23 @@ class AppFormNormalizer:
|
|
|
381
510
|
"required": bool(question.get("required")),
|
|
382
511
|
"description": question.get("queHint") or None,
|
|
383
512
|
}
|
|
513
|
+
default_type = _positive_int(question.get("queDefaultType"))
|
|
514
|
+
if default_type is not None and (
|
|
515
|
+
question.get("queDefaultValue") is not None
|
|
516
|
+
or (default_type != 1 and field_type != "relation")
|
|
517
|
+
):
|
|
518
|
+
field["defaultType"] = default_type
|
|
519
|
+
raw_default_value = question.get("queDefaultValue")
|
|
520
|
+
if isinstance(raw_default_value, (dict, list)) and field_type not in {"address", "attachment", "relation", "subtable"}:
|
|
521
|
+
raise AppFormFailure(
|
|
522
|
+
"UNSUPPORTED_FIELD_CONFIG",
|
|
523
|
+
"structured default value must be declared through the field-specific config",
|
|
524
|
+
"NORMALIZE",
|
|
525
|
+
details={"queId": que_id, "type": field_type},
|
|
526
|
+
)
|
|
527
|
+
if "queDefaultValue" in question and raw_default_value is not None and not isinstance(raw_default_value, (dict, list)):
|
|
528
|
+
default_value = question.get("queDefaultValue")
|
|
529
|
+
field["defaultValue"] = _decode_formula_default(default_value) if default_type == 3 else deepcopy(default_value)
|
|
384
530
|
if que_id is not None:
|
|
385
531
|
field["queId"] = que_id
|
|
386
532
|
width = _positive_int(question.get("queWidth"))
|
|
@@ -390,28 +536,123 @@ class AppFormNormalizer:
|
|
|
390
536
|
field["dataTitle"] = True
|
|
391
537
|
if cover_id is not None and que_id == cover_id:
|
|
392
538
|
field["dataCover"] = True
|
|
393
|
-
if field_type in
|
|
539
|
+
if field_type in {"text", "long_text", "link", "phone", "email"}:
|
|
540
|
+
config = _without_none({
|
|
541
|
+
"minWordCount": _canonical_unlimited(question.get("minWordCount")),
|
|
542
|
+
"maxWordCount": _canonical_unlimited(question.get("maxWordCount")),
|
|
543
|
+
"noRepeat": question.get("noRepeat"),
|
|
544
|
+
"canScan": question.get("canScan"),
|
|
545
|
+
"scanType": question.get("scanType"),
|
|
546
|
+
})
|
|
547
|
+
if field_type == "phone":
|
|
548
|
+
config.update(_without_none({
|
|
549
|
+
"msgValidateStatus": question.get("msgValidateStatus"),
|
|
550
|
+
"fixedPhone": question.get("fixedPhone"),
|
|
551
|
+
}))
|
|
552
|
+
if config:
|
|
553
|
+
field["config"] = config
|
|
554
|
+
elif field_type in {"number", "amount"}:
|
|
555
|
+
config = _without_none({
|
|
556
|
+
"numberLimitStatus": question.get("numberLimitStatus"),
|
|
557
|
+
"minValue": question.get("minValue"),
|
|
558
|
+
"maxValue": question.get("maxValue"),
|
|
559
|
+
"decimalDigit": question.get("decimalDigit"),
|
|
560
|
+
"numberFormat": question.get("numberFormat"),
|
|
561
|
+
"currencyStatus": question.get("currencyStatus"),
|
|
562
|
+
"currencyType": question.get("currencyType"),
|
|
563
|
+
"canDecimal": question.get("canDecimal"),
|
|
564
|
+
"noRepeat": question.get("noRepeat"),
|
|
565
|
+
"canScan": question.get("canScan"),
|
|
566
|
+
"scanType": question.get("scanType"),
|
|
567
|
+
})
|
|
568
|
+
if config:
|
|
569
|
+
field["config"] = config
|
|
570
|
+
elif field_type in CHOICE_TYPES:
|
|
394
571
|
options = [str(item.get("optValue")) for item in question.get("options") or [] if isinstance(item, dict) and item.get("optValue") is not None]
|
|
395
572
|
if not options and field_type == "single_choice":
|
|
396
573
|
options = ["是", "否"]
|
|
397
|
-
field["config"] = {
|
|
574
|
+
field["config"] = _without_none({
|
|
575
|
+
"options": options,
|
|
576
|
+
"minOpts": _canonical_unlimited(question.get("minOpts")),
|
|
577
|
+
"maxOpts": _canonical_unlimited(question.get("maxOpts")),
|
|
578
|
+
"optDirection": question.get("optDirection") if question.get("optDirection") not in {None, 0} else None,
|
|
579
|
+
"optSelectMode": question.get("optSelectMode"),
|
|
580
|
+
"beingShowColor": question.get("beingShowColor"),
|
|
581
|
+
"defaultOptions": [str(item.get("optValue")) for item in question.get("options") or [] if isinstance(item, dict) and item.get("beingDefault") is True],
|
|
582
|
+
})
|
|
583
|
+
if not field["config"].get("defaultOptions"):
|
|
584
|
+
field["config"].pop("defaultOptions", None)
|
|
585
|
+
elif field_type == "member":
|
|
586
|
+
field["config"] = {
|
|
587
|
+
"memberScope": _canonical_member_scope(question.get("memberSelectScope"), question.get("memberSelectScopeType")),
|
|
588
|
+
}
|
|
589
|
+
if question.get("memberDftValue") is not None:
|
|
590
|
+
field["config"]["defaultMembers"] = _canonical_member_scope(question.get("memberDftValue"), 2)
|
|
591
|
+
relation = next((item for item in select_scope_relations if _positive_int(item.get("queId")) == que_id), None)
|
|
592
|
+
scope_relation = _canonical_scope_relation(relation)
|
|
593
|
+
if scope_relation is not None:
|
|
594
|
+
field["config"]["memberScopeRelation"] = scope_relation
|
|
595
|
+
for key in ("noRepeat", "canScan", "scanType"):
|
|
596
|
+
if question.get(key) is not None:
|
|
597
|
+
field["config"][key] = question[key]
|
|
398
598
|
elif field_type == "department":
|
|
399
599
|
scope = question.get("deptSelectScope") if isinstance(question.get("deptSelectScope"), dict) else {}
|
|
400
600
|
departments = scope.get("depart") if isinstance(scope.get("depart"), list) else []
|
|
401
|
-
|
|
601
|
+
canonical_departments = [
|
|
602
|
+
_without_none({"deptId": _positive_int(item.get("deptId") or item.get("id")), "deptName": item.get("deptName") or item.get("name")})
|
|
603
|
+
for item in departments
|
|
604
|
+
if isinstance(item, dict) and _positive_int(item.get("deptId") or item.get("id")) is not None
|
|
605
|
+
]
|
|
606
|
+
scope_type = _positive_int(question.get("deptSelectScopeType")) or 1
|
|
607
|
+
if scope_type == 3:
|
|
608
|
+
field["config"] = {"departmentScope": {"mode": "relation", "includeSubDeparts": bool(scope.get("includeSubDeparts"))}}
|
|
609
|
+
elif scope_type == 2 or (question.get("deptSelectScopeType") is None and canonical_departments):
|
|
610
|
+
if not canonical_departments:
|
|
611
|
+
raise AppFormFailure(
|
|
612
|
+
"UNSUPPORTED_FIELD_CONFIG",
|
|
613
|
+
"department custom scope has no departments",
|
|
614
|
+
"NORMALIZE",
|
|
615
|
+
details={"queId": que_id},
|
|
616
|
+
)
|
|
402
617
|
field["config"] = {
|
|
403
618
|
"departmentScope": {
|
|
404
619
|
"mode": "custom",
|
|
405
|
-
"departments":
|
|
406
|
-
_without_none({"deptId": _positive_int(item.get("deptId") or item.get("id")), "deptName": item.get("deptName") or item.get("name")})
|
|
407
|
-
for item in departments
|
|
408
|
-
if isinstance(item, dict) and _positive_int(item.get("deptId") or item.get("id")) is not None
|
|
409
|
-
],
|
|
620
|
+
"departments": canonical_departments,
|
|
410
621
|
"includeSubDeparts": bool(scope.get("includeSubDeparts")),
|
|
411
622
|
}
|
|
412
623
|
}
|
|
413
624
|
else:
|
|
414
625
|
field["config"] = {"departmentScope": {"mode": "all", "includeSubDeparts": bool(scope.get("includeSubDeparts"))}}
|
|
626
|
+
if question.get("deptDftValue") is not None:
|
|
627
|
+
field["config"]["defaultDepartments"] = _canonical_member_scope(question.get("deptDftValue"), 2, department=True)
|
|
628
|
+
relation = next((item for item in select_scope_relations if _positive_int(item.get("queId")) == que_id), None)
|
|
629
|
+
scope_relation = _canonical_scope_relation(relation)
|
|
630
|
+
if scope_relation is not None:
|
|
631
|
+
field["config"]["departmentScopeRelation"] = scope_relation
|
|
632
|
+
for key in ("noRepeat", "canScan", "scanType", "beingShowParentPath"):
|
|
633
|
+
if question.get(key) is not None:
|
|
634
|
+
field["config"][key] = question[key]
|
|
635
|
+
elif field_type == "attachment":
|
|
636
|
+
field["config"] = _without_none({
|
|
637
|
+
"fileSize": question.get("fileSize"),
|
|
638
|
+
"fileType": question.get("fileType"),
|
|
639
|
+
"fileBeingCaptureOnly": question.get("fileBeingCaptureOnly"),
|
|
640
|
+
"imageCompress": question.get("imageCompress"),
|
|
641
|
+
"pluginStatus": question.get("pluginStatus"),
|
|
642
|
+
"maxFileLimitStatus": question.get("maxFileLimitStatus"),
|
|
643
|
+
"maxFileLimit": question.get("maxFileLimit"),
|
|
644
|
+
"canScan": question.get("canScan"),
|
|
645
|
+
"scanType": question.get("scanType"),
|
|
646
|
+
"defaultValues": question.get("queDefaultValues") if isinstance(question.get("queDefaultValues"), dict) and (question.get("queDefaultValues", {}).get("values") or question.get("queDefaultValues", {}).get("tableValues")) else None,
|
|
647
|
+
})
|
|
648
|
+
elif field_type == "address":
|
|
649
|
+
field["config"] = _without_none({
|
|
650
|
+
"addressPrecision": question.get("addressPrecision"),
|
|
651
|
+
"pickPosition": question.get("pickPosition"),
|
|
652
|
+
"canScan": question.get("canScan"),
|
|
653
|
+
"scanType": question.get("scanType"),
|
|
654
|
+
"defaultValues": question.get("queDefaultValues") if isinstance(question.get("queDefaultValues"), dict) and (question.get("queDefaultValues", {}).get("values") or question.get("queDefaultValues", {}).get("tableValues")) else None,
|
|
655
|
+
})
|
|
415
656
|
elif field_type == "relation":
|
|
416
657
|
reference = question.get("referenceConfig") if isinstance(question.get("referenceConfig"), dict) else {}
|
|
417
658
|
refer_questions = [item for item in reference.get("referQuestions") or [] if isinstance(item, dict)]
|
|
@@ -437,6 +678,9 @@ class AppFormNormalizer:
|
|
|
437
678
|
)
|
|
438
679
|
for item in visible
|
|
439
680
|
],
|
|
681
|
+
"beingDefaultFormulaAutoFillEnabled": reference.get("beingDefaultFormulaAutoFillEnabled"),
|
|
682
|
+
"defaultValueMatchRules": reference.get("defaultValueMatchRules"),
|
|
683
|
+
"defaultValues": question.get("queDefaultValues") if isinstance(question.get("queDefaultValues"), dict) else None,
|
|
440
684
|
}
|
|
441
685
|
)
|
|
442
686
|
elif field_type == "q_linker":
|
|
@@ -447,7 +691,7 @@ class AppFormNormalizer:
|
|
|
447
691
|
field["config"] = self._data_relation_config(question, relations)
|
|
448
692
|
elif field_type == "subtable":
|
|
449
693
|
field["subfields"] = [
|
|
450
|
-
self._field(item, relations, None, None, field_names)
|
|
694
|
+
self._field(item, relations, select_scope_relations, None, None, field_names)
|
|
451
695
|
for item in question.get("subQuestions") or []
|
|
452
696
|
if isinstance(item, dict)
|
|
453
697
|
]
|
|
@@ -455,6 +699,13 @@ class AppFormNormalizer:
|
|
|
455
699
|
subfield.pop("width", None)
|
|
456
700
|
subfield.pop("dataTitle", None)
|
|
457
701
|
subfield.pop("dataCover", None)
|
|
702
|
+
field["config"] = _without_none({
|
|
703
|
+
"tableRowsLimit": question.get("tableRowsLimit"),
|
|
704
|
+
"tableNoRepeatStatus": question.get("tableNoRepeatStatus"),
|
|
705
|
+
"tableNoRepeatQuestion": [_selector(item, field_names.get(_positive_int(item))) for item in question.get("tableNoRepeatQuestion") or [] if _positive_int(item) is not None],
|
|
706
|
+
"tableRowHeight": question.get("tableRowHeight"),
|
|
707
|
+
"defaultValues": question.get("queDefaultValues") if isinstance(question.get("queDefaultValues"), dict) and (question.get("queDefaultValues", {}).get("values") or question.get("queDefaultValues", {}).get("tableValues")) else None,
|
|
708
|
+
})
|
|
458
709
|
return field
|
|
459
710
|
|
|
460
711
|
@staticmethod
|
|
@@ -958,9 +1209,7 @@ class AppFormCompiler:
|
|
|
958
1209
|
fields = list(_iter_fields(body))
|
|
959
1210
|
templates = self._question_templates(current_form or {})
|
|
960
1211
|
internal_fields = [self._internal_field(field, templates=templates) for field in fields]
|
|
961
|
-
|
|
962
|
-
if _positive_int(internal.get("que_id")) is None:
|
|
963
|
-
internal["que_temp_id"] = -10000 - (index * 100)
|
|
1212
|
+
self._assign_layout_temp_ids(body, internal_fields)
|
|
964
1213
|
try:
|
|
965
1214
|
internal_fields, _ = _normalize_and_validate_code_block_fields_for_write(fields=internal_fields)
|
|
966
1215
|
internal_fields, relations = _compile_code_block_binding_fields(fields=internal_fields, current_schema=current_form or {})
|
|
@@ -992,6 +1241,84 @@ class AppFormCompiler:
|
|
|
992
1241
|
self._data_relation_custom_button(config.get("customButton") or {}, template)
|
|
993
1242
|
self._data_relation_relations(profile, app_key, fields, internal_fields)
|
|
994
1243
|
|
|
1244
|
+
@staticmethod
|
|
1245
|
+
def _assign_layout_temp_ids(body: list[Any], internal_fields: list[JSONObject]) -> None:
|
|
1246
|
+
"""Assign one temporary-ID sequence to fields and new section wrappers."""
|
|
1247
|
+
temp_id = -10000
|
|
1248
|
+
internal_index = 0
|
|
1249
|
+
|
|
1250
|
+
def assign_field(field: Any) -> None:
|
|
1251
|
+
nonlocal temp_id, internal_index
|
|
1252
|
+
if not isinstance(field, dict):
|
|
1253
|
+
return
|
|
1254
|
+
if internal_index >= len(internal_fields):
|
|
1255
|
+
raise AppFormFailure(
|
|
1256
|
+
"INVALID_SPEC",
|
|
1257
|
+
"form layout and internal field list are inconsistent",
|
|
1258
|
+
"COMPILE",
|
|
1259
|
+
)
|
|
1260
|
+
internal = internal_fields[internal_index]
|
|
1261
|
+
internal_index += 1
|
|
1262
|
+
if _positive_int(field.get("queId")) is None:
|
|
1263
|
+
internal["que_temp_id"] = temp_id
|
|
1264
|
+
temp_id -= 100
|
|
1265
|
+
|
|
1266
|
+
for block in body:
|
|
1267
|
+
if not isinstance(block, dict):
|
|
1268
|
+
continue
|
|
1269
|
+
if block.get("kind") == "row":
|
|
1270
|
+
for field in block.get("fields") or []:
|
|
1271
|
+
assign_field(field)
|
|
1272
|
+
continue
|
|
1273
|
+
for row in block.get("rows") or []:
|
|
1274
|
+
if not isinstance(row, dict):
|
|
1275
|
+
continue
|
|
1276
|
+
for field in row.get("fields") or []:
|
|
1277
|
+
assign_field(field)
|
|
1278
|
+
if _positive_int(block.get("sectionId")) is None:
|
|
1279
|
+
temp_id -= 100
|
|
1280
|
+
|
|
1281
|
+
if internal_index != len(internal_fields):
|
|
1282
|
+
raise AppFormFailure(
|
|
1283
|
+
"INVALID_SPEC",
|
|
1284
|
+
"form layout and internal field list are inconsistent",
|
|
1285
|
+
"COMPILE",
|
|
1286
|
+
)
|
|
1287
|
+
|
|
1288
|
+
@staticmethod
|
|
1289
|
+
def _validate_unique_temp_ids(form_ques: list[list[JSONObject]]) -> None:
|
|
1290
|
+
seen: dict[int, str] = {}
|
|
1291
|
+
|
|
1292
|
+
def visit(question: Any) -> None:
|
|
1293
|
+
if not isinstance(question, dict):
|
|
1294
|
+
return
|
|
1295
|
+
temp_id = question.get("queTempId")
|
|
1296
|
+
if temp_id is not None:
|
|
1297
|
+
try:
|
|
1298
|
+
normalized = int(temp_id)
|
|
1299
|
+
except (TypeError, ValueError):
|
|
1300
|
+
normalized = None
|
|
1301
|
+
if normalized is not None:
|
|
1302
|
+
title = str(question.get("queTitle") or "")
|
|
1303
|
+
previous = seen.get(normalized)
|
|
1304
|
+
if previous is not None:
|
|
1305
|
+
raise AppFormFailure(
|
|
1306
|
+
"DUPLICATE_TEMP_ID",
|
|
1307
|
+
"form payload contains duplicate queTempId",
|
|
1308
|
+
"COMPILE",
|
|
1309
|
+
details={"queTempId": normalized, "questions": [previous, title]},
|
|
1310
|
+
)
|
|
1311
|
+
seen[normalized] = title
|
|
1312
|
+
for subquestion in question.get("subQuestions") or []:
|
|
1313
|
+
visit(subquestion)
|
|
1314
|
+
for inner_row in question.get("innerQuestions") or []:
|
|
1315
|
+
for inner_question in inner_row if isinstance(inner_row, list) else []:
|
|
1316
|
+
visit(inner_question)
|
|
1317
|
+
|
|
1318
|
+
for row in form_ques:
|
|
1319
|
+
for question in row if isinstance(row, list) else []:
|
|
1320
|
+
visit(question)
|
|
1321
|
+
|
|
995
1322
|
def _target_body(self, current_body: list[Any], declared_body: list[Any]) -> list[JSONObject]:
|
|
996
1323
|
current_fields = list(_iter_fields(current_body))
|
|
997
1324
|
by_id = {_positive_int(field.get("queId")): field for field in current_fields if _positive_int(field.get("queId"))}
|
|
@@ -1012,6 +1339,12 @@ class AppFormCompiler:
|
|
|
1012
1339
|
target["config"] = deepcopy(declared["config"])
|
|
1013
1340
|
target.setdefault("required", False)
|
|
1014
1341
|
target.setdefault("description", None)
|
|
1342
|
+
if target.get("defaultValue") is not None:
|
|
1343
|
+
target.setdefault("defaultType", 1)
|
|
1344
|
+
if "defaultValue" in declared and declared.get("defaultValue") is None:
|
|
1345
|
+
target.pop("defaultType", None)
|
|
1346
|
+
target.pop("defaultValue", None)
|
|
1347
|
+
target["_clear_default_value"] = True
|
|
1015
1348
|
if current is not None:
|
|
1016
1349
|
target["queId"] = current.get("queId")
|
|
1017
1350
|
else:
|
|
@@ -1157,6 +1490,7 @@ class AppFormCompiler:
|
|
|
1157
1490
|
target_fields = list(_iter_fields(target["spec"]["body"]))
|
|
1158
1491
|
templates = self._question_templates(current_form)
|
|
1159
1492
|
internal_fields = [self._internal_field(field, templates=templates) for field in target_fields]
|
|
1493
|
+
self._assign_layout_temp_ids(target["spec"]["body"], internal_fields)
|
|
1160
1494
|
try:
|
|
1161
1495
|
internal_fields, _ = _normalize_and_validate_code_block_fields_for_write(fields=internal_fields)
|
|
1162
1496
|
internal_fields, relations = _compile_code_block_binding_fields(fields=internal_fields, current_schema=current_form)
|
|
@@ -1176,6 +1510,12 @@ class AppFormCompiler:
|
|
|
1176
1510
|
for relation in relations
|
|
1177
1511
|
if _positive_int(relation.get("relationType")) != 1
|
|
1178
1512
|
]
|
|
1513
|
+
payload["selectScopeRelations"] = [
|
|
1514
|
+
self._select_scope_relation(profile, field, internal)
|
|
1515
|
+
for field, internal in _iter_field_pairs(target_fields, internal_fields)
|
|
1516
|
+
if isinstance(field.get("config"), dict)
|
|
1517
|
+
and (field.get("config", {}).get("memberScopeRelation") or field.get("config", {}).get("departmentScopeRelation"))
|
|
1518
|
+
]
|
|
1179
1519
|
rows: list[list[JSONObject]] = []
|
|
1180
1520
|
temp_id = -10000
|
|
1181
1521
|
for block in target["spec"]["body"]:
|
|
@@ -1212,11 +1552,13 @@ class AppFormCompiler:
|
|
|
1212
1552
|
temp_id -= 100
|
|
1213
1553
|
rows.append([section])
|
|
1214
1554
|
payload["formQues"] = rows
|
|
1555
|
+
self._validate_unique_temp_ids(rows)
|
|
1215
1556
|
data_title = next((field for field in target_fields if field.get("dataTitle")), None)
|
|
1216
1557
|
data_cover = next((field for field in target_fields if field.get("dataCover")), None)
|
|
1217
1558
|
payload["dataTitleQueId"] = self._field_runtime_id(data_title, internal_fields)
|
|
1218
1559
|
payload["dataCoverQueId"] = self._field_runtime_id(data_cover, internal_fields) if data_cover else None
|
|
1219
1560
|
payload["questionRelations"].extend(data_relations)
|
|
1561
|
+
_normalize_formula_defaults_for_save(payload.get("formQues"))
|
|
1220
1562
|
return payload
|
|
1221
1563
|
|
|
1222
1564
|
@staticmethod
|
|
@@ -1377,6 +1719,15 @@ class AppFormCompiler:
|
|
|
1377
1719
|
"subfields": [self._internal_field(item, templates=templates) for item in field.get("subfields") or []],
|
|
1378
1720
|
"_app_form_type": canonical_type,
|
|
1379
1721
|
}
|
|
1722
|
+
if "defaultValues" in config:
|
|
1723
|
+
result["default_values"] = deepcopy(config.get("defaultValues"))
|
|
1724
|
+
if "defaultType" in field:
|
|
1725
|
+
result["default_type"] = _positive_int(field.get("defaultType")) or 1
|
|
1726
|
+
if "defaultValue" in field:
|
|
1727
|
+
result["default_value"] = deepcopy(field.get("defaultValue"))
|
|
1728
|
+
if field.get("_clear_default_value") is True:
|
|
1729
|
+
result["default_type"] = 1
|
|
1730
|
+
result["default_value"] = None
|
|
1380
1731
|
template = templates.get(result["que_id"] or -1)
|
|
1381
1732
|
if template is not None:
|
|
1382
1733
|
result["_question_template"] = deepcopy(template)
|
|
@@ -1390,6 +1741,19 @@ class AppFormCompiler:
|
|
|
1390
1741
|
],
|
|
1391
1742
|
"include_sub_departs": bool(scope.get("includeSubDeparts")),
|
|
1392
1743
|
}
|
|
1744
|
+
default_departments = config.get("defaultDepartments")
|
|
1745
|
+
if isinstance(default_departments, dict):
|
|
1746
|
+
result["department_default_scope"] = {
|
|
1747
|
+
"departments": [
|
|
1748
|
+
{"dept_id": item.get("deptId"), "dept_name": item.get("deptName")}
|
|
1749
|
+
for item in default_departments.get("departments") or []
|
|
1750
|
+
if isinstance(item, dict)
|
|
1751
|
+
],
|
|
1752
|
+
"include_sub_departs": bool(default_departments.get("includeSubDeparts")),
|
|
1753
|
+
}
|
|
1754
|
+
elif canonical_type == "member":
|
|
1755
|
+
result["member_scope"] = deepcopy(config.get("memberScope"))
|
|
1756
|
+
result["member_default_scope"] = deepcopy(config.get("defaultMembers"))
|
|
1393
1757
|
elif canonical_type == "relation":
|
|
1394
1758
|
result.update(
|
|
1395
1759
|
{
|
|
@@ -1397,6 +1761,8 @@ class AppFormCompiler:
|
|
|
1397
1761
|
"relation_mode": config.get("relationMode") or "single",
|
|
1398
1762
|
"display_field": _snake_selector(config.get("displayField")),
|
|
1399
1763
|
"visible_fields": [_snake_selector(item) for item in config.get("visibleFields") or []],
|
|
1764
|
+
"reference_default_formula_auto_fill": config.get("beingDefaultFormulaAutoFillEnabled"),
|
|
1765
|
+
"reference_default_value_match_rules": deepcopy(config.get("defaultValueMatchRules")),
|
|
1400
1766
|
"_relation_config_explicit": True,
|
|
1401
1767
|
}
|
|
1402
1768
|
)
|
|
@@ -1443,6 +1809,87 @@ class AppFormCompiler:
|
|
|
1443
1809
|
"queHint": field.get("description") or "",
|
|
1444
1810
|
}
|
|
1445
1811
|
)
|
|
1812
|
+
config = field.get("config") if isinstance(field.get("config"), dict) else {}
|
|
1813
|
+
if field_type in {"text", "long_text", "link", "phone", "email"}:
|
|
1814
|
+
for key in ("minWordCount", "maxWordCount", "noRepeat", "canScan", "scanType"):
|
|
1815
|
+
if key in config:
|
|
1816
|
+
question[key] = _backend_unlimited(config[key]) if key in {"minWordCount", "maxWordCount"} else config[key]
|
|
1817
|
+
if field_type == "phone":
|
|
1818
|
+
for key in ("msgValidateStatus", "fixedPhone"):
|
|
1819
|
+
if key in config:
|
|
1820
|
+
question[key] = config[key]
|
|
1821
|
+
elif field_type in {"number", "amount"}:
|
|
1822
|
+
for key in ("numberLimitStatus", "minValue", "maxValue", "decimalDigit", "numberFormat", "currencyStatus", "currencyType", "canDecimal", "noRepeat", "canScan", "scanType"):
|
|
1823
|
+
if key in config:
|
|
1824
|
+
question[key] = config[key]
|
|
1825
|
+
if field_type == "amount" and "canDecimal" not in question:
|
|
1826
|
+
question["canDecimal"] = True
|
|
1827
|
+
elif field_type in CHOICE_TYPES:
|
|
1828
|
+
for key in ("minOpts", "maxOpts", "optDirection", "optSelectMode", "beingShowColor"):
|
|
1829
|
+
if key in config:
|
|
1830
|
+
question[key] = _backend_unlimited(config[key]) if key in {"minOpts", "maxOpts"} else config[key]
|
|
1831
|
+
elif field_type == "member":
|
|
1832
|
+
scope = config.get("memberScope")
|
|
1833
|
+
default_scope = config.get("defaultMembers")
|
|
1834
|
+
if isinstance(scope, dict):
|
|
1835
|
+
question["memberSelectScopeType"] = {"all": 1, "custom": 2, "relation": 3}.get(scope.get("mode"), 1)
|
|
1836
|
+
question["memberSelectScope"] = _backend_member_scope(scope)
|
|
1837
|
+
if isinstance(default_scope, dict):
|
|
1838
|
+
question["memberDftValue"] = _backend_member_scope(default_scope)
|
|
1839
|
+
for key in ("noRepeat", "canScan", "scanType"):
|
|
1840
|
+
if key in config:
|
|
1841
|
+
question[key] = config[key]
|
|
1842
|
+
elif field_type == "department":
|
|
1843
|
+
scope = config.get("departmentScope") if isinstance(config.get("departmentScope"), dict) else None
|
|
1844
|
+
if scope is not None:
|
|
1845
|
+
question["deptSelectScopeType"] = {"all": 1, "custom": 2, "relation": 3}.get(scope.get("mode"), 1)
|
|
1846
|
+
question["deptSelectScope"] = {
|
|
1847
|
+
"depart": [
|
|
1848
|
+
_without_none({"deptId": item.get("deptId"), "deptName": item.get("deptName")})
|
|
1849
|
+
for item in scope.get("departments") or []
|
|
1850
|
+
if isinstance(item, dict)
|
|
1851
|
+
],
|
|
1852
|
+
"includeSubDeparts": bool(scope.get("includeSubDeparts")),
|
|
1853
|
+
}
|
|
1854
|
+
default_scope = config.get("defaultDepartments")
|
|
1855
|
+
if isinstance(default_scope, dict):
|
|
1856
|
+
question["deptDftValue"] = {
|
|
1857
|
+
"depart": [
|
|
1858
|
+
_without_none({"deptId": item.get("deptId"), "deptName": item.get("deptName")})
|
|
1859
|
+
for item in default_scope.get("departments") or []
|
|
1860
|
+
if isinstance(item, dict)
|
|
1861
|
+
],
|
|
1862
|
+
"includeSubDeparts": bool(default_scope.get("includeSubDeparts")),
|
|
1863
|
+
}
|
|
1864
|
+
for key in ("noRepeat", "canScan", "scanType", "beingShowParentPath"):
|
|
1865
|
+
if key in config:
|
|
1866
|
+
question[key] = config[key]
|
|
1867
|
+
elif field_type == "attachment":
|
|
1868
|
+
for key in ("fileSize", "fileType", "fileBeingCaptureOnly", "imageCompress", "pluginStatus", "maxFileLimitStatus", "maxFileLimit", "canScan", "scanType"):
|
|
1869
|
+
if key in config:
|
|
1870
|
+
question[key] = deepcopy(config[key])
|
|
1871
|
+
elif field_type == "address":
|
|
1872
|
+
for key in ("addressPrecision", "pickPosition", "canScan", "scanType"):
|
|
1873
|
+
if key in config:
|
|
1874
|
+
question[key] = config[key]
|
|
1875
|
+
elif field_type == "subtable":
|
|
1876
|
+
for key in ("tableRowsLimit", "tableNoRepeatStatus", "tableRowHeight"):
|
|
1877
|
+
if key in config:
|
|
1878
|
+
question[key] = deepcopy(config[key])
|
|
1879
|
+
if isinstance(config.get("tableNoRepeatQuestion"), list):
|
|
1880
|
+
question["tableNoRepeatQuestion"] = [
|
|
1881
|
+
self._resolve_subfield_runtime_id(item, internal.get("subfields") or [], "tableNoRepeatQuestion")
|
|
1882
|
+
for item in config["tableNoRepeatQuestion"]
|
|
1883
|
+
]
|
|
1884
|
+
if "defaultValues" in config:
|
|
1885
|
+
question["queDefaultValues"] = deepcopy(config.get("defaultValues"))
|
|
1886
|
+
if "defaultType" in field:
|
|
1887
|
+
question["queDefaultType"] = _positive_int(field.get("defaultType")) or 1
|
|
1888
|
+
if "defaultValue" in field:
|
|
1889
|
+
question["queDefaultValue"] = deepcopy(field.get("defaultValue"))
|
|
1890
|
+
if field.get("_clear_default_value") is True:
|
|
1891
|
+
question["queDefaultType"] = 1
|
|
1892
|
+
question["queDefaultValue"] = None
|
|
1446
1893
|
if field_type == "date":
|
|
1447
1894
|
question["dateType"] = 0
|
|
1448
1895
|
elif field_type == "datetime":
|
|
@@ -1460,7 +1907,10 @@ class AppFormCompiler:
|
|
|
1460
1907
|
option.update({key: deepcopy(value) for key, value in old.items() if key not in {"optValue"}})
|
|
1461
1908
|
question["options"] = options
|
|
1462
1909
|
question["unnamedOptions"] = [item.get("optValue") for item in options]
|
|
1463
|
-
|
|
1910
|
+
default_options = (field.get("config") or {}).get("defaultOptions")
|
|
1911
|
+
if isinstance(default_options, list):
|
|
1912
|
+
for option in options:
|
|
1913
|
+
option["beingDefault"] = str(option.get("optValue")) in {str(item) for item in default_options}
|
|
1464
1914
|
elif field_type == "subtable":
|
|
1465
1915
|
question["subQuestions"] = [
|
|
1466
1916
|
self._question(profile, app_key, subfield, sub_internal, temp_id - ((index + 1) * 10), relations)
|
|
@@ -1522,10 +1972,56 @@ class AppFormCompiler:
|
|
|
1522
1972
|
for index, item in enumerate(visible)
|
|
1523
1973
|
],
|
|
1524
1974
|
"referAuthQues": [{"queId": item["queId"], "queAuth": 1} for item in visible],
|
|
1975
|
+
"beingDefaultFormulaAutoFillEnabled": config.get("beingDefaultFormulaAutoFillEnabled"),
|
|
1976
|
+
"defaultValueMatchRules": deepcopy(config.get("defaultValueMatchRules")),
|
|
1525
1977
|
}
|
|
1526
1978
|
)
|
|
1979
|
+
if "defaultValues" in config:
|
|
1980
|
+
result["queDefaultValues"] = deepcopy(config.get("defaultValues"))
|
|
1527
1981
|
return result
|
|
1528
1982
|
|
|
1983
|
+
def _select_scope_relation(self, profile: str, field: JSONObject, internal: JSONObject) -> JSONObject:
|
|
1984
|
+
config = field.get("config") if isinstance(field.get("config"), dict) else {}
|
|
1985
|
+
relation = config.get("memberScopeRelation") or config.get("departmentScopeRelation")
|
|
1986
|
+
if not isinstance(relation, dict):
|
|
1987
|
+
raise AppFormFailure("UNSUPPORTED_FIELD_CONFIG", "select scope relation must be an object", "COMPILE")
|
|
1988
|
+
target_app_key = str(relation.get("targetAppKey") or "")
|
|
1989
|
+
if not target_app_key:
|
|
1990
|
+
raise AppFormFailure("UNSUPPORTED_FIELD_CONFIG", "select scope relation requires targetAppKey", "COMPILE")
|
|
1991
|
+
target_form = self._read_form(profile, target_app_key, being_draft=False)
|
|
1992
|
+
candidates = _backend_question_candidates(target_form)
|
|
1993
|
+
displayed = _resolve_backend_selector(relation.get("displayedField") or {}, candidates, "selectScopeRelation.displayedField")
|
|
1994
|
+
payload: JSONObject = {
|
|
1995
|
+
"queId": _positive_int(internal.get("que_id")) or int(internal.get("que_temp_id") or 0),
|
|
1996
|
+
"displayedQueId": _positive_int(displayed.get("queId")),
|
|
1997
|
+
"displayedQueInfo": {
|
|
1998
|
+
"appKey": target_app_key,
|
|
1999
|
+
"appTitle": relation.get("targetAppTitle") or target_form.get("formTitle") or "",
|
|
2000
|
+
"queId": _positive_int(displayed.get("queId")),
|
|
2001
|
+
"queTitle": displayed.get("queTitle") or "",
|
|
2002
|
+
"queType": _positive_int(displayed.get("queType")) or 2,
|
|
2003
|
+
},
|
|
2004
|
+
}
|
|
2005
|
+
for key in ("matchRuleType", "matchRules", "matchRuleFormula"):
|
|
2006
|
+
if key in relation:
|
|
2007
|
+
payload[key] = deepcopy(relation.get(key))
|
|
2008
|
+
return _without_none(payload)
|
|
2009
|
+
|
|
2010
|
+
@staticmethod
|
|
2011
|
+
def _resolve_subfield_runtime_id(selector: JSONObject, internal_fields: list[JSONObject], location: str) -> int:
|
|
2012
|
+
if not isinstance(selector, dict):
|
|
2013
|
+
raise AppFormFailure("INVALID_FIELD_SELECTOR", f"{location} must be an object", "COMPILE")
|
|
2014
|
+
que_id = _positive_int(selector.get("queId"))
|
|
2015
|
+
name = str(selector.get("name") or "")
|
|
2016
|
+
matches = [
|
|
2017
|
+
item for item in internal_fields
|
|
2018
|
+
if (que_id is None or _positive_int(item.get("que_id")) == que_id)
|
|
2019
|
+
and (not name or str(item.get("name") or "") == name)
|
|
2020
|
+
]
|
|
2021
|
+
if len(matches) != 1:
|
|
2022
|
+
raise AppFormFailure("INVALID_FIELD_SELECTOR", f"{location} must resolve to exactly one subfield", "COMPILE", details={"selector": selector, "matches": len(matches)})
|
|
2023
|
+
return _positive_int(matches[0].get("que_id")) or int(matches[0].get("que_temp_id") or 0)
|
|
2024
|
+
|
|
1529
2025
|
@staticmethod
|
|
1530
2026
|
def _data_relation_custom_button(config: JSONObject, template: JSONObject | None) -> JSONObject:
|
|
1531
2027
|
current = (template or {}).get("customButtonConfig")
|
|
@@ -1823,7 +2319,7 @@ class AppFormCompiler:
|
|
|
1823
2319
|
|
|
1824
2320
|
@staticmethod
|
|
1825
2321
|
def _save_base(current: JSONObject) -> JSONObject:
|
|
1826
|
-
read_only = {"appKey", "formId", "baseQues", "applyAuthType", "canCurrentApply", "wsType", "accountLevel", "collectionStatus", "editVersionNo", "formQues", "questionRelations"}
|
|
2322
|
+
read_only = {"appKey", "formId", "baseQues", "applyAuthType", "canCurrentApply", "wsType", "accountLevel", "collectionStatus", "editVersionNo", "formQues", "questionRelations", "selectScopeRelations"}
|
|
1827
2323
|
return {key: deepcopy(value) for key, value in current.items() if key not in read_only}
|
|
1828
2324
|
|
|
1829
2325
|
def _apply_form_settings(self, payload: JSONObject, *, current_form: JSONObject, target_spec: JSONObject, internal_fields: list[JSONObject]) -> None:
|
|
@@ -2664,17 +3160,34 @@ class AppFormReconciler:
|
|
|
2664
3160
|
|
|
2665
3161
|
@staticmethod
|
|
2666
3162
|
def _failure(error_code: str, message: str, phase: str, *, status: str = "failed", details: JSONObject | None = None, diagnostics: JSONObject | None = None, app_key: str | None = None, warnings: list[JSONObject] | None = None) -> JSONObject:
|
|
3163
|
+
output_diagnostics = deepcopy(diagnostics or {})
|
|
3164
|
+
for key in ("diff", "remainingDiff"):
|
|
3165
|
+
if key in output_diagnostics:
|
|
3166
|
+
output_diagnostics[key] = AppFormReconciler._compact_diff(output_diagnostics[key])
|
|
2667
3167
|
result = {
|
|
2668
3168
|
"status": status,
|
|
2669
3169
|
"error_code": error_code,
|
|
2670
3170
|
"recoverable": status != "failed" or error_code in {"PUBLISH_FAILED", "PACKAGE_ATTACH_RESULT_UNKNOWN"},
|
|
2671
3171
|
"message": message,
|
|
2672
|
-
**
|
|
3172
|
+
**output_diagnostics,
|
|
2673
3173
|
"phase": phase,
|
|
2674
3174
|
**({"appKey": app_key} if app_key else {}),
|
|
2675
|
-
**({"details": details} if details else {}),
|
|
2676
3175
|
**({"warnings": warnings} if warnings else {}),
|
|
2677
3176
|
}
|
|
3177
|
+
if details:
|
|
3178
|
+
# Apply diagnostics are already exposed at the top level. Keep only
|
|
3179
|
+
# additional error context in details to avoid returning the same
|
|
3180
|
+
# large diagnostic object twice.
|
|
3181
|
+
compact_details = {
|
|
3182
|
+
key: value
|
|
3183
|
+
for key, value in details.items()
|
|
3184
|
+
if key not in result
|
|
3185
|
+
}
|
|
3186
|
+
for key in ("diff", "remainingDiff"):
|
|
3187
|
+
if key in compact_details:
|
|
3188
|
+
compact_details[key] = AppFormReconciler._compact_diff(compact_details[key])
|
|
3189
|
+
if compact_details:
|
|
3190
|
+
result["details"] = compact_details
|
|
2678
3191
|
recovery = {
|
|
2679
3192
|
"EDIT_VERSION_CONFLICT": "read the latest draft, rebuild the complete declaration, validate, and retry once",
|
|
2680
3193
|
"EDIT_FINISHED_FAILED": "if published equals draft, release the current account's edit lock without publishing again",
|
|
@@ -2686,6 +3199,22 @@ class AppFormReconciler:
|
|
|
2686
3199
|
result["recoveryHint"] = recovery
|
|
2687
3200
|
return result
|
|
2688
3201
|
|
|
3202
|
+
@staticmethod
|
|
3203
|
+
def _compact_diff(diff: Any) -> Any:
|
|
3204
|
+
if not isinstance(diff, dict):
|
|
3205
|
+
return diff
|
|
3206
|
+
compact = deepcopy(diff)
|
|
3207
|
+
for key in ("added", "updated", "removed"):
|
|
3208
|
+
fields = compact.get(key)
|
|
3209
|
+
if not isinstance(fields, list):
|
|
3210
|
+
continue
|
|
3211
|
+
compact[key] = [
|
|
3212
|
+
str(field.get("name") or field.get("queId") or "unknown") if isinstance(field, dict) else str(field)
|
|
3213
|
+
for field in fields
|
|
3214
|
+
if isinstance(field, (dict, str, int))
|
|
3215
|
+
]
|
|
3216
|
+
return compact
|
|
3217
|
+
|
|
2689
3218
|
@classmethod
|
|
2690
3219
|
def _from_error(cls, error: AppFormContractError | AppFormFailure) -> JSONObject:
|
|
2691
3220
|
if isinstance(error, AppFormContractError):
|