openxiangda 1.0.22 → 1.0.25

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.
Files changed (94) hide show
  1. package/README.md +28 -10
  2. package/lib/cli.js +351 -20
  3. package/lib/workspace-init.js +13 -0
  4. package/openxiangda-skills/SKILL.md +26 -10
  5. package/openxiangda-skills/references/architecture-patterns.md +44 -22
  6. package/openxiangda-skills/references/best-practices.md +180 -0
  7. package/openxiangda-skills/references/component-guide.md +34 -8
  8. package/openxiangda-skills/references/pages/publish-flow.md +26 -0
  9. package/openxiangda-skills/references/pages/workspace-structure.md +5 -3
  10. package/openxiangda-skills/references/workspace-state.md +6 -0
  11. package/openxiangda-skills/skills/openxiangda-app/SKILL.md +12 -7
  12. package/openxiangda-skills/skills/openxiangda-core/SKILL.md +34 -4
  13. package/openxiangda-skills/skills/openxiangda-form/SKILL.md +13 -1
  14. package/openxiangda-skills/skills/openxiangda-page/SKILL.md +22 -1
  15. package/openxiangda-skills/skills/openxiangda-permission-settings/SKILL.md +3 -0
  16. package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +7 -0
  17. package/package.json +1 -1
  18. package/packages/sdk/src/build-source/scripts/publish-all.mjs +44 -5
  19. package/packages/sdk/src/build-source/scripts/utils/incremental.mjs +95 -0
  20. package/packages/sdk/src/build-source/scripts/utils/incremental.test.ts +62 -0
  21. package/templates/sy-lowcode-app-workspace/examples/best-practices/README.md +32 -0
  22. package/templates/sy-lowcode-app-workspace/examples/best-practices/catalog.json +61 -0
  23. package/templates/sy-lowcode-app-workspace/examples/best-practices/decision-guide.md +44 -0
  24. package/templates/sy-lowcode-app-workspace/examples/best-practices/design-style.md +36 -0
  25. package/templates/sy-lowcode-app-workspace/examples/best-practices/module-structure.md +48 -0
  26. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/index.ts +2 -0
  27. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/permissions.test.ts +35 -0
  28. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/permissions.ts +24 -0
  29. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/types.ts +17 -0
  30. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/index.ts +4 -0
  31. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/permissions.test.ts +42 -0
  32. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/permissions.ts +23 -0
  33. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/state-machine.test.ts +63 -0
  34. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/state-machine.ts +73 -0
  35. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/ticket-query.test.ts +34 -0
  36. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/ticket-query.ts +73 -0
  37. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/types.ts +64 -0
  38. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/app-role/page.tsx +1 -0
  39. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/app-role/schema.ts +57 -0
  40. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/customer-profile/page.tsx +1 -0
  41. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/customer-profile/schema.ts +83 -0
  42. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/service-ticket/page.tsx +1 -0
  43. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/service-ticket/schema.ts +97 -0
  44. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/ticket-action-log/page.tsx +1 -0
  45. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/ticket-action-log/schema.ts +65 -0
  46. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/js-code-nodes/daily_ticket_digest/index.ts +44 -0
  47. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/js-code-nodes/sync_roles_to_platform/index.ts +33 -0
  48. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/App.tsx +7 -0
  49. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/WorkbenchPage.tsx +36 -0
  50. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/components/ConfigPanel.tsx +34 -0
  51. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/components/PreviewPanel.tsx +17 -0
  52. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/index.tsx +10 -0
  53. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/page.config.ts +9 -0
  54. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/reducer.ts +29 -0
  55. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/styles.css +24 -0
  56. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/App.tsx +7 -0
  57. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/MobilePortalShell.tsx +31 -0
  58. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/index.tsx +10 -0
  59. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/modules/MobileHome.tsx +13 -0
  60. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/page.config.ts +14 -0
  61. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/routes.ts +13 -0
  62. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/styles.css +11 -0
  63. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/App.tsx +7 -0
  64. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/PcPortalShell.tsx +35 -0
  65. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/components/PortalMetric.tsx +11 -0
  66. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/index.tsx +10 -0
  67. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/modules/HomeModule.tsx +25 -0
  68. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/modules/TicketsModule.tsx +14 -0
  69. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/page.config.ts +14 -0
  70. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/routes.ts +19 -0
  71. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/styles.css +35 -0
  72. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/App.tsx +7 -0
  73. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/TicketOpsPage.tsx +105 -0
  74. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/components/TicketActionTimeline.tsx +22 -0
  75. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/components/TicketDetailDrawer.tsx +41 -0
  76. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/components/TicketTableActions.tsx +55 -0
  77. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/index.tsx +10 -0
  78. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/page.config.ts +9 -0
  79. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/styles.css +35 -0
  80. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/automations/daily-ticket-digest/automation.json +25 -0
  81. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/automations/daily-ticket-digest/trigger.json +9 -0
  82. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/notifications/daily-ticket-digest.json +24 -0
  83. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/permissions/form-groups/service-ticket-college.json +21 -0
  84. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/permissions/roles.json +17 -0
  85. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/workflows/expense-approval-workflow.json +48 -0
  86. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/ConfirmAction.tsx +22 -0
  87. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/QueryState.tsx +37 -0
  88. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/StatusTag.tsx +20 -0
  89. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/hooks/useTicketOps.ts +96 -0
  90. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/services/role-governance.ts +48 -0
  91. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/services/service-ticket.ts +113 -0
  92. package/templates/sy-lowcode-app-workspace/package.json +2 -0
  93. package/templates/sy-lowcode-app-workspace/src/dev/App.tsx +11 -1
  94. package/templates/sy-lowcode-app-workspace/tsconfig.examples.json +24 -0
@@ -34,6 +34,8 @@ Use role codes in permission group JSON. Bind existing role IDs only inside the
34
34
  openxiangda permission role-bind sales --role-id <id> --profile dev
35
35
  ```
36
36
 
37
+ For dynamic multi-role apps, do not hardcode role behavior only in page code. Create a role maintenance form, sync it to platform roles with automation / JS_CODE, and add redundant ownership fields to business forms (`collegeId`, `classId`, `ownerDeptId`, `ownerUserId`, `roleCode`). Use page permission groups for entry visibility and form permission groups with condition-based data permissions for real data isolation.
38
+
37
39
  ## Page Permission Groups
38
40
 
39
41
  Page permission groups control menu/page visibility:
@@ -92,5 +94,6 @@ Do not store platform-specific public access IDs locally. The CLI resolves by `a
92
94
  ## References
93
95
 
94
96
  - Permission model and examples: `../../references/permissions-settings.md`
97
+ - Dynamic role governance and data-isolation pattern: `../../references/best-practices.md`
95
98
  - Profile-isolated IDs: `../../references/workspace-state.md`
96
99
  - API fields: `../../references/openxiangda-api.md`
@@ -7,6 +7,12 @@ description: Build, validate, publish, enable, and inspect OpenXiangda workflow
7
7
 
8
8
  Use this skill when the user asks for approval workflows, workflow forms, automation rules, scheduled jobs, form-event triggers, or process-triggered automation in OpenXiangda.
9
9
 
10
+ ## Architecture Boundary
11
+
12
+ Most business lifecycle flows are not workflows. For ordinary status changes such as `pending -> processing -> resolved -> closed`, use a normal form, a `status` field, redundant responsibility fields, an action-log form, a domain state machine, a service method, and optional automation / JS_CODE.
13
+
14
+ Create workflow definitions only when the scenario has real approval semantics: approvers, approval tasks, agree/reject actions, approval opinions, node-level permissions, process records, or approval-node side effects.
15
+
10
16
  ## Required Context
11
17
 
12
18
  Before any write:
@@ -101,6 +107,7 @@ Use `automation disable` before risky edits. Published automations create a draf
101
107
 
102
108
  ## References
103
109
 
110
+ - Best-practice architecture and status-flow boundary: `../../references/best-practices.md`
104
111
  - Workflow v3 JSON: `../../references/workflow-v3.md`
105
112
  - Automation v3 JSON and triggers: `../../references/automation-v3.md`
106
113
  - Notification resources and runtime calls: `../../references/notifications.md`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda",
3
- "version": "1.0.22",
3
+ "version": "1.0.25",
4
4
  "description": "OpenXiangda CLI, workspace build tools, runtime SDK, and form components.",
5
5
  "private": false,
6
6
  "bin": {
@@ -17,6 +17,7 @@ import {
17
17
  normalizeOnly,
18
18
  planIncrementalPublish,
19
19
  printPlan,
20
+ resolveGitChangedWorkspaceTargets,
20
21
  } from "./utils/incremental.mjs";
21
22
 
22
23
  const require = createRequire(import.meta.url);
@@ -36,11 +37,8 @@ const dryRun = Boolean(args["dry-run"]);
36
37
  const targetForm = args.form || "";
37
38
  const targetPage = args.page || "";
38
39
  const force = Boolean(args.force);
39
- const only = targetForm
40
- ? [`forms/${targetForm}`]
41
- : targetPage
42
- ? [`pages/${targetPage}`]
43
- : normalizeOnly(args.only);
40
+ const changedOnly = Boolean(args.changed || args["changed-only"]);
41
+ const changedSince = args.since || "HEAD";
44
42
 
45
43
  if (args.help || args.h) {
46
44
  console.log(`
@@ -54,6 +52,8 @@ publish-all - 同步、构建、上传并注册应用工作区产物
54
52
  --form <name> 只发布指定表单
55
53
  --page <name> 只发布指定代码页
56
54
  --only <list> 只发布指定模块,如 forms/customer,pages/dashboard
55
+ --changed 只发布 git 变更触达的 src/forms/* 和 src/pages/*
56
+ --since <ref> 配合 --changed 使用,默认 HEAD
57
57
  --force 忽略增量缓存,强制发布
58
58
  --help, -h 显示帮助信息
59
59
  `);
@@ -64,6 +64,45 @@ if (targetForm && targetPage) {
64
64
  console.error("❌ --form 和 --page 不能同时使用,请分两次发布");
65
65
  process.exit(1);
66
66
  }
67
+ if (changedOnly && (targetForm || targetPage || args.only)) {
68
+ console.error("❌ --changed 不能与 --form、--page 或 --only 同时使用");
69
+ process.exit(1);
70
+ }
71
+
72
+ const changedTargets = changedOnly
73
+ ? resolveGitChangedWorkspaceTargets({ since: changedSince })
74
+ : null;
75
+ if (changedTargets && !changedTargets.available) {
76
+ console.error(`❌ 无法读取 git 变更: ${changedTargets.error}`);
77
+ process.exit(1);
78
+ }
79
+ if (changedTargets?.globalFiles?.length) {
80
+ console.warn(
81
+ `[publish] --changed 检测到 shared/config 变更: ${changedTargets.globalFiles.join(", ")}`,
82
+ );
83
+ console.warn(
84
+ "[publish] 将只发布直接变更的 forms/pages;若 shared 改动影响其他模块,请显式使用 --only 或不传 --changed。",
85
+ );
86
+ }
87
+ if (changedTargets?.resourceFiles?.length) {
88
+ console.warn(
89
+ `[publish] --changed 检测到资源变更: ${changedTargets.resourceFiles.join(", ")}`,
90
+ );
91
+ console.warn("[publish] publish-all 只处理 forms/pages;资源请通过 openxiangda resource plan|publish 处理。");
92
+ }
93
+
94
+ const only = changedOnly
95
+ ? changedTargets.only
96
+ : targetForm
97
+ ? [`forms/${targetForm}`]
98
+ : targetPage
99
+ ? [`pages/${targetPage}`]
100
+ : normalizeOnly(args.only);
101
+
102
+ if (changedOnly && only.length === 0) {
103
+ console.log("✅ 没有检测到 src/forms 或 src/pages 的 git 变更,跳过发布");
104
+ process.exit(0);
105
+ }
67
106
 
68
107
  const buildId = process.env.APP_BUILD_ID || createBuildId();
69
108
  const childEnv = {
@@ -1,4 +1,5 @@
1
1
  import crypto from "node:crypto";
2
+ import { spawnSync } from "node:child_process";
2
3
  import fs from "node:fs";
3
4
  import path from "node:path";
4
5
  import { rootDir } from "./load-config.mjs";
@@ -124,6 +125,100 @@ export function normalizeOnly(value) {
124
125
  .filter(Boolean);
125
126
  }
126
127
 
128
+ function runGit(args) {
129
+ const result = spawnSync("git", args, {
130
+ cwd: rootDir,
131
+ encoding: "utf-8",
132
+ });
133
+ if (result.error || result.status !== 0) {
134
+ return {
135
+ ok: false,
136
+ error: result.error?.message || result.stderr || result.stdout || "git command failed",
137
+ stdout: "",
138
+ };
139
+ }
140
+ return {
141
+ ok: true,
142
+ error: "",
143
+ stdout: result.stdout || "",
144
+ };
145
+ }
146
+
147
+ function uniqueSorted(values) {
148
+ return [...new Set(values)].sort((left, right) => left.localeCompare(right));
149
+ }
150
+
151
+ function parseGitNameOnly(stdout) {
152
+ return String(stdout || "")
153
+ .split(/\r?\n/)
154
+ .map((item) => item.trim().replace(/\\/g, "/"))
155
+ .filter(Boolean);
156
+ }
157
+
158
+ function workspaceTargetForFile(filePath) {
159
+ const matched = filePath.match(/^src\/(forms|pages)\/([^/]+)\//);
160
+ if (!matched) return null;
161
+ return `${matched[1]}/${matched[2]}`;
162
+ }
163
+
164
+ function isGlobalWorkspaceFile(filePath) {
165
+ return (
166
+ filePath === "package.json" ||
167
+ filePath === "pnpm-lock.yaml" ||
168
+ filePath === "package-lock.json" ||
169
+ filePath === "yarn.lock" ||
170
+ filePath === "app-workspace.config.ts" ||
171
+ filePath === "tailwind.config.cjs" ||
172
+ filePath === "postcss.config.cjs" ||
173
+ filePath === "vite.config.ts" ||
174
+ filePath === "tsconfig.json" ||
175
+ filePath === "src/index.css" ||
176
+ filePath.startsWith("src/shared/")
177
+ );
178
+ }
179
+
180
+ export function resolveGitChangedWorkspaceTargets(options = {}) {
181
+ const since = options.since || "HEAD";
182
+ const insideWorkTree = runGit(["rev-parse", "--is-inside-work-tree"]);
183
+ if (!insideWorkTree.ok || insideWorkTree.stdout.trim() !== "true") {
184
+ return {
185
+ available: false,
186
+ error: insideWorkTree.error || "not a git worktree",
187
+ files: [],
188
+ only: [],
189
+ resourceFiles: [],
190
+ globalFiles: [],
191
+ };
192
+ }
193
+
194
+ const changed = runGit(["diff", "--name-only", "--relative", since, "--"]);
195
+ if (!changed.ok) {
196
+ return {
197
+ available: false,
198
+ error: changed.error,
199
+ files: [],
200
+ only: [],
201
+ resourceFiles: [],
202
+ globalFiles: [],
203
+ };
204
+ }
205
+
206
+ const untracked = runGit(["ls-files", "--others", "--exclude-standard"]);
207
+ const files = uniqueSorted([
208
+ ...parseGitNameOnly(changed.stdout),
209
+ ...(untracked.ok ? parseGitNameOnly(untracked.stdout) : []),
210
+ ]);
211
+
212
+ return {
213
+ available: true,
214
+ error: "",
215
+ files,
216
+ only: uniqueSorted(files.map(workspaceTargetForFile).filter(Boolean)),
217
+ resourceFiles: files.filter((item) => item.startsWith("src/resources/")),
218
+ globalFiles: files.filter(isGlobalWorkspaceFile),
219
+ };
220
+ }
221
+
127
222
  function createIncrementalPlan(modules, options = {}, cache = null) {
128
223
  const only = normalizeOnly(options.only);
129
224
  const selected = only.length
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
+ import { spawnSync } from "node:child_process";
4
5
 
5
6
  import { afterEach, describe, expect, it, vi } from "vitest";
6
7
 
@@ -31,6 +32,24 @@ function createWorkspace() {
31
32
  return dir;
32
33
  }
33
34
 
35
+ function runGit(workspaceRoot: string, args: string[]) {
36
+ const result = spawnSync("git", args, {
37
+ cwd: workspaceRoot,
38
+ encoding: "utf-8",
39
+ });
40
+ if (result.status !== 0) {
41
+ throw new Error(`git ${args.join(" ")} failed: ${result.stderr || result.stdout}`);
42
+ }
43
+ }
44
+
45
+ function commitWorkspace(workspaceRoot: string) {
46
+ runGit(workspaceRoot, ["init"]);
47
+ runGit(workspaceRoot, ["config", "user.email", "test@example.com"]);
48
+ runGit(workspaceRoot, ["config", "user.name", "Test User"]);
49
+ runGit(workspaceRoot, ["add", "."]);
50
+ runGit(workspaceRoot, ["commit", "-m", "initial"]);
51
+ }
52
+
34
53
  async function loadIncremental(rootDir: string) {
35
54
  const previousRoot = process.env.LOWCODE_WORKSPACE_ROOT;
36
55
  process.env.LOWCODE_WORKSPACE_ROOT = rootDir;
@@ -75,4 +94,47 @@ describe("incremental publish cache", () => {
75
94
 
76
95
  expect(incremental.planIncrementalPublish(modules).changed).toHaveLength(0);
77
96
  });
97
+
98
+ it("resolves git changed files to workspace publish targets", async () => {
99
+ const workspaceRoot = createWorkspace();
100
+ commitWorkspace(workspaceRoot);
101
+
102
+ fs.writeFileSync(
103
+ path.join(workspaceRoot, "src", "pages", "dashboard", "index.tsx"),
104
+ "export default function Dashboard() { return 'changed'; }\n",
105
+ "utf-8",
106
+ );
107
+ fs.mkdirSync(path.join(workspaceRoot, "src", "forms", "customer"), {
108
+ recursive: true,
109
+ });
110
+ fs.writeFileSync(
111
+ path.join(workspaceRoot, "src", "forms", "customer", "schema.ts"),
112
+ "export default { code: 'customer' };\n",
113
+ "utf-8",
114
+ );
115
+ fs.mkdirSync(path.join(workspaceRoot, "src", "resources"), {
116
+ recursive: true,
117
+ });
118
+ fs.writeFileSync(
119
+ path.join(workspaceRoot, "src", "resources", "menus.json"),
120
+ "[]\n",
121
+ "utf-8",
122
+ );
123
+ fs.mkdirSync(path.join(workspaceRoot, "src", "shared"), {
124
+ recursive: true,
125
+ });
126
+ fs.writeFileSync(
127
+ path.join(workspaceRoot, "src", "shared", "format.ts"),
128
+ "export const format = String;\n",
129
+ "utf-8",
130
+ );
131
+
132
+ const incremental = await loadIncremental(workspaceRoot);
133
+ const changed = incremental.resolveGitChangedWorkspaceTargets();
134
+
135
+ expect(changed.available).toBe(true);
136
+ expect(changed.only).toEqual(["forms/customer", "pages/dashboard"]);
137
+ expect(changed.resourceFiles).toEqual(["src/resources/menus.json"]);
138
+ expect(changed.globalFiles).toEqual(["src/shared/format.ts"]);
139
+ });
78
140
  });
@@ -0,0 +1,32 @@
1
+ # OpenXiangda Best Practices
2
+
3
+ These examples are intentionally placed under `examples/best-practices/`.
4
+ They are copied by `workspace init`, but they are not scanned by
5
+ `lowcode-workspace build` or `openxiangda workspace publish`.
6
+
7
+ Copy the pieces you need into `src/`:
8
+
9
+ ```bash
10
+ cp -R examples/best-practices/src/domain/service-ticket src/domain/
11
+ cp -R examples/best-practices/src/shared/services/service-ticket.ts src/shared/services/
12
+ cp -R examples/best-practices/src/pages/service-ticket-ops src/pages/
13
+ cp -R examples/best-practices/src/forms/service-ticket src/forms/
14
+ ```
15
+
16
+ Run `pnpm examples:check` after editing examples, and run the normal
17
+ `pnpm check` after copying code into `src/`.
18
+
19
+ ## Core Rules
20
+
21
+ - Keep view code thin. Pages compose components, hooks, and services.
22
+ - Put business rules in `domain/`; keep it free of React, SDK, and UI imports.
23
+ - Put platform calls in `shared/services/`.
24
+ - Put reusable loading, empty, error, status, and confirmation UI in
25
+ `shared/components/`.
26
+ - Use state fields and operation logs for business lifecycle flows.
27
+ - Use workflow only for real approval tasks.
28
+ - Query with pagination and structured conditions. Avoid large page sizes,
29
+ client-side filtering, and broad `searchKeyWord` queries.
30
+
31
+ Read `decision-guide.md`, `module-structure.md`, and `design-style.md` before
32
+ using any template.
@@ -0,0 +1,61 @@
1
+ {
2
+ "version": 1,
3
+ "publishedByDefault": false,
4
+ "copyInto": "src/",
5
+ "templates": [
6
+ {
7
+ "code": "customer-profile",
8
+ "type": "form",
9
+ "path": "src/forms/customer-profile",
10
+ "description": "Standard form schema covering field types, validation, users, departments, attachments, and subforms."
11
+ },
12
+ {
13
+ "code": "service-ticket-lifecycle",
14
+ "type": "state-flow",
15
+ "path": "src/domain/service-ticket",
16
+ "description": "Lifecycle state machine with ticket and action-log forms. Does not use workflow."
17
+ },
18
+ {
19
+ "code": "service-ticket-ops",
20
+ "type": "page",
21
+ "path": "src/pages/service-ticket-ops",
22
+ "description": "DataManagementList-based ops page with modular actions, drawer, timeline, hook, and service."
23
+ },
24
+ {
25
+ "code": "role-governance",
26
+ "type": "permissions",
27
+ "path": "src/domain/role-governance",
28
+ "description": "Dynamic role maintenance, role sync, redundant ownership fields, and permission-group resource examples."
29
+ },
30
+ {
31
+ "code": "pc-portal-shell",
32
+ "type": "app-shell-page",
33
+ "path": "src/pages/pc-portal-shell",
34
+ "description": "PC app-shell entry with routes, modules, shared domain usage, and no hardcoded view URLs."
35
+ },
36
+ {
37
+ "code": "mobile-portal-shell",
38
+ "type": "app-shell-page",
39
+ "path": "src/pages/mobile-portal-shell",
40
+ "description": "Mobile app-shell entry sharing domain/service logic with the PC portal."
41
+ },
42
+ {
43
+ "code": "interactive-workbench",
44
+ "type": "page",
45
+ "path": "src/pages/interactive-workbench",
46
+ "description": "Pure interaction workbench with reducer state, modular panels, and unified query states."
47
+ },
48
+ {
49
+ "code": "expense-approval-workflow",
50
+ "type": "workflow",
51
+ "path": "src/resources/workflows/expense-approval-workflow.json",
52
+ "description": "True approval workflow example. Use only when approval tasks are required."
53
+ },
54
+ {
55
+ "code": "daily-ticket-digest",
56
+ "type": "automation",
57
+ "path": "src/resources/automations/daily-ticket-digest",
58
+ "description": "Scheduled JS_CODE automation that queries paginated data and sends notification resources."
59
+ }
60
+ ]
61
+ }
@@ -0,0 +1,44 @@
1
+ # Decision Guide
2
+
3
+ ## Page And Data Shape
4
+
5
+ Use a form when you need a data table. Each form schema defines fields,
6
+ validation, storage shape, and generated platform data APIs.
7
+
8
+ Use a code page when you need a workbench, portal, dashboard, cross-form
9
+ composition, custom list actions, or complex interaction.
10
+
11
+ Use `DataManagementList` for list/search/export/batch-management pages. Extend
12
+ it with row actions, custom renderers, and drawers instead of rebuilding
13
+ pagination, export, and filters from scratch.
14
+
15
+ ## State Flow Is Not Workflow
16
+
17
+ Most business "processes" are lifecycle state changes:
18
+
19
+ - work tickets: new -> accepted -> processing -> resolved -> closed
20
+ - orders: draft -> submitted -> paid -> fulfilled -> completed
21
+ - assets: available -> reserved -> in_use -> maintenance -> retired
22
+
23
+ Use a normal form with a `status` field, ownership fields, and an operation-log
24
+ form. Define allowed transitions in `domain/<feature>/state-machine.ts`, and
25
+ execute changes through a service method that updates state and writes logs.
26
+
27
+ Use workflow only when the platform must create approval tasks with approvers,
28
+ approval comments, agree/reject actions, copy nodes, node field permissions, and
29
+ approval history.
30
+
31
+ ## Automation And JS_CODE
32
+
33
+ Use automation or workflow JS_CODE when logic must run on the backend after a
34
+ trigger: scheduled scans, cross-form synchronization, notification fan-out,
35
+ external HTTP calls, or platform role synchronization.
36
+
37
+ Frontend code must not be responsible for data isolation or background jobs.
38
+
39
+ ## Permissions
40
+
41
+ For dynamic multi-role apps, create a business role table and synchronize it to
42
+ platform app roles. Add redundant ownership fields to business forms, such as
43
+ `collegeId`, `classId`, `ownerDeptId`, and `ownerUserId`. Use form permission
44
+ groups with condition-style data permissions to enforce data isolation.
@@ -0,0 +1,36 @@
1
+ # Design Style
2
+
3
+ OpenXiangda business apps should feel like focused operational tools.
4
+
5
+ ## Interaction Defaults
6
+
7
+ - Every async page has loading, refreshing, empty, error, and submit-pending
8
+ states.
9
+ - List refresh should keep existing rows visible and show a small refresh state.
10
+ - Mutating actions need confirmation, pending feedback, success feedback, and a
11
+ failure refresh or rollback path.
12
+ - Mobile actions should use a bottom action area, drawer, or action sheet; do
13
+ not copy a dense PC table toolbar to mobile.
14
+
15
+ ## Layout Defaults
16
+
17
+ - Use dense but readable workbench layouts for admin and ops pages.
18
+ - Put high-value filters near the list, not in hidden configuration pages.
19
+ - Use status tags, responsibility fields, and latest action summaries for
20
+ lifecycle data.
21
+ - Do not build marketing-style hero pages for business tools.
22
+
23
+ ## Styling Rules
24
+
25
+ - Use platform CSS variables, Tailwind semantic classes, Ant Design, and
26
+ antd-mobile.
27
+ - Use mature packages for mature interactions: antd controls instead of native
28
+ inputs, ECharts for charts, GSAP for complex animation timelines, and
29
+ maintained drag/drop or virtual-list libraries when those behaviors are
30
+ required.
31
+ - Research package docs and current maintenance before adding a new dependency;
32
+ write business adapters instead of copying library internals.
33
+ - Keep page styles in `styles.css`; avoid large inline style objects.
34
+ - Keep reusable visual states in shared components: status tags, query states,
35
+ confirmation triggers, and operation timelines.
36
+ - Do not override private Ant Design class names.
@@ -0,0 +1,48 @@
1
+ # Module Structure
2
+
3
+ Use this structure for substantial features:
4
+
5
+ ```text
6
+ src/
7
+ domain/<feature>/
8
+ types.ts
9
+ state-machine.ts
10
+ permissions.ts
11
+ query.ts
12
+ index.ts
13
+ shared/
14
+ services/<feature>.ts
15
+ hooks/use<Feature>.ts
16
+ components/
17
+ QueryState.tsx
18
+ StatusTag.tsx
19
+ pages/<feature>/
20
+ page.config.ts
21
+ index.tsx
22
+ App.tsx
23
+ styles.css
24
+ components/
25
+ forms/<feature>/
26
+ schema.ts
27
+ page.tsx
28
+ ```
29
+
30
+ ## Dependency Direction
31
+
32
+ ```text
33
+ pages -> shared/hooks -> shared/services -> domain
34
+ forms -> shared -> domain
35
+ ```
36
+
37
+ - `domain/` has no React, Ant Design, SDK, or page imports.
38
+ - `shared/` does not import from `pages/`.
39
+ - `pages/` compose modules and call hooks; they do not build complex query
40
+ payloads inline.
41
+ - PC and mobile views can have different components and styles, but they reuse
42
+ the same domain and service logic.
43
+
44
+ ## File Size
45
+
46
+ Avoid large single-file pages. If a page grows beyond roughly 250 lines, split
47
+ table actions, drawers, timelines, query builders, hooks, and styles into
48
+ separate files.
@@ -0,0 +1,2 @@
1
+ export * from "./permissions";
2
+ export * from "./types";
@@ -0,0 +1,35 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+
4
+ import { buildConditionDataPermission, buildRoleCode } from "./permissions";
5
+
6
+ describe("role-governance permission helpers", () => {
7
+ it("normalizes dynamic role codes", () => {
8
+ assert.equal(buildRoleCode({ roleCode: " College Admin " }), "college_admin");
9
+ assert.equal(buildRoleCode({ roleCode: "class advisor" }), "class_advisor");
10
+ });
11
+
12
+ it("builds condition-based data permissions from redundant fields", () => {
13
+ const permission = buildConditionDataPermission({
14
+ collegeId: "college_science",
15
+ classId: "",
16
+ ownerDeptId: "dept_lab",
17
+ });
18
+
19
+ assert.equal(permission.type, "condition");
20
+ assert.deepEqual(permission.condition.rules, [
21
+ {
22
+ field: "collegeId",
23
+ componentType: "Text",
24
+ op: "=",
25
+ value: "college_science",
26
+ },
27
+ {
28
+ field: "ownerDeptId",
29
+ componentType: "Text",
30
+ op: "=",
31
+ value: "dept_lab",
32
+ },
33
+ ]);
34
+ });
35
+ });
@@ -0,0 +1,24 @@
1
+ import type { AppRoleRecord, DataOwnershipFields } from "./types";
2
+
3
+ export function buildRoleCode(role: Pick<AppRoleRecord, "roleCode">) {
4
+ return role.roleCode.trim().replace(/\s+/g, "_").toLowerCase();
5
+ }
6
+
7
+ export function buildConditionDataPermission(scope: DataOwnershipFields) {
8
+ const rules = Object.entries(scope)
9
+ .filter(([, value]) => Boolean(value))
10
+ .map(([field, value]) => ({
11
+ field,
12
+ componentType: "Text",
13
+ op: "=",
14
+ value,
15
+ }));
16
+
17
+ return {
18
+ type: "condition" as const,
19
+ condition: {
20
+ logic: "AND" as const,
21
+ rules,
22
+ },
23
+ };
24
+ }
@@ -0,0 +1,17 @@
1
+ export interface AppRoleRecord {
2
+ formInstanceId: string;
3
+ roleCode: string;
4
+ roleName: string;
5
+ members?: Array<{ label: string; value: string }>;
6
+ collegeId?: string;
7
+ classId?: string;
8
+ enabled?: { label: string; value: "enabled" | "disabled" };
9
+ lastSyncedAt?: string;
10
+ }
11
+
12
+ export interface DataOwnershipFields {
13
+ collegeId?: string;
14
+ classId?: string;
15
+ ownerDeptId?: string;
16
+ ownerUserId?: string;
17
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./permissions";
2
+ export * from "./state-machine";
3
+ export * from "./ticket-query";
4
+ export * from "./types";
@@ -0,0 +1,42 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+
4
+ import { canViewTicket, getTicketUiPermissions } from "./permissions";
5
+ import type { TicketOperator, TicketRecord } from "./types";
6
+
7
+ const ticket: TicketRecord = {
8
+ formInstanceId: "ticket_001",
9
+ title: "实验室报修",
10
+ status: "accepted",
11
+ ownerUserId: "owner_001",
12
+ ownerDeptId: "dept_lab",
13
+ collegeId: "college_science",
14
+ };
15
+
16
+ describe("service-ticket permission helpers", () => {
17
+ it("allows admins and redundant ownership matches", () => {
18
+ assert.equal(canViewTicket(ticket, user({ roleCodes: ["app_admin"] })), true);
19
+ assert.equal(canViewTicket(ticket, user({ userId: "owner_001" })), true);
20
+ assert.equal(canViewTicket(ticket, user({ collegeIds: ["college_science"] })), true);
21
+ assert.equal(canViewTicket(ticket, user({ departmentIds: ["dept_lab"] })), true);
22
+ });
23
+
24
+ it("keeps action visibility tied to operation permission", () => {
25
+ const allowed = getTicketUiPermissions(ticket, user({ departmentIds: ["dept_lab"] }));
26
+ assert.equal(allowed.canView, true);
27
+ assert.deepEqual(allowed.actions, ["start", "cancel"]);
28
+
29
+ const denied = getTicketUiPermissions(ticket, user({ userId: "other" }));
30
+ assert.equal(denied.canView, false);
31
+ assert.deepEqual(denied.actions, []);
32
+ });
33
+ });
34
+
35
+ function user(input: Partial<TicketOperator>): TicketOperator {
36
+ return {
37
+ userId: "u_001",
38
+ roleCodes: [],
39
+ departmentIds: [],
40
+ ...input,
41
+ };
42
+ }