@qingflow-tech/qingflow-app-builder-mcp 1.0.40 → 1.0.41
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 +2 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-builder/SKILL.md +14 -4
- package/skills/qingflow-app-builder/references/complete-system-development-guide.md +59 -0
- package/skills/qingflow-app-builder/references/create-app.md +3 -1
- package/skills/qingflow-app-builder/references/gotchas.md +6 -0
- package/skills/qingflow-app-builder/references/single-app-development-guide.md +47 -0
- package/skills/qingflow-app-builder/references/solution-playbooks.md +10 -0
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +2 -0
- package/src/qingflow_mcp/builder_facade/models.py +183 -0
- package/src/qingflow_mcp/builder_facade/service.py +722 -74
- package/src/qingflow_mcp/cli/formatters.py +1 -0
- package/src/qingflow_mcp/cli/main.py +2 -0
- package/src/qingflow_mcp/response_trim.py +1 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +388 -17
- package/src/qingflow_mcp/tools/record_tools.py +28 -2
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @qingflow-tech/qingflow-app-builder-mcp@1.0.
|
|
6
|
+
npm install @qingflow-tech/qingflow-app-builder-mcp@1.0.41
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @qingflow-tech/qingflow-app-builder-mcp@1.0.
|
|
12
|
+
npx -y -p @qingflow-tech/qingflow-app-builder-mcp@1.0.41 qingflow-app-builder-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -7,6 +7,8 @@ metadata:
|
|
|
7
7
|
|
|
8
8
|
# Qingflow App Builder
|
|
9
9
|
|
|
10
|
+
> **Skill 版本**:`qingflow-skills-2026.06.23.1`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
|
|
11
|
+
|
|
10
12
|
## Overview
|
|
11
13
|
|
|
12
14
|
This skill is for the current **public builder surface**, not for legacy low-level builder writes.
|
|
@@ -15,6 +17,7 @@ In Wingent Momo runtime, trust the injected MCP session, credentials, workspace,
|
|
|
15
17
|
Before any write or verification flow, identify whether the task targets `test` or `prod` and read [references/environments.md](references/environments.md). If the user did not specify one, default to `prod`.
|
|
16
18
|
|
|
17
19
|
Before choosing a route, skim the shared maintenance baseline: [public-surface-sync.md](references/public-surface-sync.md).
|
|
20
|
+
Then pick the matching development guide: [single-app-development-guide.md](references/single-app-development-guide.md) for one app, or [complete-system-development-guide.md](references/complete-system-development-guide.md) for app packages/systems.
|
|
18
21
|
|
|
19
22
|
## Current Public Mental Model
|
|
20
23
|
|
|
@@ -36,11 +39,13 @@ Default modeling rules:
|
|
|
36
39
|
|
|
37
40
|
Before any builder write, classify the request:
|
|
38
41
|
|
|
39
|
-
- **Complete system / app package**: the user asks for a system, package, workspace module set, or several related forms/apps. Use `package_apply` for the package, then one `app_schema_apply(package_id=..., apps=[...])` for the app shells and fields. Do not squeeze several business objects into one app.
|
|
42
|
+
- **Complete system / app package**: the user asks for a system, package, workspace module set, or several related forms/apps. Use `package_apply` for the package, then one `app_schema_apply(package_id=..., apps=[...])` for the app shells and fields. This is the main path for complete systems, not a bulk shortcut to abandon after the first slow response. Do not squeeze several business objects into one app.
|
|
40
43
|
- **Single app**: the user names one form/app or gives one `app_key`. Use `app_resolve`/`app_get`, then `app_schema_apply` and the app-scoped apply tools.
|
|
41
44
|
- **Record/user operation**: the user wants to add, edit, delete, approve, or analyze data. Route to the record/task skills instead of builder tools.
|
|
42
45
|
|
|
43
|
-
For complete systems, `apps[]` should use stable `client_key` values. Same-call relation fields
|
|
46
|
+
For complete systems, `apps[]` should use stable `client_key` values. Same-call relation fields should use `target_app_ref` for another `apps[].client_key`; use `target_app_key` only when the target app already exists or has been confirmed by readback. Create the related apps with one multi-app schema apply; do not create each app separately just to collect app keys and then patch relations afterward.
|
|
47
|
+
|
|
48
|
+
If a complete-system `app_schema_apply` times out, returns `partial_success`, returns `write_executed=true`, has `safe_to_retry=false`, or has incomplete readback, treat it as an uncertain write, not as a failed create. The next action is always `readback_before_retry`: read the package/app/fields first, compare intended `client_key`/`app_name`/relations against reality, and only then repair the missing slice. Do not retry the whole multi-app create, create `V2`/`测试`/random-suffix apps, or split the system into single-app rebuilds to bypass a duplicate/conflict.
|
|
44
49
|
|
|
45
50
|
Builder schema inputs should follow agent-intuitive semantics:
|
|
46
51
|
|
|
@@ -69,7 +74,8 @@ Treat these as the official surface. Do not default to `package_create`, `packag
|
|
|
69
74
|
- Multi-app schema work:
|
|
70
75
|
- use one `app_schema_apply(package_id=..., apps=[...])` / CLI `builder schema apply --apps-file` when creating several apps in one package
|
|
71
76
|
- every `apps[]` item should carry its own `client_key`, `app_name`, `icon`, `color`, and `add_fields`
|
|
72
|
-
- same-call relation fields
|
|
77
|
+
- same-call relation fields should use `target_app_ref` to point at another `apps[].client_key`; use `target_app` only when the app name is unique and stable, and use `target_app_key` only after the target app already exists or readback has confirmed it
|
|
78
|
+
- timeout / `partial_success` / `write_executed=true` / `safe_to_retry=false` means `readback_before_retry`; do not directly downgrade to single-app creation
|
|
73
79
|
- App base permissions:
|
|
74
80
|
- trust `app_get.editability.can_edit_app_base` for app base-info writes like app name, icon, and visibility
|
|
75
81
|
- `can_edit_form` only means schema/form-route capability; it no longer implies app base-info write capability
|
|
@@ -161,6 +167,8 @@ Treat these as the official surface. Do not default to `package_create`, `packag
|
|
|
161
167
|
- Do not guess package identity from a loose name. Public package work assumes a known `package_id`, or an explicit create/update intent through `package_apply`.
|
|
162
168
|
- Do not perform routine auth probes before every builder action in runtime contexts with injected MCP credentials; recover auth only after an actual auth/workspace failure.
|
|
163
169
|
- If `package_id` is unknown, derive it from related app/portal readback when possible; otherwise ask the user instead of falling back to hidden package lookup tools.
|
|
170
|
+
- Do not bypass package/app-name conflicts by inventing `V2`, `测试`, timestamp, or random suffix apps in a real business package. Read back and decide whether to update the existing app, create only a truly missing app, or ask the user.
|
|
171
|
+
- After any uncertain schema write, do `readback_before_retry` with `package_get` / `app_resolve` / `app_get_fields`; retry only the verified missing or failed slice.
|
|
164
172
|
- Do not use `package_create` or `package_attach_app` as a public default path. If they still appear in low-level code, treat them as internal/legacy implementation details.
|
|
165
173
|
- Do not use raw `portal_*` writes or raw `qingbi_report_*` writes as the default builder strategy.
|
|
166
174
|
- `app_schema_apply`, `app_layout_apply`, `app_flow_apply`, and `app_views_apply` publish by default; `app_custom_buttons_apply` and `app_associated_resources_apply` publish after at least one write succeeds and do not accept a draft-only `publish` parameter; `app_charts_apply` is immediate-live without publish.
|
|
@@ -179,7 +187,7 @@ Treat these as the official surface. Do not default to `package_create`, `packag
|
|
|
179
187
|
- For UI cards or quick narration, read `resources[]` first. Each resource has `resource_type`, `operation`, `status`, `id`, `key`, `name`, typed `ids`, and `parent`.
|
|
180
188
|
- Use legacy fields such as `field_diff`, `views_diff`, `chart_results`, `created/updated/removed`, and `verification` only for compatibility and troubleshooting.
|
|
181
189
|
- Treat post-write readback as the source of truth, not just write status codes.
|
|
182
|
-
- `success` means write and verification completed; `partial_success` means the write landed but verification is incomplete.
|
|
190
|
+
- `success` means write and verification completed; `partial_success` means the write landed or may have landed but verification is incomplete. If `write_executed=true`, `safe_to_retry=false`, or readback is unavailable, do `readback_before_retry` before any create retry.
|
|
183
191
|
- For portals, distinguish clearly between:
|
|
184
192
|
- base-info-only update with layout preserved
|
|
185
193
|
- full sections replace
|
|
@@ -271,6 +279,8 @@ For add-data buttons that create a child record linked back to the current recor
|
|
|
271
279
|
- Tool choice and sequencing: [references/tool-selection.md](references/tool-selection.md)
|
|
272
280
|
- Field matching rules: [references/match-rules.md](references/match-rules.md)
|
|
273
281
|
- Result semantics and gotchas: [references/gotchas.md](references/gotchas.md)
|
|
282
|
+
- Single app development guide: [references/single-app-development-guide.md](references/single-app-development-guide.md)
|
|
283
|
+
- Complete system development guide: [references/complete-system-development-guide.md](references/complete-system-development-guide.md)
|
|
274
284
|
- Create one app in an existing package: [references/create-app.md](references/create-app.md)
|
|
275
285
|
- Update fields only: [references/update-schema.md](references/update-schema.md)
|
|
276
286
|
- Update layout only: [references/update-layout.md](references/update-layout.md)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Complete System Development Guide
|
|
2
|
+
|
|
3
|
+
Use this when the user asks for a system, app package, workspace module set, or several related apps/forms.
|
|
4
|
+
Do not compress several business objects into one app.
|
|
5
|
+
|
|
6
|
+
## Recommended Completeness
|
|
7
|
+
|
|
8
|
+
Required:
|
|
9
|
+
|
|
10
|
+
- package exists or is created through `package_apply`
|
|
11
|
+
- core business objects are modeled as separate apps
|
|
12
|
+
- all new apps are created in one multi-app `app_schema_apply(package_id=..., apps=[...])`
|
|
13
|
+
- every app has stable `client_key`, `app_name`, `icon`, `color`, and one readable `as_data_title: true`
|
|
14
|
+
- same-call relations use `target_app_ref` to point to another `apps[].client_key`
|
|
15
|
+
- basic operating views exist for each core app
|
|
16
|
+
- package/apps/fields/relations are read back before repair or retry
|
|
17
|
+
|
|
18
|
+
Strongly recommended:
|
|
19
|
+
|
|
20
|
+
- cross-app relation fields for the main business links
|
|
21
|
+
- core metric charts and BI charts using semantic `metric`, `metrics`, `group_by`, and `where`
|
|
22
|
+
- a standard portal: business entry area, core metrics, BI charts, then concrete data views
|
|
23
|
+
- portal grid/business-entry sections contain real `config.items[]`, not empty containers
|
|
24
|
+
|
|
25
|
+
Optional:
|
|
26
|
+
|
|
27
|
+
- workflow, reminders, buttons, associated resources, sample data, roles, and permissions when the user asks for them or the business process clearly depends on them
|
|
28
|
+
|
|
29
|
+
## Standard Path
|
|
30
|
+
|
|
31
|
+
1. Resolve or create package: `package_get` / `package_apply`.
|
|
32
|
+
2. Draft all apps and relations with stable `client_key` values.
|
|
33
|
+
3. Run one multi-app `app_schema_apply` with `apps[]`.
|
|
34
|
+
4. If write result is uncertain, run readback before retrying.
|
|
35
|
+
5. For each app, apply layout and views.
|
|
36
|
+
6. Create charts before portals when the portal needs metrics or BI sections.
|
|
37
|
+
7. Create portal only after referenced apps, views, and charts are known.
|
|
38
|
+
8. Add workflows/buttons/associated resources when they are part of the requested business process.
|
|
39
|
+
|
|
40
|
+
## Recovery Rules
|
|
41
|
+
|
|
42
|
+
- Timeout, `partial_success`, `write_executed=true`, `safe_to_retry=false`, or incomplete readback means `write_may_have_succeeded`.
|
|
43
|
+
- Next action is always `readback_before_retry`: read package, resolve intended apps, then read fields/relations.
|
|
44
|
+
- Retry only verified missing apps, fields, or relations.
|
|
45
|
+
- Do not rebuild the system as separate single-app creates.
|
|
46
|
+
- Do not create `V2`, `测试`, timestamp, or random-suffix apps to bypass duplicate names.
|
|
47
|
+
|
|
48
|
+
## Portal Template
|
|
49
|
+
|
|
50
|
+
- Top area: one business entry grid plus one todo/common/frequent component when useful.
|
|
51
|
+
- Metrics: one row of 4-6 indicator/target cards with portal section `role: "metric"`; recommended height 5.
|
|
52
|
+
- BI charts: one row of 2-3 charts, 1-2 rows; recommended height 7.
|
|
53
|
+
- Data views: one row of 1-2 concrete views, 1-2 rows; recommended height 11.
|
|
54
|
+
|
|
55
|
+
## Stop Conditions
|
|
56
|
+
|
|
57
|
+
- If the user only asked for one app, switch to the single-app guide.
|
|
58
|
+
- If required package/app/field/relation readback contradicts the intended model, repair the specific missing slice before building dependent resources.
|
|
59
|
+
- If a public tool cannot express a needed business feature, state the gap and ask before using any fallback or submitting feedback.
|
|
@@ -8,7 +8,7 @@ Do not use this playbook when the user is really asking for a system/package wit
|
|
|
8
8
|
1. read or create the package through `package_get` / `package_apply`
|
|
9
9
|
2. create the related apps in one `app_schema_apply(package_id=..., apps=[...])` call
|
|
10
10
|
3. keep package ownership on the public `package_id` path instead of a separate attach step
|
|
11
|
-
4. add same-call relation fields with `target_app_ref` when one new app references another new app
|
|
11
|
+
4. add same-call relation fields with `target_app_ref` when one new app references another new app; use `target_app_key` only after the target app already exists or readback confirms it
|
|
12
12
|
5. choose explicit non-template `icon + color` for each new package/app
|
|
13
13
|
|
|
14
14
|
Hierarchy reminder:
|
|
@@ -41,6 +41,8 @@ Use this pattern instead:
|
|
|
41
41
|
2. for a multi-app system, run one `app_schema_apply` with `package_id` and `apps[]`
|
|
42
42
|
3. use `apps[].client_key` plus relation field `target_app_ref` when one new app references another new app
|
|
43
43
|
|
|
44
|
+
If that multi-app call times out, returns `partial_success`, returns `write_executed=true`, has `safe_to_retry=false`, or has incomplete readback, do not decide that the create failed. Run `readback_before_retry`: read the package, resolve each intended app by package/name or returned app key, read fields, and retry only verified missing apps or fields. Do not rebuild the same complete system as separate single-app creates, and do not create `V2`, `测试`, timestamp, or random-suffix apps to bypass duplicate names.
|
|
45
|
+
|
|
44
46
|
## Example
|
|
45
47
|
|
|
46
48
|
When designing fields, do not add platform system fields such as `数据ID`, `编号`, `申请人`, `申请时间`, `创建人`, `创建时间`, `提交人`, `提交时间`, `更新时间`, `更新人`, `当前流程状态`, `当前处理人`, `当前处理节点`, or `流程标题`. Qingflow provides them automatically; create only business fields.
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
- Another app is not a field
|
|
18
18
|
- If the user names multiple forms/modules that relate to each other, create multiple apps and connect them with relation fields
|
|
19
19
|
- Do not use child app names like “项目/需求/任务/缺陷/团队” as plain text fields inside one app
|
|
20
|
+
- For a complete system, the main creation path is one multi-app `app_schema_apply(package_id=..., apps=[...])` with stable `apps[].client_key` values and same-call relation fields using `target_app_ref`
|
|
21
|
+
- Use `target_app_key` only for an app that already exists or has been confirmed by readback
|
|
20
22
|
|
|
21
23
|
## Auto publish
|
|
22
24
|
|
|
@@ -88,7 +90,11 @@
|
|
|
88
90
|
## Retry discipline
|
|
89
91
|
|
|
90
92
|
- If a write returns `partial_success`, read back before retrying
|
|
93
|
+
- If multi-app schema apply times out, returns `write_executed=true`, returns `safe_to_retry=false`, or has incomplete readback, treat it as `write_may_have_succeeded`; the next action is `readback_before_retry`
|
|
94
|
+
- `readback_before_retry` means read package/app/fields first, classify which intended apps and relation fields actually exist, and retry only the verified missing slice
|
|
91
95
|
- Do not repeat create steps after `app_key` already exists
|
|
96
|
+
- Do not split a complete-system schema create into single-app rebuilds until readback proves exactly what is missing
|
|
97
|
+
- Do not bypass duplicate/conflict states by inventing `V2`, `测试`, timestamp, or random-suffix app names in a real business package
|
|
92
98
|
- For backend rejects, keep the retry narrow: retry only the failed tool, not the whole chain
|
|
93
99
|
- For `VALIDATION_ERROR`, do not keep guessing. Reuse `suggested_next_call`, `canonical_arguments`, `allowed_keys`, and `allowed_values` first.
|
|
94
100
|
- For layout `VALIDATION_ERROR`, also inspect `section_allowed_keys`, `section_aliases`, and `minimal_section_example`. If the same layout-shape error repeats twice, stop free-form retries and re-read `builder_tool_contract(app_layout_apply)`.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Single App Development Guide
|
|
2
|
+
|
|
3
|
+
Use this when the user asks for one app/form, or gives one `app_key`.
|
|
4
|
+
If the user asks for a package, system, or several related modules, use `complete-system-development-guide.md` instead.
|
|
5
|
+
|
|
6
|
+
## Recommended Completeness
|
|
7
|
+
|
|
8
|
+
Required:
|
|
9
|
+
|
|
10
|
+
- target package/app resolved by `package_get`, `app_resolve`, or `app_get`
|
|
11
|
+
- business fields created or updated through `app_schema_apply`
|
|
12
|
+
- exactly one readable top-level `as_data_title: true`
|
|
13
|
+
- no platform system fields in `add_fields`
|
|
14
|
+
- layout places the important fields
|
|
15
|
+
- at least one business view beyond platform default views when the task includes user-facing list work
|
|
16
|
+
|
|
17
|
+
Strongly recommended:
|
|
18
|
+
|
|
19
|
+
- saved filters and query panel fields for the main operating views
|
|
20
|
+
- simple charts when the app has status, amount, date, or owner fields worth tracking
|
|
21
|
+
- associated resources or buttons only when they support a concrete workflow
|
|
22
|
+
|
|
23
|
+
Optional:
|
|
24
|
+
|
|
25
|
+
- workflow, portal entry, sample data, roles, or visibility changes when requested by the user or clearly needed by the app's job
|
|
26
|
+
|
|
27
|
+
## Standard Path
|
|
28
|
+
|
|
29
|
+
1. Read current target: `app_resolve` or `app_get`.
|
|
30
|
+
2. Read current fields: `app_get_fields`.
|
|
31
|
+
3. Apply fields with `app_schema_apply`.
|
|
32
|
+
4. Apply layout with `app_layout_apply`.
|
|
33
|
+
5. Apply views with `app_views_apply` when list/table/card/gantt behavior is part of the request.
|
|
34
|
+
6. Apply charts/buttons/associated resources only if they are part of the app's actual workflow.
|
|
35
|
+
7. Use `app_publish_verify` only when explicit live verification is required.
|
|
36
|
+
|
|
37
|
+
## Field Rules
|
|
38
|
+
|
|
39
|
+
- Use agent-friendly field types where possible: `text`, `multiline`, `select`, `multi_select`, `number`, `amount`, `date`, `datetime`, `member`, `department`, `attachment`, `relation`.
|
|
40
|
+
- Do not create `数据ID`, `编号`, `申请人`, `申请时间`, `创建人`, `创建时间`, `提交人`, `提交时间`, `更新时间`, `更新人`, `当前流程状态`, `当前处理人`, `当前处理节点`, or `流程标题`.
|
|
41
|
+
- Do not create built-in default views such as `全部数据` or `我的数据`; Qingflow provides system views.
|
|
42
|
+
|
|
43
|
+
## Stop Conditions
|
|
44
|
+
|
|
45
|
+
- If the task actually needs multiple business objects, stop and switch to the complete-system guide.
|
|
46
|
+
- If a write returns `partial_success`, `write_executed=true`, or `safe_to_retry=false`, read back before retrying.
|
|
47
|
+
- If the same validation error repeats twice, re-read `builder_tool_contract` instead of guessing.
|
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
Use these when you need a quick reminder of the standard v2 builder sequences.
|
|
4
4
|
|
|
5
|
+
## Create a complete system / app package
|
|
6
|
+
|
|
7
|
+
1. `package_get` or `package_apply`
|
|
8
|
+
2. one multi-app `app_schema_apply(package_id=..., apps=[...])`
|
|
9
|
+
3. use stable `apps[].client_key` values and relation `target_app_ref` for same-call app references
|
|
10
|
+
4. use returned/readback `app_key` values for layout, views, workflow, charts, buttons, associated resources, and portal work
|
|
11
|
+
5. `app_publish_verify` only when the user asks for explicit live verification or a final verification pass is required
|
|
12
|
+
|
|
13
|
+
If the multi-app schema write times out or returns `partial_success`, `write_executed=true`, `safe_to_retry=false`, or incomplete readback, the next action is `readback_before_retry`: read package/app/fields first, then retry only verified missing pieces. Do not repeat the whole apps payload, split into single-app rebuilds, or create `V2` / `测试` / random-suffix apps to dodge duplicate names.
|
|
14
|
+
|
|
5
15
|
## Create a new app in an existing package
|
|
6
16
|
|
|
7
17
|
1. `package_get`
|
|
@@ -7,6 +7,8 @@ metadata:
|
|
|
7
7
|
|
|
8
8
|
# Qingflow App Builder Code Integrations
|
|
9
9
|
|
|
10
|
+
> **Skill 版本**:`qingflow-skills-2026.06.23.1`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
|
|
11
|
+
|
|
10
12
|
Use this skill when the user wants to build or repair:
|
|
11
13
|
- `code_block` fields
|
|
12
14
|
- `q_linker` fields
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import re
|
|
3
4
|
from enum import Enum
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
@@ -70,17 +71,36 @@ class PublicExternalVisibilityMode(str, Enum):
|
|
|
70
71
|
|
|
71
72
|
|
|
72
73
|
FIELD_TYPE_ALIASES: dict[str, PublicFieldType] = {
|
|
74
|
+
"multiline": PublicFieldType.long_text,
|
|
75
|
+
"multiline_text": PublicFieldType.long_text,
|
|
76
|
+
"multi_line": PublicFieldType.long_text,
|
|
77
|
+
"multi_line_text": PublicFieldType.long_text,
|
|
73
78
|
"textarea": PublicFieldType.long_text,
|
|
79
|
+
"longtext": PublicFieldType.long_text,
|
|
80
|
+
"long-text": PublicFieldType.long_text,
|
|
74
81
|
"amount": PublicFieldType.amount,
|
|
75
82
|
"currency": PublicFieldType.amount,
|
|
76
83
|
"mobile": PublicFieldType.phone,
|
|
77
84
|
"user": PublicFieldType.member,
|
|
78
85
|
"users": PublicFieldType.member,
|
|
79
86
|
"select": PublicFieldType.single_select,
|
|
87
|
+
"single_choice": PublicFieldType.single_select,
|
|
88
|
+
"single-choice": PublicFieldType.single_select,
|
|
89
|
+
"single choice": PublicFieldType.single_select,
|
|
90
|
+
"choice": PublicFieldType.single_select,
|
|
91
|
+
"dropdown": PublicFieldType.single_select,
|
|
80
92
|
"radio": PublicFieldType.single_select,
|
|
81
93
|
"checkbox": PublicFieldType.multi_select,
|
|
82
94
|
"multi_select": PublicFieldType.multi_select,
|
|
83
95
|
"multi-select": PublicFieldType.multi_select,
|
|
96
|
+
"multi select": PublicFieldType.multi_select,
|
|
97
|
+
"multiselect": PublicFieldType.multi_select,
|
|
98
|
+
"multi_choice": PublicFieldType.multi_select,
|
|
99
|
+
"multi-choice": PublicFieldType.multi_select,
|
|
100
|
+
"multi choice": PublicFieldType.multi_select,
|
|
101
|
+
"multiple_choice": PublicFieldType.multi_select,
|
|
102
|
+
"multiple-choice": PublicFieldType.multi_select,
|
|
103
|
+
"multiple choice": PublicFieldType.multi_select,
|
|
84
104
|
"departments": PublicFieldType.department,
|
|
85
105
|
"qlinker": PublicFieldType.q_linker,
|
|
86
106
|
"q_linker": PublicFieldType.q_linker,
|
|
@@ -1796,6 +1816,65 @@ class ChartFilterRulePatch(StrictModel):
|
|
|
1796
1816
|
return self
|
|
1797
1817
|
|
|
1798
1818
|
|
|
1819
|
+
class ChartMetricPatch(StrictModel):
|
|
1820
|
+
op: str = "count"
|
|
1821
|
+
field_name: str | None = Field(default=None, validation_alias=AliasChoices("field", "field_name", "fieldName", "name"))
|
|
1822
|
+
alias: str | None = None
|
|
1823
|
+
|
|
1824
|
+
@model_validator(mode="before")
|
|
1825
|
+
@classmethod
|
|
1826
|
+
def normalize_metric(cls, value: Any) -> Any:
|
|
1827
|
+
if isinstance(value, str):
|
|
1828
|
+
raw = value.strip()
|
|
1829
|
+
match = re.fullmatch(r"([A-Za-z_][A-Za-z0-9_]*)\s*\(\s*(.*?)\s*\)", raw)
|
|
1830
|
+
if match:
|
|
1831
|
+
op = match.group(1).strip().lower()
|
|
1832
|
+
field = match.group(2).strip()
|
|
1833
|
+
payload: dict[str, Any] = {"op": op}
|
|
1834
|
+
if field and field != "*":
|
|
1835
|
+
payload["field_name"] = field
|
|
1836
|
+
return payload
|
|
1837
|
+
if raw:
|
|
1838
|
+
return {"op": "sum", "field_name": raw}
|
|
1839
|
+
return {"op": "count"}
|
|
1840
|
+
if not isinstance(value, dict):
|
|
1841
|
+
return value
|
|
1842
|
+
payload = dict(value)
|
|
1843
|
+
if "operation" in payload and "op" not in payload:
|
|
1844
|
+
payload["op"] = payload.pop("operation")
|
|
1845
|
+
if "agg" in payload and "op" not in payload:
|
|
1846
|
+
payload["op"] = payload.pop("agg")
|
|
1847
|
+
if "aggregate" in payload and "op" not in payload:
|
|
1848
|
+
payload["op"] = payload.pop("aggregate")
|
|
1849
|
+
if "aggregation" in payload and "op" not in payload:
|
|
1850
|
+
payload["op"] = payload.pop("aggregation")
|
|
1851
|
+
return payload
|
|
1852
|
+
|
|
1853
|
+
@model_validator(mode="after")
|
|
1854
|
+
def validate_metric(self) -> "ChartMetricPatch":
|
|
1855
|
+
normalized_op = str(self.op or "count").strip().lower()
|
|
1856
|
+
op_aliases = {
|
|
1857
|
+
"average": "avg",
|
|
1858
|
+
"mean": "avg",
|
|
1859
|
+
"total": "sum",
|
|
1860
|
+
"cnt": "count",
|
|
1861
|
+
"count_all": "count",
|
|
1862
|
+
}
|
|
1863
|
+
self.op = op_aliases.get(normalized_op, normalized_op)
|
|
1864
|
+
if self.field_name is not None:
|
|
1865
|
+
field_name = str(self.field_name).strip()
|
|
1866
|
+
self.field_name = field_name or None
|
|
1867
|
+
if self.alias is not None:
|
|
1868
|
+
alias = str(self.alias).strip()
|
|
1869
|
+
self.alias = alias or None
|
|
1870
|
+
supported = {"count", "sum", "avg", "max", "min"}
|
|
1871
|
+
if self.op not in supported:
|
|
1872
|
+
raise ValueError(f"chart metric op must be one of {sorted(supported)}")
|
|
1873
|
+
if self.op != "count" and not self.field_name:
|
|
1874
|
+
raise ValueError(f"chart metric op '{self.op}' requires field")
|
|
1875
|
+
return self
|
|
1876
|
+
|
|
1877
|
+
|
|
1799
1878
|
class ChartUpsertPatch(StrictModel):
|
|
1800
1879
|
chart_id: str | None = None
|
|
1801
1880
|
name: str
|
|
@@ -1803,6 +1882,17 @@ class ChartUpsertPatch(StrictModel):
|
|
|
1803
1882
|
dimension_field_ids: list[str] = Field(default_factory=list)
|
|
1804
1883
|
indicator_field_ids: list[str] = Field(default_factory=list)
|
|
1805
1884
|
filters: list[ChartFilterRulePatch] = Field(default_factory=list)
|
|
1885
|
+
group_by: list[str] = Field(default_factory=list)
|
|
1886
|
+
rows: list[str] = Field(default_factory=list)
|
|
1887
|
+
columns: list[str] = Field(default_factory=list)
|
|
1888
|
+
metric: ChartMetricPatch | None = None
|
|
1889
|
+
metrics: list[ChartMetricPatch] = Field(default_factory=list)
|
|
1890
|
+
x_metric: ChartMetricPatch | None = None
|
|
1891
|
+
y_metric: ChartMetricPatch | None = None
|
|
1892
|
+
left_metric: ChartMetricPatch | None = None
|
|
1893
|
+
right_metric: ChartMetricPatch | None = None
|
|
1894
|
+
value_metric: ChartMetricPatch | None = None
|
|
1895
|
+
target_metric: ChartMetricPatch | None = None
|
|
1806
1896
|
question_config: list[dict[str, Any]] = Field(default_factory=list)
|
|
1807
1897
|
user_config: list[dict[str, Any]] = Field(default_factory=list)
|
|
1808
1898
|
config: dict[str, Any] = Field(default_factory=dict)
|
|
@@ -1820,10 +1910,74 @@ class ChartUpsertPatch(StrictModel):
|
|
|
1820
1910
|
payload["chart_type"] = payload.pop("type")
|
|
1821
1911
|
if "dimension_fields" in payload and "dimension_field_ids" not in payload:
|
|
1822
1912
|
payload["dimension_field_ids"] = payload.pop("dimension_fields")
|
|
1913
|
+
if "dimensions" in payload and "dimension_field_ids" not in payload and "group_by" not in payload:
|
|
1914
|
+
payload["group_by"] = payload.pop("dimensions")
|
|
1915
|
+
if "groupBy" in payload and "group_by" not in payload:
|
|
1916
|
+
payload["group_by"] = payload.pop("groupBy")
|
|
1917
|
+
if "where" in payload and "filters" not in payload:
|
|
1918
|
+
payload["filters"] = payload.pop("where")
|
|
1919
|
+
if "filter_rules" in payload and "filters" not in payload:
|
|
1920
|
+
payload["filters"] = payload.pop("filter_rules")
|
|
1921
|
+
if "filterRules" in payload and "filters" not in payload:
|
|
1922
|
+
payload["filters"] = payload.pop("filterRules")
|
|
1823
1923
|
if "indicator_fields" in payload and "indicator_field_ids" not in payload:
|
|
1824
1924
|
payload["indicator_field_ids"] = payload.pop("indicator_fields")
|
|
1825
1925
|
if "metric_field_ids" in payload and "indicator_field_ids" not in payload:
|
|
1826
1926
|
payload["indicator_field_ids"] = payload.pop("metric_field_ids")
|
|
1927
|
+
metric_slots: list[Any] = []
|
|
1928
|
+
generic_metric_keys = ("metric", "metrics")
|
|
1929
|
+
axis_metric_keys = (
|
|
1930
|
+
"x_metric",
|
|
1931
|
+
"xMetric",
|
|
1932
|
+
"y_metric",
|
|
1933
|
+
"yMetric",
|
|
1934
|
+
"left_metric",
|
|
1935
|
+
"leftMetric",
|
|
1936
|
+
"right_metric",
|
|
1937
|
+
"rightMetric",
|
|
1938
|
+
"value_metric",
|
|
1939
|
+
"valueMetric",
|
|
1940
|
+
"target_metric",
|
|
1941
|
+
"targetMetric",
|
|
1942
|
+
)
|
|
1943
|
+
|
|
1944
|
+
def has_metric_value(key: str) -> bool:
|
|
1945
|
+
entry = payload.get(key)
|
|
1946
|
+
return entry is not None and entry != "" and entry != []
|
|
1947
|
+
|
|
1948
|
+
if any(has_metric_value(key) for key in generic_metric_keys) and any(has_metric_value(key) for key in axis_metric_keys):
|
|
1949
|
+
raise ValueError(
|
|
1950
|
+
"chart metric input is ambiguous: use either metric/metrics or axis-specific "
|
|
1951
|
+
"x_metric/y_metric, left_metric/right_metric, value_metric/target_metric, not both"
|
|
1952
|
+
)
|
|
1953
|
+
if "metric" in payload and "metrics" not in payload:
|
|
1954
|
+
payload["metrics"] = [payload["metric"]]
|
|
1955
|
+
for key in ("x_metric", "xMetric", "left_metric", "leftMetric", "value_metric", "valueMetric"):
|
|
1956
|
+
if key in payload:
|
|
1957
|
+
slot_value = payload.get(key)
|
|
1958
|
+
if slot_value is not None:
|
|
1959
|
+
metric_slots.append(slot_value)
|
|
1960
|
+
canonical = {
|
|
1961
|
+
"xMetric": "x_metric",
|
|
1962
|
+
"leftMetric": "left_metric",
|
|
1963
|
+
"valueMetric": "value_metric",
|
|
1964
|
+
}.get(key)
|
|
1965
|
+
if canonical and canonical not in payload:
|
|
1966
|
+
payload[canonical] = payload.pop(key)
|
|
1967
|
+
for key in ("y_metric", "yMetric", "right_metric", "rightMetric", "target_metric", "targetMetric"):
|
|
1968
|
+
if key in payload:
|
|
1969
|
+
slot_value = payload.get(key)
|
|
1970
|
+
if slot_value is not None:
|
|
1971
|
+
metric_slots.append(slot_value)
|
|
1972
|
+
canonical = {
|
|
1973
|
+
"yMetric": "y_metric",
|
|
1974
|
+
"rightMetric": "right_metric",
|
|
1975
|
+
"targetMetric": "target_metric",
|
|
1976
|
+
}.get(key)
|
|
1977
|
+
if canonical and canonical not in payload:
|
|
1978
|
+
payload[canonical] = payload.pop(key)
|
|
1979
|
+
if metric_slots and "metrics" not in payload:
|
|
1980
|
+
payload["metrics"] = metric_slots
|
|
1827
1981
|
raw_type = payload.get("chart_type")
|
|
1828
1982
|
if isinstance(raw_type, str):
|
|
1829
1983
|
normalized = raw_type.strip().lower()
|
|
@@ -1850,8 +2004,31 @@ class ChartUpsertPatch(StrictModel):
|
|
|
1850
2004
|
payload["dimension_field_ids"] = [str(item) for item in payload["dimension_field_ids"] if item is not None and str(item).strip()]
|
|
1851
2005
|
if isinstance(payload.get("indicator_field_ids"), list):
|
|
1852
2006
|
payload["indicator_field_ids"] = [str(item) for item in payload["indicator_field_ids"] if item is not None and str(item).strip()]
|
|
2007
|
+
for key in ("group_by", "rows", "columns"):
|
|
2008
|
+
if isinstance(payload.get(key), str):
|
|
2009
|
+
payload[key] = [payload[key]]
|
|
2010
|
+
if isinstance(payload.get(key), list):
|
|
2011
|
+
payload[key] = [str(item) for item in payload[key] if item is not None and str(item).strip()]
|
|
1853
2012
|
return payload
|
|
1854
2013
|
|
|
2014
|
+
@model_validator(mode="after")
|
|
2015
|
+
def apply_semantic_chart_fields(self) -> "ChartUpsertPatch":
|
|
2016
|
+
if self.group_by and not self.dimension_field_ids:
|
|
2017
|
+
self.dimension_field_ids = list(self.group_by)
|
|
2018
|
+
if self.rows and not self.dimension_field_ids:
|
|
2019
|
+
self.dimension_field_ids = list(self.rows)
|
|
2020
|
+
semantic_metrics: list[ChartMetricPatch] = []
|
|
2021
|
+
if self.metrics:
|
|
2022
|
+
semantic_metrics.extend(self.metrics)
|
|
2023
|
+
for metric in (self.x_metric, self.y_metric, self.left_metric, self.right_metric, self.value_metric, self.target_metric):
|
|
2024
|
+
if metric is not None and metric not in semantic_metrics:
|
|
2025
|
+
semantic_metrics.append(metric)
|
|
2026
|
+
if semantic_metrics and not self.metrics:
|
|
2027
|
+
self.metrics = semantic_metrics
|
|
2028
|
+
if self.metrics and not self.metric:
|
|
2029
|
+
self.metric = self.metrics[0]
|
|
2030
|
+
return self
|
|
2031
|
+
|
|
1855
2032
|
|
|
1856
2033
|
class ChartPartialPatch(StrictModel):
|
|
1857
2034
|
chart_id: str | None = None
|
|
@@ -1993,6 +2170,7 @@ class PortalViewRefPatch(StrictModel):
|
|
|
1993
2170
|
class PortalSectionPatch(StrictModel):
|
|
1994
2171
|
title: str
|
|
1995
2172
|
source_type: str = Field(validation_alias=AliasChoices("source_type", "sourceType"))
|
|
2173
|
+
role: str | None = Field(default=None, validation_alias=AliasChoices("role", "zone", "section_role", "sectionRole"))
|
|
1996
2174
|
position: PortalComponentPositionPatch | None = None
|
|
1997
2175
|
dash_style_config: dict[str, Any] | None = Field(default=None, validation_alias=AliasChoices("dash_style_config", "dashStyleConfigBO"))
|
|
1998
2176
|
config: dict[str, Any] = Field(default_factory=dict)
|
|
@@ -2010,6 +2188,9 @@ class PortalSectionPatch(StrictModel):
|
|
|
2010
2188
|
raw_type = payload.get("source_type", payload.get("sourceType"))
|
|
2011
2189
|
if isinstance(raw_type, str):
|
|
2012
2190
|
payload["source_type"] = raw_type.strip().lower()
|
|
2191
|
+
raw_role = payload.get("role", payload.get("zone", payload.get("section_role", payload.get("sectionRole"))))
|
|
2192
|
+
if isinstance(raw_role, str):
|
|
2193
|
+
payload["role"] = raw_role.strip().lower()
|
|
2013
2194
|
if "chartRef" in payload and "chart_ref" not in payload:
|
|
2014
2195
|
payload["chart_ref"] = payload.pop("chartRef")
|
|
2015
2196
|
if "viewRef" in payload and "view_ref" not in payload:
|
|
@@ -2216,6 +2397,8 @@ class ChartGetResponse(StrictModel):
|
|
|
2216
2397
|
base: dict[str, Any] = Field(default_factory=dict)
|
|
2217
2398
|
visibility: dict[str, Any] = Field(default_factory=dict)
|
|
2218
2399
|
filters: list[list[dict[str, Any]]] = Field(default_factory=list)
|
|
2400
|
+
group_by: list[str] = Field(default_factory=list)
|
|
2401
|
+
metrics: list[dict[str, Any]] = Field(default_factory=list)
|
|
2219
2402
|
config: dict[str, Any] = Field(default_factory=dict)
|
|
2220
2403
|
|
|
2221
2404
|
|