openxiangda 1.0.34 → 1.0.36
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 +18 -0
- package/lib/cli.js +410 -1
- package/lib/workspace-init.js +1 -0
- package/openxiangda-skills/SKILL.md +5 -3
- package/openxiangda-skills/references/best-practices.md +11 -6
- package/openxiangda-skills/references/component-guide.md +10 -11
- package/openxiangda-skills/references/connector-resources.md +3 -0
- package/openxiangda-skills/references/data-views.md +217 -0
- package/openxiangda-skills/references/forms/component-registry.md +4 -3
- package/openxiangda-skills/references/forms/form-schema.md +31 -2
- package/openxiangda-skills/references/pages/page-sdk.md +43 -0
- package/openxiangda-skills/references/style-system.md +14 -18
- package/openxiangda-skills/references/troubleshooting.md +13 -13
- package/openxiangda-skills/references/workspace-state.md +9 -0
- package/openxiangda-skills/skills/openxiangda-form/SKILL.md +3 -3
- package/openxiangda-skills/skills/openxiangda-page/SKILL.md +2 -2
- package/openxiangda-skills/skills/openxiangda-permission-settings/SKILL.md +1 -1
- package/package.json +1 -1
- package/packages/sdk/dist/components/index.cjs +944 -765
- package/packages/sdk/dist/components/index.cjs.map +1 -1
- package/packages/sdk/dist/components/index.d.mts +18 -2
- package/packages/sdk/dist/components/index.d.ts +18 -2
- package/packages/sdk/dist/components/index.mjs +938 -761
- package/packages/sdk/dist/components/index.mjs.map +1 -1
- package/packages/sdk/dist/runtime/index.cjs +114 -30
- package/packages/sdk/dist/runtime/index.cjs.map +1 -1
- package/packages/sdk/dist/runtime/index.d.mts +18 -1
- package/packages/sdk/dist/runtime/index.d.ts +18 -1
- package/packages/sdk/dist/runtime/index.mjs +114 -30
- package/packages/sdk/dist/runtime/index.mjs.map +1 -1
- package/packages/sdk/dist/styles/antd-theme.cjs +11 -3
- package/packages/sdk/dist/styles/antd-theme.cjs.map +1 -1
- package/packages/sdk/dist/styles/antd-theme.d.mts +2 -1
- package/packages/sdk/dist/styles/antd-theme.d.ts +2 -1
- package/packages/sdk/dist/styles/antd-theme.mjs +11 -3
- package/packages/sdk/dist/styles/antd-theme.mjs.map +1 -1
- package/packages/sdk/dist/styles/tailwind-preset.cjs +0 -1
- package/packages/sdk/dist/styles/tailwind-preset.cjs.map +1 -1
- package/packages/sdk/dist/styles/tailwind-preset.d.mts +0 -1
- package/packages/sdk/dist/styles/tailwind-preset.d.ts +0 -1
- package/packages/sdk/dist/styles/tailwind-preset.mjs +0 -1
- package/packages/sdk/dist/styles/tailwind-preset.mjs.map +1 -1
- package/packages/sdk/dist/styles/tokens.css +1 -0
- package/packages/sdk/src/build-source/scripts/build-forms.mjs +135 -50
- package/packages/sdk/src/build-source/scripts/build-pages.mjs +37 -10
- package/packages/sdk/src/build-source/scripts/register.mjs +2 -0
- package/packages/sdk/src/build-source/scripts/utils/form-api.mjs +1 -0
- package/packages/sdk/src/build-source/scripts/utils/load-config.mjs +3 -2
- package/packages/sdk/src/build-source/scripts/utils/register-payload.test.ts +2 -1
- package/packages/sdk/src/build-source/scripts/utils/tailwind-config.mjs +9 -7
- package/packages/sdk/src/build-source/scripts/utils/tailwind-config.test.ts +6 -4
- package/packages/sdk/src/build-source/src/cli.mjs +17 -0
- package/templates/sy-lowcode-app-workspace/app-workspace.config.ts +3 -3
- package/templates/sy-lowcode-app-workspace/examples/best-practices/decision-guide.md +4 -3
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/permissions.test.ts +1 -1
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/app-role/schema.ts +36 -18
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/service-ticket/schema.ts +36 -18
- package/templates/sy-lowcode-app-workspace/postcss.config.cjs +0 -15
- package/templates/sy-lowcode-app-workspace/src/main.tsx +1 -12
- package/templates/sy-lowcode-app-workspace/src/shared/form-schema.ts +0 -1
package/README.md
CHANGED
|
@@ -29,6 +29,8 @@ openxiangda automation executions daily_ticket_digest --profile dev
|
|
|
29
29
|
openxiangda automation diagnose daily_ticket_digest --profile dev
|
|
30
30
|
openxiangda permission role-list --profile dev
|
|
31
31
|
openxiangda settings get customer --profile dev
|
|
32
|
+
openxiangda data-view list --profile dev
|
|
33
|
+
openxiangda data-view status ticket_with_customer --profile dev
|
|
32
34
|
openxiangda resource validate --profile dev
|
|
33
35
|
openxiangda resource plan --profile dev
|
|
34
36
|
openxiangda resource publish --profile dev
|
|
@@ -79,6 +81,8 @@ Use `openxiangda skill install --dest <skills-dir>` to target a different Codex
|
|
|
79
81
|
|
|
80
82
|
Create a new publishable app workspace with `openxiangda workspace init <dir>`. The command writes a minimal `sy-lowcode-app-workspace` template with React, Ant Design, the single `openxiangda` package, and the required `publish:all` / `openxiangda:publish` scripts. Use `--install` to run `pnpm install` immediately, or run it manually after creation.
|
|
81
83
|
|
|
84
|
+
New OpenXiangda code pages and form custom pages publish with `cssIsolation: "none"` by default, so Tailwind utilities and normal Ant Design styles apply without a `.sy-app-workspace` prefix. Explicit `namespace` and `shadow` settings are kept only for legacy compatibility.
|
|
85
|
+
|
|
82
86
|
Local workspace state is authoritative. If the current folder has no `.openxiangda/state.json` app binding, create a new app instead of searching the platform for a similar app name:
|
|
83
87
|
|
|
84
88
|
```bash
|
|
@@ -96,6 +100,20 @@ openxiangda workspace bind --profile dev --app-type APP_XXXX
|
|
|
96
100
|
|
|
97
101
|
工程化资源放在工作区 `src/resources/` 下,由 `openxiangda resource validate|plan|publish|pull` 管理。`workspace publish` 会先构建并注册 workspace 表单/页面,再执行非破坏性资源 upsert,这样菜单、权限组、流程和表单设置可以解析最新的 profile-local ID。需要删除平台中 manifest 未声明的资源时,显式传 `--prune`。连接器页面运行时通过 `sdk.connector.invoke()` / `sdk.connector.call("connector.api")` 调用平台运行时接口,第三方密钥只保存在后端连接器配置中。
|
|
98
102
|
|
|
103
|
+
多表只读查询优先声明 `src/resources/data-views/*.json` 数据视图,而不是在页面里手写多次单表查询再拼数据。数据视图适合工单+客户、订单+商品、项目+成员、报表列表、跨页面复用查询等读多写少场景。发布时 CLI 会把 `formCode` 解析为当前 profile 的 `formUuid`,平台创建 PostgreSQL materialized view;页面通过 `sdk.dataView.query(code, params)` 或 `sdk.dataSource.run()` 查询输出字段。数据视图只读,刷新后才反映源表变化,不适合单表 CRUD、写回源表、强实时状态或简单 linkedForm 下拉。
|
|
104
|
+
|
|
105
|
+
常用数据视图命令:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
openxiangda resource validate --profile dev
|
|
109
|
+
openxiangda resource plan --profile dev
|
|
110
|
+
openxiangda resource publish --profile dev
|
|
111
|
+
openxiangda data-view list --profile dev
|
|
112
|
+
openxiangda data-view status ticket_with_customer --profile dev
|
|
113
|
+
openxiangda data-view refresh ticket_with_customer --profile dev
|
|
114
|
+
openxiangda data-view query ticket_with_customer --query-json query.json --profile dev
|
|
115
|
+
```
|
|
116
|
+
|
|
99
117
|
AI-authored automation can use code-first resources:
|
|
100
118
|
|
|
101
119
|
- Source: `src/automations/<resourceCode>/index.ts`
|
package/lib/cli.js
CHANGED
|
@@ -54,6 +54,7 @@ async function main(argv) {
|
|
|
54
54
|
if (command === 'menu') return menu(rest);
|
|
55
55
|
if (command === 'workflow') return workflow(rest);
|
|
56
56
|
if (command === 'automation') return automation(rest);
|
|
57
|
+
if (command === 'data-view') return dataView(rest);
|
|
57
58
|
if (command === 'permission') return permission(rest);
|
|
58
59
|
if (command === 'settings') return settings(rest);
|
|
59
60
|
if (command === 'resource') return resource(rest);
|
|
@@ -105,6 +106,7 @@ Usage:
|
|
|
105
106
|
openxiangda automation logs <instanceId> [--automation <automationCode|automationId>] [--summary] [--redact] [--json]
|
|
106
107
|
openxiangda automation diagnose <automationCode|automationId> [--redact] [--json]
|
|
107
108
|
openxiangda automation publish|enable|disable <automationCode|automationId>
|
|
109
|
+
openxiangda data-view list|status|refresh|query <dataViewCode> [--profile name] [--json]
|
|
108
110
|
openxiangda permission role-list|role-create|role-bind
|
|
109
111
|
openxiangda permission page-group-list|page-group-create|page-group-bind
|
|
110
112
|
openxiangda permission form-group-list|form-group-create|form-group-bind
|
|
@@ -1090,6 +1092,7 @@ async function workspace(args) {
|
|
|
1090
1092
|
menus: {},
|
|
1091
1093
|
roles: {},
|
|
1092
1094
|
connectors: {},
|
|
1095
|
+
dataViews: {},
|
|
1093
1096
|
pagePermissionGroups: {},
|
|
1094
1097
|
formPermissionGroups: {},
|
|
1095
1098
|
formSettings: {},
|
|
@@ -1479,7 +1482,7 @@ async function page(args) {
|
|
|
1479
1482
|
jsUrls: splitList(flags['js-urls']),
|
|
1480
1483
|
framework: flags.framework || 'react',
|
|
1481
1484
|
frameworkVersion: flags['framework-version'] || '',
|
|
1482
|
-
cssIsolation: flags['css-isolation'] || '
|
|
1485
|
+
cssIsolation: flags['css-isolation'] || 'none',
|
|
1483
1486
|
format: flags.format || 'esm',
|
|
1484
1487
|
},
|
|
1485
1488
|
menu: flags.menu
|
|
@@ -1981,6 +1984,75 @@ async function automation(args) {
|
|
|
1981
1984
|
fail('用法: openxiangda automation list|create|bind|pull|executions|logs|diagnose|publish|unpublish|enable|disable|delete|validate|cron-validate');
|
|
1982
1985
|
}
|
|
1983
1986
|
|
|
1987
|
+
async function dataView(args) {
|
|
1988
|
+
const [subcommand, ...rest] = args;
|
|
1989
|
+
const { flags, positional } = parseArgs(rest);
|
|
1990
|
+
const config = loadConfig();
|
|
1991
|
+
const profileName = flags.profile || config.currentProfile;
|
|
1992
|
+
const target = getWorkspaceTarget(config, profileName, flags);
|
|
1993
|
+
|
|
1994
|
+
if (subcommand === 'list') {
|
|
1995
|
+
const data = await requestWithAuth(
|
|
1996
|
+
config,
|
|
1997
|
+
target.profileName,
|
|
1998
|
+
apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/data-views`, {
|
|
1999
|
+
page: flags.page,
|
|
2000
|
+
pageSize: flags['page-size'] || flags.limit,
|
|
2001
|
+
code: flags.code,
|
|
2002
|
+
})
|
|
2003
|
+
);
|
|
2004
|
+
if (flags.json) return writeJson(data);
|
|
2005
|
+
print(JSON.stringify(data, null, 2));
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
if (subcommand === 'status') {
|
|
2010
|
+
const [code] = positional;
|
|
2011
|
+
if (!code) fail('用法: openxiangda data-view status <dataViewCode>');
|
|
2012
|
+
const data = await requestWithAuth(
|
|
2013
|
+
config,
|
|
2014
|
+
target.profileName,
|
|
2015
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/data-views/${encodeURIComponent(code)}/status`
|
|
2016
|
+
);
|
|
2017
|
+
if (flags.json) return writeJson(data);
|
|
2018
|
+
print(JSON.stringify(data, null, 2));
|
|
2019
|
+
return;
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
if (subcommand === 'refresh') {
|
|
2023
|
+
const [code] = positional;
|
|
2024
|
+
if (!code) fail('用法: openxiangda data-view refresh <dataViewCode>');
|
|
2025
|
+
const data = await requestWithAuth(
|
|
2026
|
+
config,
|
|
2027
|
+
target.profileName,
|
|
2028
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/data-views/${encodeURIComponent(code)}/refresh`,
|
|
2029
|
+
{ method: 'POST' }
|
|
2030
|
+
);
|
|
2031
|
+
if (flags.json) return writeJson(data);
|
|
2032
|
+
print(JSON.stringify(data, null, 2));
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
if (subcommand === 'query') {
|
|
2037
|
+
const [code] = positional;
|
|
2038
|
+
if (!code) fail('用法: openxiangda data-view query <dataViewCode> [--query-json file]');
|
|
2039
|
+
const data = await requestWithAuth(
|
|
2040
|
+
config,
|
|
2041
|
+
target.profileName,
|
|
2042
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/data-views/${encodeURIComponent(code)}/query`,
|
|
2043
|
+
{
|
|
2044
|
+
method: 'POST',
|
|
2045
|
+
body: buildDataViewQueryBody(flags),
|
|
2046
|
+
}
|
|
2047
|
+
);
|
|
2048
|
+
if (flags.json) return writeJson(data);
|
|
2049
|
+
print(JSON.stringify(data, null, 2));
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
fail('用法: openxiangda data-view list|status|refresh|query');
|
|
2054
|
+
}
|
|
2055
|
+
|
|
1984
2056
|
async function permission(args) {
|
|
1985
2057
|
const [subcommand, ...rest] = args;
|
|
1986
2058
|
const { flags, positional } = parseArgs(rest);
|
|
@@ -2601,6 +2673,7 @@ async function commands(args) {
|
|
|
2601
2673
|
'menu list|create|bind|delete',
|
|
2602
2674
|
'workflow list|create|bind|pull|publish|delete|validate',
|
|
2603
2675
|
'automation list|create|bind|pull|publish|unpublish|enable|disable|delete|validate|cron-validate',
|
|
2676
|
+
'data-view list|status|refresh|query',
|
|
2604
2677
|
'permission role-list|role-create|role-bind|role-users|role-add-users',
|
|
2605
2678
|
'permission page-group-list|page-group-create|page-group-bind',
|
|
2606
2679
|
'permission form-group-list|form-group-create|form-group-bind|form-summary|menu-permissions',
|
|
@@ -2747,6 +2820,7 @@ function ensureResourceBuckets(bound) {
|
|
|
2747
2820
|
bound.resources.menus = bound.resources.menus || {};
|
|
2748
2821
|
bound.resources.roles = bound.resources.roles || {};
|
|
2749
2822
|
bound.resources.connectors = bound.resources.connectors || {};
|
|
2823
|
+
bound.resources.dataViews = bound.resources.dataViews || {};
|
|
2750
2824
|
bound.resources.notifications = bound.resources.notifications || {};
|
|
2751
2825
|
bound.resources.notifications.templates = bound.resources.notifications.templates || {};
|
|
2752
2826
|
bound.resources.notifications.typeConfigs = bound.resources.notifications.typeConfigs || {};
|
|
@@ -2939,6 +3013,14 @@ function saveAutomationResource(target, automationCode, automationId, extra = {}
|
|
|
2939
3013
|
}, keys);
|
|
2940
3014
|
}
|
|
2941
3015
|
|
|
3016
|
+
function saveDataViewResource(target, dataViewCode, dataViewId, extra = {}) {
|
|
3017
|
+
const keys = ['dataViewId', 'materializedViewName', 'status'];
|
|
3018
|
+
saveStateResource(target, 'dataViews', dataViewCode, {
|
|
3019
|
+
...pickStateFields(extra, keys),
|
|
3020
|
+
dataViewId,
|
|
3021
|
+
}, keys);
|
|
3022
|
+
}
|
|
3023
|
+
|
|
2942
3024
|
function saveRoleResource(target, roleCode, roleId) {
|
|
2943
3025
|
saveStateResource(target, 'roles', roleCode, { roleId }, ['roleId']);
|
|
2944
3026
|
}
|
|
@@ -3044,6 +3126,7 @@ const RESOURCE_SPECS = [
|
|
|
3044
3126
|
{ key: 'menus', dir: 'menus', topFiles: ['menus.json'], pluralKeys: ['menus'] },
|
|
3045
3127
|
{ key: 'workflows', dir: 'workflows', topFiles: ['workflows.json'], pluralKeys: ['workflows'] },
|
|
3046
3128
|
{ key: 'automations', dir: 'automations', topFiles: ['automations.json'], pluralKeys: ['automations'] },
|
|
3129
|
+
{ key: 'dataViews', dir: 'data-views', topFiles: ['data-views.json'], pluralKeys: ['dataViews', 'data-views'] },
|
|
3047
3130
|
{
|
|
3048
3131
|
key: 'pagePermissionGroups',
|
|
3049
3132
|
dir: path.join('permissions', 'page-groups'),
|
|
@@ -3254,6 +3337,14 @@ function validateResourceItem(kind, item, errors, warnings) {
|
|
|
3254
3337
|
errors.push(`${label}: 缺少 definitionJson 或 definitionFile`);
|
|
3255
3338
|
}
|
|
3256
3339
|
}
|
|
3340
|
+
if (kind === 'dataViews') {
|
|
3341
|
+
const definition = item.definition || item;
|
|
3342
|
+
if (!item.name && !definition.name) errors.push(`${label}: 缺少 name`);
|
|
3343
|
+
if (!definition.base) errors.push(`${label}: 缺少 base`);
|
|
3344
|
+
if (!Array.isArray(definition.select) || definition.select.length === 0) {
|
|
3345
|
+
errors.push(`${label}: 缺少 select`);
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3257
3348
|
if (kind === 'pagePermissionGroups' && !item.name) errors.push(`${label}: 缺少 name`);
|
|
3258
3349
|
if (kind === 'formPermissionGroups') {
|
|
3259
3350
|
if (!item.name) errors.push(`${label}: 缺少 name`);
|
|
@@ -3542,6 +3633,7 @@ async function buildResourcePlan(config, target, manifest) {
|
|
|
3542
3633
|
addNotificationPlanActions(target, actions, manifest.notifications || [], existing);
|
|
3543
3634
|
await addWorkflowPlanActions(config, target, actions, manifest.workflows, existing.workflows);
|
|
3544
3635
|
await addAutomationPlanActions(config, target, actions, manifest.automations, existing.automations);
|
|
3636
|
+
addPlanActions(actions, 'dataView', manifest.dataViews, existing.dataViews, (item, current) => dataViewEquals(target.bound, item, current));
|
|
3545
3637
|
addPlanActions(actions, 'pagePermissionGroup', manifest.pagePermissionGroups, existing.pagePermissionGroups, (item, current) => pagePermissionGroupEquals(target.bound, item, current));
|
|
3546
3638
|
addPlanActions(actions, 'formPermissionGroup', manifest.formPermissionGroups, existing.formPermissionGroups, (item, current) => formPermissionGroupEquals(target.bound, item, current));
|
|
3547
3639
|
for (const item of manifest.formSettings || []) {
|
|
@@ -3575,6 +3667,7 @@ async function publishResourceManifest(config, target, manifest, options = {}) {
|
|
|
3575
3667
|
await publishNotificationResources(config, target, manifest.notifications || [], result);
|
|
3576
3668
|
await publishWorkflowResources(config, target, manifest.workflows || [], result);
|
|
3577
3669
|
await publishAutomationResources(config, target, manifest.automations || [], result);
|
|
3670
|
+
await publishDataViewResources(config, target, manifest.dataViews || [], result);
|
|
3578
3671
|
await publishPagePermissionGroupResources(config, target, manifest.pagePermissionGroups || [], result);
|
|
3579
3672
|
await publishFormPermissionGroupResources(config, target, manifest.formPermissionGroups || [], result);
|
|
3580
3673
|
if (options.prune) {
|
|
@@ -3596,6 +3689,7 @@ async function fetchExistingResourceMaps(config, target, manifest) {
|
|
|
3596
3689
|
notificationTypeConfigs: new Map(),
|
|
3597
3690
|
workflows: new Map(),
|
|
3598
3691
|
automations: new Map(),
|
|
3692
|
+
dataViews: new Map(),
|
|
3599
3693
|
pagePermissionGroups: new Map(),
|
|
3600
3694
|
formPermissionGroups: new Map(),
|
|
3601
3695
|
};
|
|
@@ -3674,6 +3768,37 @@ async function fetchExistingResourceMaps(config, target, manifest) {
|
|
|
3674
3768
|
);
|
|
3675
3769
|
indexByCode(maps.automations, normalizeItems(data), item => item.resourceCode);
|
|
3676
3770
|
}
|
|
3771
|
+
if ((manifest.dataViews || []).length > 0) {
|
|
3772
|
+
const data = await requestWithAuth(
|
|
3773
|
+
config,
|
|
3774
|
+
target.profileName,
|
|
3775
|
+
apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/data-views`, {
|
|
3776
|
+
page: 1,
|
|
3777
|
+
pageSize: 1000,
|
|
3778
|
+
})
|
|
3779
|
+
);
|
|
3780
|
+
indexByCode(maps.dataViews, normalizeItems(data), item => item.code || item.resourceCode);
|
|
3781
|
+
for (const item of manifest.dataViews || []) {
|
|
3782
|
+
const code = item.code || item.resourceCode;
|
|
3783
|
+
const existing = maps.dataViews.get(code);
|
|
3784
|
+
if (!existing) continue;
|
|
3785
|
+
const detail = await requestOptionalWithAuth(
|
|
3786
|
+
config,
|
|
3787
|
+
target.profileName,
|
|
3788
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/data-views/${encodeURIComponent(code)}`
|
|
3789
|
+
);
|
|
3790
|
+
const groups = await requestOptionalWithAuth(
|
|
3791
|
+
config,
|
|
3792
|
+
target.profileName,
|
|
3793
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/data-views/${encodeURIComponent(code)}/permission-groups`
|
|
3794
|
+
);
|
|
3795
|
+
maps.dataViews.set(code, {
|
|
3796
|
+
...existing,
|
|
3797
|
+
...(detail || {}),
|
|
3798
|
+
permissionGroups: normalizeItems(groups),
|
|
3799
|
+
});
|
|
3800
|
+
}
|
|
3801
|
+
}
|
|
3677
3802
|
if ((manifest.pagePermissionGroups || []).length > 0) {
|
|
3678
3803
|
const data = await requestWithAuth(
|
|
3679
3804
|
config,
|
|
@@ -4237,6 +4362,52 @@ async function publishAutomationResources(config, target, automations, result) {
|
|
|
4237
4362
|
}
|
|
4238
4363
|
}
|
|
4239
4364
|
|
|
4365
|
+
async function publishDataViewResources(config, target, dataViews, result) {
|
|
4366
|
+
for (const dataViewItem of dataViews) {
|
|
4367
|
+
const existing = await findExistingDataView(config, target, dataViewItem.code);
|
|
4368
|
+
const body = normalizeDataViewManifest(target.bound, dataViewItem);
|
|
4369
|
+
const data = existing
|
|
4370
|
+
? await requestWithAuth(
|
|
4371
|
+
config,
|
|
4372
|
+
target.profileName,
|
|
4373
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/data-views/${encodeURIComponent(dataViewItem.code)}`,
|
|
4374
|
+
{ method: 'PUT', body }
|
|
4375
|
+
)
|
|
4376
|
+
: await requestWithAuth(
|
|
4377
|
+
config,
|
|
4378
|
+
target.profileName,
|
|
4379
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/data-views`,
|
|
4380
|
+
{ method: 'POST', body }
|
|
4381
|
+
);
|
|
4382
|
+
if (data?.id) {
|
|
4383
|
+
saveDataViewResource(target, dataViewItem.code, data.id, {
|
|
4384
|
+
materializedViewName: data.materializedViewName,
|
|
4385
|
+
status: data.status,
|
|
4386
|
+
});
|
|
4387
|
+
}
|
|
4388
|
+
result.published.push({
|
|
4389
|
+
kind: 'dataView',
|
|
4390
|
+
code: dataViewItem.code,
|
|
4391
|
+
action: existing ? 'update' : 'create',
|
|
4392
|
+
id: data?.id,
|
|
4393
|
+
});
|
|
4394
|
+
}
|
|
4395
|
+
}
|
|
4396
|
+
|
|
4397
|
+
async function findExistingDataView(config, target, code) {
|
|
4398
|
+
const stateId = target.bound.resources?.dataViews?.[code]?.dataViewId;
|
|
4399
|
+
const byCode = await requestOptionalWithAuth(
|
|
4400
|
+
config,
|
|
4401
|
+
target.profileName,
|
|
4402
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/data-views/${encodeURIComponent(code)}`
|
|
4403
|
+
);
|
|
4404
|
+
if (byCode?.id) return byCode;
|
|
4405
|
+
if (stateId) {
|
|
4406
|
+
return { id: stateId, code };
|
|
4407
|
+
}
|
|
4408
|
+
return null;
|
|
4409
|
+
}
|
|
4410
|
+
|
|
4240
4411
|
async function findExistingAutomation(config, target, code) {
|
|
4241
4412
|
const stateId = target.bound.resources?.automations?.[code]?.automationId;
|
|
4242
4413
|
if (stateId) {
|
|
@@ -4369,6 +4540,7 @@ async function pruneResourceManifest(config, target, manifest, result) {
|
|
|
4369
4540
|
await pruneMenus(config, target, desiredCodes(manifest.menus), result);
|
|
4370
4541
|
await pruneWorkflows(config, target, desiredCodes(manifest.workflows), result);
|
|
4371
4542
|
await pruneAutomations(config, target, desiredCodes(manifest.automations), result);
|
|
4543
|
+
await pruneDataViews(config, target, desiredCodes(manifest.dataViews), result);
|
|
4372
4544
|
await prunePagePermissionGroups(
|
|
4373
4545
|
config,
|
|
4374
4546
|
target,
|
|
@@ -4517,6 +4689,29 @@ async function pruneAutomations(config, target, desired, result) {
|
|
|
4517
4689
|
}
|
|
4518
4690
|
}
|
|
4519
4691
|
|
|
4692
|
+
async function pruneDataViews(config, target, desired, result) {
|
|
4693
|
+
const data = await requestWithAuth(
|
|
4694
|
+
config,
|
|
4695
|
+
target.profileName,
|
|
4696
|
+
apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/data-views`, {
|
|
4697
|
+
page: 1,
|
|
4698
|
+
pageSize: 1000,
|
|
4699
|
+
})
|
|
4700
|
+
);
|
|
4701
|
+
for (const dataView of normalizeItems(data)) {
|
|
4702
|
+
const code = dataView.code || dataView.resourceCode;
|
|
4703
|
+
if (!code || desired.has(code)) continue;
|
|
4704
|
+
await pruneOne(config, target, result, 'dataView', code, dataView.id, async () => {
|
|
4705
|
+
await requestWithAuth(
|
|
4706
|
+
config,
|
|
4707
|
+
target.profileName,
|
|
4708
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/data-views/${encodeURIComponent(code)}`,
|
|
4709
|
+
{ method: 'DELETE' }
|
|
4710
|
+
);
|
|
4711
|
+
});
|
|
4712
|
+
}
|
|
4713
|
+
}
|
|
4714
|
+
|
|
4520
4715
|
async function prunePagePermissionGroups(config, target, desired, result) {
|
|
4521
4716
|
const data = await requestWithAuth(
|
|
4522
4717
|
config,
|
|
@@ -4591,6 +4786,7 @@ function removeStateResource(target, kind, code) {
|
|
|
4591
4786
|
menu: 'menus',
|
|
4592
4787
|
workflow: 'workflows',
|
|
4593
4788
|
automation: 'automations',
|
|
4789
|
+
dataView: 'dataViews',
|
|
4594
4790
|
pagePermissionGroup: 'pagePermissionGroups',
|
|
4595
4791
|
formPermissionGroup: 'formPermissionGroups',
|
|
4596
4792
|
};
|
|
@@ -4759,6 +4955,31 @@ async function pullResources(config, target) {
|
|
|
4759
4955
|
written.push(path.relative(process.cwd(), filePath));
|
|
4760
4956
|
}
|
|
4761
4957
|
|
|
4958
|
+
const dataViews = await requestWithAuth(
|
|
4959
|
+
config,
|
|
4960
|
+
target.profileName,
|
|
4961
|
+
apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/data-views`, {
|
|
4962
|
+
page: 1,
|
|
4963
|
+
pageSize: 1000,
|
|
4964
|
+
})
|
|
4965
|
+
);
|
|
4966
|
+
for (const dataView of normalizeItems(dataViews)) {
|
|
4967
|
+
const code = dataView.code || dataView.resourceCode || dataView.id;
|
|
4968
|
+
const detail = await requestWithAuth(
|
|
4969
|
+
config,
|
|
4970
|
+
target.profileName,
|
|
4971
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/data-views/${encodeURIComponent(code)}`
|
|
4972
|
+
);
|
|
4973
|
+
const permissionGroups = await requestOptionalWithAuth(
|
|
4974
|
+
config,
|
|
4975
|
+
target.profileName,
|
|
4976
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/data-views/${encodeURIComponent(code)}/permission-groups`
|
|
4977
|
+
);
|
|
4978
|
+
const filePath = path.join(baseDir, 'data-views', `${code}.json`);
|
|
4979
|
+
writeResourceJsonFile(filePath, toPulledDataView(detail, permissionGroups, pullLookups));
|
|
4980
|
+
written.push(path.relative(process.cwd(), filePath));
|
|
4981
|
+
}
|
|
4982
|
+
|
|
4762
4983
|
const pageGroups = await requestWithAuth(
|
|
4763
4984
|
config,
|
|
4764
4985
|
target.profileName,
|
|
@@ -4920,6 +5141,43 @@ function toPulledNotificationTypeConfig(configItem, lookups) {
|
|
|
4920
5141
|
});
|
|
4921
5142
|
}
|
|
4922
5143
|
|
|
5144
|
+
function toPulledDataView(dataView, permissionGroups, lookups) {
|
|
5145
|
+
const definition = rewriteDataViewDefinitionForManifest(
|
|
5146
|
+
clonePlainJson(dataView.definition || {}),
|
|
5147
|
+
lookups
|
|
5148
|
+
);
|
|
5149
|
+
const groups = normalizeItems(permissionGroups).map(group => {
|
|
5150
|
+
const item = stripPulledResource(group);
|
|
5151
|
+
delete item.dataViewCode;
|
|
5152
|
+
return item;
|
|
5153
|
+
});
|
|
5154
|
+
return stripUndefinedValues({
|
|
5155
|
+
code: dataView.code || dataView.resourceCode,
|
|
5156
|
+
name: dataView.name,
|
|
5157
|
+
description: dataView.description || '',
|
|
5158
|
+
definition,
|
|
5159
|
+
refreshConfig: dataView.refreshConfig || undefined,
|
|
5160
|
+
permissionGroups: groups.length > 0 ? groups : undefined,
|
|
5161
|
+
});
|
|
5162
|
+
}
|
|
5163
|
+
|
|
5164
|
+
function rewriteDataViewDefinitionForManifest(definition, lookups) {
|
|
5165
|
+
rewriteDataViewSourceForManifest(definition.base, lookups);
|
|
5166
|
+
for (const join of definition.joins || []) {
|
|
5167
|
+
rewriteDataViewSourceForManifest(join, lookups);
|
|
5168
|
+
}
|
|
5169
|
+
return definition;
|
|
5170
|
+
}
|
|
5171
|
+
|
|
5172
|
+
function rewriteDataViewSourceForManifest(source, lookups) {
|
|
5173
|
+
if (!source || typeof source !== 'object') return;
|
|
5174
|
+
const formCode = lookups.formCodeByUuid.get(source.formUuid);
|
|
5175
|
+
if (formCode) {
|
|
5176
|
+
source.formCode = formCode;
|
|
5177
|
+
delete source.formUuid;
|
|
5178
|
+
}
|
|
5179
|
+
}
|
|
5180
|
+
|
|
4923
5181
|
function stripNotificationSecrets(value) {
|
|
4924
5182
|
if (!value || typeof value !== 'object') return value;
|
|
4925
5183
|
if (Array.isArray(value)) return value.map(stripNotificationSecrets);
|
|
@@ -5056,6 +5314,70 @@ function notificationTypeConfigExistingKey(config) {
|
|
|
5056
5314
|
return `${level}:${config.formUuid || ''}:${config.notificationType}`;
|
|
5057
5315
|
}
|
|
5058
5316
|
|
|
5317
|
+
function normalizeDataViewManifest(bound, dataView) {
|
|
5318
|
+
const definition = dataView.definition !== undefined
|
|
5319
|
+
? clonePlainJson(dataView.definition)
|
|
5320
|
+
: clonePlainJson(withoutDataViewManifestMeta(dataView));
|
|
5321
|
+
if (!definition.code) definition.code = dataView.code || dataView.resourceCode;
|
|
5322
|
+
if (!definition.name && dataView.name) definition.name = dataView.name;
|
|
5323
|
+
if (definition.description === undefined && dataView.description !== undefined) {
|
|
5324
|
+
definition.description = dataView.description;
|
|
5325
|
+
}
|
|
5326
|
+
const normalizedDefinition = normalizeDataViewDefinitionFormRefs(bound, definition, dataView);
|
|
5327
|
+
return stripUndefinedValues({
|
|
5328
|
+
code: dataView.code || dataView.resourceCode || normalizedDefinition.code,
|
|
5329
|
+
name: dataView.name || normalizedDefinition.name || dataView.code || normalizedDefinition.code,
|
|
5330
|
+
description: dataView.description !== undefined
|
|
5331
|
+
? dataView.description
|
|
5332
|
+
: normalizedDefinition.description || '',
|
|
5333
|
+
definition: normalizedDefinition,
|
|
5334
|
+
refreshConfig: dataView.refreshConfig || dataView.refresh || normalizedDefinition.refresh,
|
|
5335
|
+
permissionGroups: Array.isArray(dataView.permissionGroups)
|
|
5336
|
+
? dataView.permissionGroups.map(group => stripUndefinedValues(withoutResourceMeta(group)))
|
|
5337
|
+
: undefined,
|
|
5338
|
+
});
|
|
5339
|
+
}
|
|
5340
|
+
|
|
5341
|
+
function withoutDataViewManifestMeta(dataView) {
|
|
5342
|
+
const next = withoutResourceMeta(dataView);
|
|
5343
|
+
delete next.permissionGroups;
|
|
5344
|
+
delete next.refreshConfig;
|
|
5345
|
+
return next;
|
|
5346
|
+
}
|
|
5347
|
+
|
|
5348
|
+
function normalizeDataViewDefinitionFormRefs(bound, definition, dataView) {
|
|
5349
|
+
if (!definition?.base) {
|
|
5350
|
+
fail(`${resourceLabel('dataView', dataView)} 缺少 base`);
|
|
5351
|
+
}
|
|
5352
|
+
resolveDataViewSourceFormUuid(bound, definition.base, dataView, 'base');
|
|
5353
|
+
for (const [index, join] of (definition.joins || []).entries()) {
|
|
5354
|
+
resolveDataViewSourceFormUuid(bound, join, dataView, `joins[${index}]`);
|
|
5355
|
+
}
|
|
5356
|
+
return definition;
|
|
5357
|
+
}
|
|
5358
|
+
|
|
5359
|
+
function resolveDataViewSourceFormUuid(bound, source, dataView, label) {
|
|
5360
|
+
if (!source || typeof source !== 'object') {
|
|
5361
|
+
fail(`${resourceLabel('dataView', dataView)} ${label} 必须是 object`);
|
|
5362
|
+
}
|
|
5363
|
+
if (source.formUuid) {
|
|
5364
|
+
delete source.formCode;
|
|
5365
|
+
delete source.form;
|
|
5366
|
+
return;
|
|
5367
|
+
}
|
|
5368
|
+
const formCode = source.formCode || source.form;
|
|
5369
|
+
if (!formCode) {
|
|
5370
|
+
fail(`${resourceLabel('dataView', dataView)} ${label} 缺少 formCode 或 formUuid`);
|
|
5371
|
+
}
|
|
5372
|
+
const formUuid = bound.resources?.forms?.[formCode]?.formUuid;
|
|
5373
|
+
if (!formUuid) {
|
|
5374
|
+
fail(`${resourceLabel('dataView', dataView)} ${label}.formCode 未绑定: ${formCode}`);
|
|
5375
|
+
}
|
|
5376
|
+
source.formUuid = formUuid;
|
|
5377
|
+
delete source.formCode;
|
|
5378
|
+
delete source.form;
|
|
5379
|
+
}
|
|
5380
|
+
|
|
5059
5381
|
async function resolveManifestJson(config, profileName, item, objectKey, fileKey, optional = false) {
|
|
5060
5382
|
let value = item[objectKey];
|
|
5061
5383
|
if (value === undefined && item[fileKey]) {
|
|
@@ -5315,6 +5637,11 @@ function stripUndefinedValues(value) {
|
|
|
5315
5637
|
}, {});
|
|
5316
5638
|
}
|
|
5317
5639
|
|
|
5640
|
+
function clonePlainJson(value) {
|
|
5641
|
+
if (value === undefined) return undefined;
|
|
5642
|
+
return JSON.parse(JSON.stringify(value));
|
|
5643
|
+
}
|
|
5644
|
+
|
|
5318
5645
|
function workflowEquals(bound, desired, existing) {
|
|
5319
5646
|
if (!existing) return false;
|
|
5320
5647
|
const desiredDefinition = resolveManifestPlainJson(desired, 'definitionJson', 'definitionFile');
|
|
@@ -5361,6 +5688,57 @@ function automationEquals(target, desired, existing) {
|
|
|
5361
5688
|
);
|
|
5362
5689
|
}
|
|
5363
5690
|
|
|
5691
|
+
function dataViewEquals(bound, desired, existing) {
|
|
5692
|
+
if (!existing) return false;
|
|
5693
|
+
const expected = normalizeDataViewManifest(bound, desired);
|
|
5694
|
+
const permissionGroupsEqual = expected.permissionGroups === undefined
|
|
5695
|
+
? true
|
|
5696
|
+
: dataViewPermissionGroupsEqual(expected.permissionGroups, existing.permissionGroups || []);
|
|
5697
|
+
return (
|
|
5698
|
+
String(existing.code || existing.resourceCode || '') === String(expected.code || '') &&
|
|
5699
|
+
String(existing.name || '') === String(expected.name || '') &&
|
|
5700
|
+
String(existing.description || '') === String(expected.description || '') &&
|
|
5701
|
+
jsonEqualsForPlan(existing.definition || {}, expected.definition || {}, desired.__dir) &&
|
|
5702
|
+
jsonEqualsForPlan(existing.refreshConfig || {}, expected.refreshConfig || {}, desired.__dir) &&
|
|
5703
|
+
permissionGroupsEqual
|
|
5704
|
+
);
|
|
5705
|
+
}
|
|
5706
|
+
|
|
5707
|
+
function dataViewPermissionGroupsEqual(desiredGroups = [], existingGroups = []) {
|
|
5708
|
+
return stableStringifyForPlan(normalizeDataViewPermissionGroupsForPlan(existingGroups), process.cwd()) ===
|
|
5709
|
+
stableStringifyForPlan(normalizeDataViewPermissionGroupsForPlan(desiredGroups), process.cwd());
|
|
5710
|
+
}
|
|
5711
|
+
|
|
5712
|
+
function normalizeDataViewPermissionGroupsForPlan(groups = []) {
|
|
5713
|
+
return (groups || [])
|
|
5714
|
+
.map(group => {
|
|
5715
|
+
const code = group.code || group.resourceCode || '';
|
|
5716
|
+
return stripUndefinedValues({
|
|
5717
|
+
code,
|
|
5718
|
+
name: group.name || code || '默认权限组',
|
|
5719
|
+
roles: Array.isArray(group.roles) ? sortStringValues(group.roles) : [],
|
|
5720
|
+
operations: Array.isArray(group.operations) ? sortStringValues(group.operations) : ['query'],
|
|
5721
|
+
fieldPermissions: normalizeFieldPermissionsForPlan(group.fieldPermissions),
|
|
5722
|
+
dataPermission: group.dataPermission || undefined,
|
|
5723
|
+
});
|
|
5724
|
+
})
|
|
5725
|
+
.sort((left, right) => {
|
|
5726
|
+
const leftKey = `${left.code || ''}:${left.name || ''}`;
|
|
5727
|
+
const rightKey = `${right.code || ''}:${right.name || ''}`;
|
|
5728
|
+
return leftKey.localeCompare(rightKey);
|
|
5729
|
+
});
|
|
5730
|
+
}
|
|
5731
|
+
|
|
5732
|
+
function normalizeFieldPermissionsForPlan(fieldPermissions) {
|
|
5733
|
+
if (!Array.isArray(fieldPermissions) || !fieldPermissions.length) return undefined;
|
|
5734
|
+
return fieldPermissions
|
|
5735
|
+
.map(item => ({
|
|
5736
|
+
field: item.field,
|
|
5737
|
+
value: item.value,
|
|
5738
|
+
}))
|
|
5739
|
+
.sort((left, right) => `${left.field || ''}:${left.value || ''}`.localeCompare(`${right.field || ''}:${right.value || ''}`));
|
|
5740
|
+
}
|
|
5741
|
+
|
|
5364
5742
|
function resolveManifestPlainJson(item, objectKey, fileKey, optional = false) {
|
|
5365
5743
|
if (item[objectKey] !== undefined) return item[objectKey];
|
|
5366
5744
|
if (item[fileKey]) {
|
|
@@ -5510,6 +5888,37 @@ function readJsonArg(value, label) {
|
|
|
5510
5888
|
return readJsonArgWithBase(value, label).data;
|
|
5511
5889
|
}
|
|
5512
5890
|
|
|
5891
|
+
function buildDataViewQueryBody(flags = {}) {
|
|
5892
|
+
const explicit = flags['query-json'] || flags['params-json'] || flags['body-json'];
|
|
5893
|
+
if (explicit) return readJsonArg(explicit, explicit === flags['query-json'] ? 'query-json' : 'params-json');
|
|
5894
|
+
const body = {};
|
|
5895
|
+
const fields = splitList(flags.fields || flags.field);
|
|
5896
|
+
if (fields.length > 0) body.fields = fields;
|
|
5897
|
+
if (flags['filters-json']) body.filters = readJsonArg(flags['filters-json'], 'filters-json');
|
|
5898
|
+
if (flags['filter-json']) body.filters = readJsonArg(flags['filter-json'], 'filter-json');
|
|
5899
|
+
if (flags['condition-type']) body.conditionType = String(flags['condition-type']).toUpperCase();
|
|
5900
|
+
if (flags.keyword || flags.search) body.searchKeyWord = flags.keyword || flags.search;
|
|
5901
|
+
if (flags.page) body.currentPage = Number(flags.page);
|
|
5902
|
+
if (flags['page-size'] || flags.limit) body.pageSize = Number(flags['page-size'] || flags.limit);
|
|
5903
|
+
if (flags['order-json']) {
|
|
5904
|
+
body.order = readJsonArg(flags['order-json'], 'order-json');
|
|
5905
|
+
} else if (flags['sort-field'] || flags.sort) {
|
|
5906
|
+
const field = flags['sort-field'] || flags.sort;
|
|
5907
|
+
body.order = [
|
|
5908
|
+
{
|
|
5909
|
+
field,
|
|
5910
|
+
isAsc:
|
|
5911
|
+
String(flags.desc || flags.direction || '').toLowerCase() === 'desc'
|
|
5912
|
+
? 'n'
|
|
5913
|
+
: flags.asc === false
|
|
5914
|
+
? 'n'
|
|
5915
|
+
: 'y',
|
|
5916
|
+
},
|
|
5917
|
+
];
|
|
5918
|
+
}
|
|
5919
|
+
return body;
|
|
5920
|
+
}
|
|
5921
|
+
|
|
5513
5922
|
function readJsonArgWithBase(value, label) {
|
|
5514
5923
|
if (!value) fail(`缺少 ${label}`);
|
|
5515
5924
|
const raw = String(value).trim();
|
package/lib/workspace-init.js
CHANGED
|
@@ -85,12 +85,13 @@ When the user provides a root domain such as `https://yida.wisejob.cn/`, use it
|
|
|
85
85
|
- Shared workspace env values such as `APP_OSS_*` should live in `~/.openxiangda/.env` by default. Project `.env` files are only per-workspace overrides.
|
|
86
86
|
- For suspected platform defects, bugs, or product optimization requests, ask the user to confirm first, then use `openxiangda feedback preview` and `openxiangda feedback submit --yes` to send a detailed DingTalk robot report. Include the command, error, relevant files, and logs/context files when available. The CLI redacts tokens, cookies, secrets, phone numbers, and emails before sending.
|
|
87
87
|
- For form pages, every visible field should have a concise user-facing `placeholder`. Use `tips` only for special constraints or non-obvious business rules; do not add tips to every field.
|
|
88
|
-
- Form pages should display only fields the user needs to see. Use `SelectField` / `RadioField` for enums
|
|
89
|
-
- Permission scope keys, computed fields, sync fields, and developer/internal fields should be present only when needed for permissions or logic, and should normally be derived from
|
|
88
|
+
- Form pages should display only fields the user needs to see. Use `SelectField` / `RadioField` for enums. For data maintained by another form, such as class, college, customer, or project, use `SelectField` with `optionSource.type: "linkedForm"` so the runtime queries form data through the SDK and builds dropdown options; set `remoteSearch: true` and an explicit `searchFieldId` when the source data can be large. Do not make users maintain raw ID text fields, and do not use `AssociationFormField` for new form work.
|
|
89
|
+
- Permission scope keys, computed fields, sync fields, and developer/internal fields should be present only when needed for permissions or logic, and should normally be derived from visible select/person/department fields and marked `behavior: "HIDDEN"`. For select-derived scalar keys, use `valueSync`.
|
|
90
90
|
- Do not add extra fields for platform system metadata such as creator, updater, creator department, updater department, created time, or updated time unless the user explicitly needs a separate business field; the platform already creates system fields for each form.
|
|
91
91
|
- All visible copy in forms and pages must be written for end users. Do not put implementation notes, developer explanations, schema descriptions, or "this area is generated by..." text into page sections, cards, labels, tips, or empty states.
|
|
92
92
|
- Use logical resource codes in local files. Platform-specific IDs such as `formUuid`, `pageId`, `workflowId`, and `automationId` must be isolated by profile.
|
|
93
93
|
- Put engineering-managed resources in `src/resources/` and use `openxiangda resource validate|plan|publish|pull`. `workspace publish` publishes workspace forms/pages first, then runs non-destructive resource upsert. Only pass `--prune` when the user explicitly wants local manifests to delete platform-side extras.
|
|
94
|
+
- For repeated read-only multi-form queries, create a data view manifest in `src/resources/data-views/` and query it with `sdk.dataView.query` or `sdk.dataSource.run`. Use data views for joined list/report/lookup sources where refresh lag is acceptable; do not use them for single-form CRUD, simple linkedForm selects, real-time writes, or write-back. Read `references/data-views.md` before designing one.
|
|
94
95
|
- For external APIs, create a connector manifest in `src/resources/connectors/` and call it from pages through `sdk.connector`; never put third-party API keys in page source.
|
|
95
96
|
- Before scaffolding a real app, complex page, data management page, portal shell, status lifecycle, role governance, or automation, read `references/best-practices.md` and pick the architecture first. Prefer copying the relevant pattern from `examples/best-practices/` into `src/` instead of generating a single large page file.
|
|
96
97
|
- Before publishing to another platform, verify the workspace is bound for that profile. Resource IDs from one profile must not be reused for another profile.
|
|
@@ -127,6 +128,7 @@ Core CLI / state:
|
|
|
127
128
|
- `references/openxiangda-api.md` — `/openxiangda-api/v1` request and response fields.
|
|
128
129
|
- `references/workspace-state.md` — `.openxiangda/state.json` shape and profile isolation rules.
|
|
129
130
|
- `references/connector-resources.md` — `src/resources` manifests, connector schema, and SDK connector calls.
|
|
131
|
+
- `references/data-views.md` — `src/resources/data-views` materialized view resources, DSL, permissions, refresh, CLI commands, and runtime SDK usage.
|
|
130
132
|
- `references/notifications.md` — `src/resources/notifications`, notification templates/type bindings, and `sdk.notification` / `ctx.notification`.
|
|
131
133
|
- `references/best-practices.md` — initialized examples for modular pages, state lifecycles, role governance, permission isolation, high-performance queries, portal shells, workflow boundaries, and automation patterns.
|
|
132
134
|
|
|
@@ -151,7 +153,7 @@ Workflow / automation / permissions:
|
|
|
151
153
|
Platform domain knowledge (read these first when generating forms or pages so the output matches the live platform behavior):
|
|
152
154
|
|
|
153
155
|
- `references/platform-data-model.md` — how the platform persists field values (JSONB, `{label, value}` for option fields, attachment shape, etc.). Use this whenever you write, render, or diagnose form data.
|
|
154
|
-
- `references/style-system.md` —
|
|
156
|
+
- `references/style-system.md` — style isolation defaults, legacy namespace compatibility, and flexible Tailwind/CSS guidance. Use this before writing substantial page or form CSS.
|
|
155
157
|
- `references/architecture-patterns.md` — CRUD data flow, `DataManagementList` pattern, recommended directory layout for `src/pages` and `src/forms`. Use this before scaffolding a new page or list view.
|
|
156
158
|
- `references/best-practices.md` — read this before implementing app-level business patterns; it points to `examples/best-practices/` and explains when to use status fields instead of workflow.
|
|
157
159
|
- `references/component-guide.md` — when to pick platform components vs. raw Ant Design vs. custom components, with the rules for option fields. Use this before introducing a new component.
|