openxiangda 1.0.33 → 1.0.34
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/openxiangda-skills/SKILL.md +5 -0
- package/openxiangda-skills/references/architecture-patterns.md +2 -2
- package/openxiangda-skills/references/best-practices.md +37 -12
- package/openxiangda-skills/references/forms/component-registry.md +1 -0
- package/openxiangda-skills/references/forms/form-schema.md +8 -0
- package/openxiangda-skills/references/pages/workspace-structure.md +1 -0
- package/openxiangda-skills/skills/openxiangda-form/SKILL.md +8 -1
- package/openxiangda-skills/skills/openxiangda-page/SKILL.md +1 -0
- package/openxiangda-skills/skills/openxiangda-permission-settings/SKILL.md +1 -1
- package/package.json +1 -1
- package/templates/sy-lowcode-app-workspace/examples/best-practices/catalog.json +1 -1
- package/templates/sy-lowcode-app-workspace/examples/best-practices/decision-guide.md +11 -6
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/permissions.test.ts +24 -7
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/permissions.ts +13 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/types.ts +8 -6
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/permissions.test.ts +5 -5
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/permissions.ts +7 -5
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/state-machine.test.ts +1 -1
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/state-machine.ts +3 -3
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/ticket-query.test.ts +2 -2
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/ticket-query.ts +8 -8
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/types.ts +12 -10
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/app-role/schema.ts +40 -6
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/customer-profile/schema.ts +9 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/service-ticket/schema.ts +54 -16
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/components/TicketDetailDrawer.tsx +6 -3
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/components/TicketTableActions.tsx +6 -4
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/permissions/form-groups/service-ticket-college.json +2 -2
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/permissions/roles.json +2 -2
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/services/service-ticket.ts +4 -4
- package/templates/sy-lowcode-app-workspace/examples/forms/customer/schema.ts +1 -0
- package/templates/sy-lowcode-app-workspace/src/shared/form-schema.ts +2 -0
|
@@ -84,6 +84,11 @@ When the user provides a root domain such as `https://yida.wisejob.cn/`, use it
|
|
|
84
84
|
- Never store token data in the project directory. User tokens live in `~/.openxiangda/profiles.json`; project state lives in `.openxiangda/state.json` and stores only IDs and mappings.
|
|
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
|
+
- 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 and `AssociationFormField` for data maintained by another form, such as class, college, customer, or project. Do not make users maintain raw ID text fields.
|
|
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 a visible select/association field and marked `behavior: "HIDDEN"`.
|
|
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
|
+
- 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.
|
|
87
92
|
- Use logical resource codes in local files. Platform-specific IDs such as `formUuid`, `pageId`, `workflowId`, and `automationId` must be isolated by profile.
|
|
88
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.
|
|
89
94
|
- 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.
|
|
@@ -155,8 +155,8 @@ export default function InstrumentListPage() {
|
|
|
155
155
|
|
|
156
156
|
### 4.1 状态流转不是审批流
|
|
157
157
|
|
|
158
|
-
- 普通业务流转默认使用普通表单:`status`
|
|
159
|
-
- 每次状态变更都由 service
|
|
158
|
+
- 普通业务流转默认使用普通表单:`status` 字段、可维护的责任人/归属字段、必要的隐藏权限键、操作日志表、domain 状态机、service 统一提交。
|
|
159
|
+
- 每次状态变更都由 service 完成,统一写操作日志并更新责任人、归属、更新时间和隐藏权限键等派生字段。
|
|
160
160
|
- workflow 只用于真实审批:审批人、审批任务、同意/驳回、审批意见、节点权限、流程记录明确存在的场景。
|
|
161
161
|
- 不要把「待处理 / 处理中 / 已完成 / 已关闭」这类状态流转建成流程表单。
|
|
162
162
|
|
|
@@ -42,8 +42,10 @@ Most business processes are status lifecycles, not approval workflows.
|
|
|
42
42
|
Use a normal form plus:
|
|
43
43
|
|
|
44
44
|
- a `status` field
|
|
45
|
-
- responsibility fields such as
|
|
46
|
-
|
|
45
|
+
- user-facing responsibility fields such as personnel, department, enum, or
|
|
46
|
+
association fields
|
|
47
|
+
- hidden permission scope keys, when form permission groups require scalar
|
|
48
|
+
matching
|
|
47
49
|
- an action log form
|
|
48
50
|
- a pure state machine in `domain/<feature>/state-machine.ts`
|
|
49
51
|
- a service method that changes status and writes the log in one path
|
|
@@ -103,20 +105,43 @@ For apps with dynamic roles:
|
|
|
103
105
|
|
|
104
106
|
1. Create an app role maintenance form, such as `app-role`.
|
|
105
107
|
2. Use automation / JS_CODE to sync role records to platform roles.
|
|
106
|
-
3.
|
|
107
|
-
-
|
|
108
|
-
- `
|
|
109
|
-
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
108
|
+
3. Model visible scope fields with maintainable controls:
|
|
109
|
+
- personnel and departments use platform personnel/department fields
|
|
110
|
+
- enum scopes use `SelectField` / `RadioField` with `options`
|
|
111
|
+
- class, college, project, customer, and similar maintained records use
|
|
112
|
+
`AssociationFormField`
|
|
113
|
+
4. Add hidden derived scope keys only when platform form permission conditions
|
|
114
|
+
require scalar matching, for example:
|
|
115
|
+
- `collegeScopeKey`
|
|
116
|
+
- `classScopeKey`
|
|
117
|
+
- `ownerDeptScopeKey`
|
|
118
|
+
- `ownerUserScopeKey`
|
|
119
|
+
5. Create page permission groups for entry visibility.
|
|
120
|
+
6. Create form permission groups with condition-based data permissions for real
|
|
114
121
|
data isolation.
|
|
115
122
|
|
|
116
123
|
Frontend button hiding is only user experience. It is not permission control.
|
|
117
124
|
Every sensitive action must still be protected by platform role/form permission
|
|
118
125
|
groups or backend-side JS_CODE checks.
|
|
119
126
|
|
|
127
|
+
## Form Copy And Field Visibility
|
|
128
|
+
|
|
129
|
+
- Every visible form field should have a short, user-facing placeholder.
|
|
130
|
+
- Do not add tips to every field. Tips are only for special constraints,
|
|
131
|
+
unusual formats, compliance notes, or non-obvious business rules.
|
|
132
|
+
- Use select/radio controls for enums and association controls for values
|
|
133
|
+
maintained by other forms. Do not ask users to type raw IDs.
|
|
134
|
+
- Hide permission scope keys, computed, sync, and developer/internal fields with
|
|
135
|
+
`behavior: "HIDDEN"` when they must exist in the schema. Scope keys should be
|
|
136
|
+
derived from visible select/association/person/department fields, not typed by
|
|
137
|
+
users.
|
|
138
|
+
- Do not add separate creator, updater, creator department, updater department,
|
|
139
|
+
created time, or updated time fields unless the user explicitly needs a
|
|
140
|
+
separate business field. The platform creates system metadata for every form.
|
|
141
|
+
- All visible copy must be written for end users. Do not put implementation
|
|
142
|
+
notes, schema descriptions, or development explanations in sections, cards,
|
|
143
|
+
labels, tips, helper text, or empty states.
|
|
144
|
+
|
|
120
145
|
## Query Performance
|
|
121
146
|
|
|
122
147
|
- Always use paginated APIs with `currentPage`, `pageSize`, sort, and structured
|
|
@@ -166,8 +191,8 @@ groups or backend-side JS_CODE checks.
|
|
|
166
191
|
- `service-ticket-ops`: custom data management page based on
|
|
167
192
|
`DataManagementList`, split into page, components, hook, query builder, and
|
|
168
193
|
detail drawer.
|
|
169
|
-
- `role-governance`: role maintenance form, role sync JS_CODE,
|
|
170
|
-
|
|
194
|
+
- `role-governance`: role maintenance form, role sync JS_CODE, maintainable
|
|
195
|
+
scope fields, hidden permission keys, and permission-group resource examples.
|
|
171
196
|
- `pc-portal-shell`: app-shell PC portal with routes, modules, components, and
|
|
172
197
|
services.
|
|
173
198
|
- `mobile-portal-shell`: app-shell mobile portal with mobile-only components
|
|
@@ -36,6 +36,7 @@ Rules:
|
|
|
36
36
|
- Use field-level `rules` for validation. Required fields should set both `required: true` and `rules: [{ required: true, message: "..." }]` when a custom message is needed.
|
|
37
37
|
- For option components (`SelectField`, `MultiSelectField`, `RadioField`, `CheckboxField`, `CascadeSelectField`), always provide an `options` array. Use `options: [{ value: "stable_code", label: "显示名" }]`; if options will be loaded elsewhere later, still emit `options: []` so the runtime does not crash on `options.map(...)`.
|
|
38
38
|
- Do not rely on custom `relation` metadata alone for normal form pages. A `SelectField` with `relation` but no `options` can white-screen with `Cannot read properties of undefined (reading 'map')`. Use static `options`, `AssociationFormField`, or a custom code page that resolves linked records.
|
|
39
|
+
- Do not model business enums or data-source references as free text IDs. Use `SelectField` / `RadioField` with `options` for enums, and use `AssociationFormField` for records maintained by another form.
|
|
39
40
|
- For `NumberField`, prefer explicit `min`, `max`, `precision`, and `unit` when the business meaning is constrained.
|
|
40
41
|
- For `DateField`, use `mode: "date"` or `mode: "datetime"` and a stable `format` such as `YYYY-MM-DD` or `YYYY-MM-DD HH:mm`.
|
|
41
42
|
- For `CascadeDateField`, store a date range and document whether the value is inclusive.
|
|
@@ -35,6 +35,7 @@ export default defineFormSchema({
|
|
|
35
35
|
fieldId: "customer_type",
|
|
36
36
|
componentName: "SelectField",
|
|
37
37
|
label: "客户类型",
|
|
38
|
+
placeholder: "请选择客户类型",
|
|
38
39
|
options: [
|
|
39
40
|
{ value: "enterprise", label: "企业客户" },
|
|
40
41
|
{ value: "individual", label: "个人客户" },
|
|
@@ -55,6 +56,13 @@ export default defineFormSchema({
|
|
|
55
56
|
|
|
56
57
|
- Each persisted field needs a stable `fieldId`.
|
|
57
58
|
- Field labels should be user-facing Chinese names when the app is Chinese.
|
|
59
|
+
- Each visible field should include a concise user-facing `placeholder`. Hidden/internal fields do not need placeholders.
|
|
60
|
+
- Use `tips` only for special constraints, unusual formats, compliance notes, or non-obvious business rules. Do not add tips to every field.
|
|
61
|
+
- Use `SelectField` / `RadioField` for enum values and always provide `options`.
|
|
62
|
+
- Use `AssociationFormField` for values maintained in another form or data source, such as class, college, customer, project, category, or asset. Do not ask users to type raw IDs in `TextField`.
|
|
63
|
+
- Keep only user-needed fields visible. If a scalar key is needed for permissions, synchronization, computed state, or internal logic, derive it from a visible select/association/person/department field and keep it in the schema with `behavior: "HIDDEN"`.
|
|
64
|
+
- Do not create duplicate platform system fields such as creator, updater, creator department, updater department, created time, or updated time unless the user explicitly needs a distinct business field. The platform already creates system metadata for every form.
|
|
65
|
+
- Labels, placeholders, tips, section titles, descriptions, and empty states must be end-user-facing. Do not write developer-facing implementation explanations into visible form copy.
|
|
58
66
|
- Use platform-supported field components from `component-registry.md`.
|
|
59
67
|
- Do not generate fields that are only visual layout containers.
|
|
60
68
|
- Put validation rules on fields as `field.rules`. Do not put validation objects in top-level `schema.rules`.
|
|
@@ -38,3 +38,4 @@ Rules:
|
|
|
38
38
|
- Shared components can live under `src/components/` when reused.
|
|
39
39
|
- Build artifacts under `dist/` are generated and should not be hand edited.
|
|
40
40
|
- Publish through `openxiangda workspace publish --profile <name>`.
|
|
41
|
+
- Visible page copy must be written for end users. Do not show implementation notes, schema explanations, or developer-only guidance inside page sections, cards, alerts, tooltips, or empty states.
|
|
@@ -74,8 +74,15 @@ Read these references only when writing or reviewing schema:
|
|
|
74
74
|
## Rules
|
|
75
75
|
|
|
76
76
|
- Prefer deterministic `formCode` as local key; bind live `formUuid` under the active profile.
|
|
77
|
+
- Every visible field should have a concise, user-facing `placeholder` that tells the user what to enter or select.
|
|
78
|
+
- Use `tips` sparingly. Add tips only for special constraints, unusual formats, or non-obvious business rules; ordinary fields should not have tips.
|
|
79
|
+
- Use `SelectField` or `RadioField` for enumerable business values. Do not model enums as `TextField` values that users must type manually.
|
|
80
|
+
- Use `AssociationFormField` when the value is maintained by another form or data source, such as class, college, customer, project, category, or asset. Do not make users maintain raw ID text fields for those values.
|
|
81
|
+
- Display only fields the user needs to interact with. Permission scope keys, computed fields, sync fields, and developer/internal fields should normally be derived from visible select/association fields and stay in the schema with `behavior: "HIDDEN"` instead of appearing on the form page.
|
|
82
|
+
- Do not create separate fields for platform system metadata such as creator, updater, creator department, updater department, created time, or updated time unless the user explicitly asks for a separate business concept. The platform creates those system fields for every form.
|
|
83
|
+
- All labels, placeholders, tips, section titles, empty states, and helper text must be end-user-facing business copy. Do not write developer explanations or implementation notes into visible page copy.
|
|
77
84
|
- For business lifecycles, model status with normal form fields (`status`, owner/responsibility fields, action-log relation fields) unless the scenario has real approval tasks. Do not create workflow forms for ordinary status transitions.
|
|
78
|
-
- For permission isolation,
|
|
85
|
+
- For permission isolation, collect user-facing ownership with select, personnel, department, or association fields. If form permission groups require scalar matching, add hidden derived keys such as `collegeScopeKey`, `classScopeKey`, `ownerDeptScopeKey`, and `ownerUserScopeKey`; never expose those keys as editable text inputs.
|
|
79
86
|
- For multi-platform publishing, create or bind the form separately for each profile.
|
|
80
87
|
- Do not copy `formUuid` from dev to prod unless the target platform explicitly already uses that ID.
|
|
81
88
|
- Keep `schema.ts` and `page.tsx` as the source for fields/layout/rules and presentation; generated build output is not the source of truth.
|
|
@@ -71,6 +71,7 @@ Read these references only when editing page code:
|
|
|
71
71
|
- Formal user-facing entries such as admin consoles, PC portals, and mobile portals must be app-shell code pages. Declare `entry: { mode: "app-shell", hidePlatformNav: true, defaultRoute: "<home-route>" }` in `page.config.ts`.
|
|
72
72
|
- Do not generate single-file large pages. Split complex code pages into `domain/`, `shared/services/`, `shared/hooks/`, shared/page-local `components/`, route/config files, and `styles.css` as described in `../../references/best-practices.md`.
|
|
73
73
|
- Keep view code thin. Page components call hooks/services; business rules, state transition rules, permission predicates, and query builders live outside TSX and are reusable by PC and mobile pages.
|
|
74
|
+
- All visible page copy must be end-user-facing business text. Do not put developer explanations, implementation notes, schema descriptions, or "this module is generated by..." text into sections, cards, alerts, empty states, tooltips, or helper copy.
|
|
74
75
|
- Store live `pageId`, `routeKey`, and `legacyFormUuid` under the current profile only.
|
|
75
76
|
- Use `openxiangda/runtime` for platform data access instead of hardcoding backend URLs in page code.
|
|
76
77
|
- For reminders, alerts, and business messages, declare `src/resources/notifications/` first and call `sdk.notification`; do not hardcode notification API URLs.
|
|
@@ -34,7 +34,7 @@ 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
|
|
37
|
+
For dynamic multi-role apps, do not hardcode role behavior only in page code. Create a role maintenance form and sync it to platform roles with automation / JS_CODE. Visible scope fields should be maintainable controls: personnel/department fields for people and orgs, `SelectField` / `RadioField` for enums, and `AssociationFormField` for records maintained by other forms. If form permission groups need scalar matching, derive hidden keys such as `collegeScopeKey`, `classScopeKey`, `ownerDeptScopeKey`, `ownerUserScopeKey`, and `roleCode`; do not expose raw ID text fields to users. Use page permission groups for entry visibility and form permission groups with condition-based data permissions for real data isolation.
|
|
38
38
|
|
|
39
39
|
## Page Permission Groups
|
|
40
40
|
|
package/package.json
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"code": "role-governance",
|
|
26
26
|
"type": "permissions",
|
|
27
27
|
"path": "src/domain/role-governance",
|
|
28
|
-
"description": "Dynamic role maintenance, role sync,
|
|
28
|
+
"description": "Dynamic role maintenance, role sync, maintainable scope fields, hidden permission keys, and permission-group resource examples."
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
"code": "pc-portal-shell",
|
|
@@ -20,9 +20,10 @@ Most business "processes" are lifecycle state changes:
|
|
|
20
20
|
- orders: draft -> submitted -> paid -> fulfilled -> completed
|
|
21
21
|
- assets: available -> reserved -> in_use -> maintenance -> retired
|
|
22
22
|
|
|
23
|
-
Use a normal form with a `status` field, ownership fields,
|
|
24
|
-
form. Define allowed transitions in
|
|
25
|
-
execute changes through a service
|
|
23
|
+
Use a normal form with a `status` field, maintainable ownership/scope fields,
|
|
24
|
+
and an operation-log form. Define allowed transitions in
|
|
25
|
+
`domain/<feature>/state-machine.ts`, and execute changes through a service
|
|
26
|
+
method that updates state and writes logs.
|
|
26
27
|
|
|
27
28
|
Use workflow only when the platform must create approval tasks with approvers,
|
|
28
29
|
approval comments, agree/reject actions, copy nodes, node field permissions, and
|
|
@@ -39,6 +40,10 @@ Frontend code must not be responsible for data isolation or background jobs.
|
|
|
39
40
|
## Permissions
|
|
40
41
|
|
|
41
42
|
For dynamic multi-role apps, create a business role table and synchronize it to
|
|
42
|
-
platform app roles.
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
platform app roles. User-facing scope fields should be maintainable controls:
|
|
44
|
+
personnel/department fields for people and orgs, enum fields for fixed option
|
|
45
|
+
sets, and association fields for classes, colleges, projects, customers, and
|
|
46
|
+
other records maintained by forms. If the platform permission condition needs a
|
|
47
|
+
scalar value, derive a hidden scope key such as `collegeScopeKey` or
|
|
48
|
+
`ownerDeptScopeKey`. Use form permission groups with condition-style data
|
|
49
|
+
permissions to enforce data isolation.
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { describe, it } from "node:test";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
buildConditionDataPermission,
|
|
6
|
+
buildRoleCode,
|
|
7
|
+
deriveRoleScopeKeys,
|
|
8
|
+
} from "./permissions";
|
|
5
9
|
|
|
6
10
|
describe("role-governance permission helpers", () => {
|
|
7
11
|
it("normalizes dynamic role codes", () => {
|
|
@@ -9,23 +13,36 @@ describe("role-governance permission helpers", () => {
|
|
|
9
13
|
assert.equal(buildRoleCode({ roleCode: "class advisor" }), "class_advisor");
|
|
10
14
|
});
|
|
11
15
|
|
|
12
|
-
it("
|
|
16
|
+
it("derives hidden scope keys from maintainable association values", () => {
|
|
17
|
+
assert.deepEqual(
|
|
18
|
+
deriveRoleScopeKeys({
|
|
19
|
+
collegeScope: { label: "理学院", value: "college_science" },
|
|
20
|
+
classScope: { label: "2026 级一班", value: "class_2026_01" },
|
|
21
|
+
}),
|
|
22
|
+
{
|
|
23
|
+
collegeScopeKey: "college_science",
|
|
24
|
+
classScopeKey: "class_2026_01",
|
|
25
|
+
},
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("builds condition-based data permissions from hidden scope keys", () => {
|
|
13
30
|
const permission = buildConditionDataPermission({
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
31
|
+
collegeScopeKey: "college_science",
|
|
32
|
+
classScopeKey: "",
|
|
33
|
+
ownerDeptScopeKey: "dept_lab",
|
|
17
34
|
});
|
|
18
35
|
|
|
19
36
|
assert.equal(permission.type, "condition");
|
|
20
37
|
assert.deepEqual(permission.condition.rules, [
|
|
21
38
|
{
|
|
22
|
-
field: "
|
|
39
|
+
field: "collegeScopeKey",
|
|
23
40
|
componentType: "Text",
|
|
24
41
|
op: "=",
|
|
25
42
|
value: "college_science",
|
|
26
43
|
},
|
|
27
44
|
{
|
|
28
|
-
field: "
|
|
45
|
+
field: "ownerDeptScopeKey",
|
|
29
46
|
componentType: "Text",
|
|
30
47
|
op: "=",
|
|
31
48
|
value: "dept_lab",
|
|
@@ -4,6 +4,19 @@ export function buildRoleCode(role: Pick<AppRoleRecord, "roleCode">) {
|
|
|
4
4
|
return role.roleCode.trim().replace(/\s+/g, "_").toLowerCase();
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
function scopeKey(value: { value?: string | number } | undefined) {
|
|
8
|
+
return value?.value === undefined || value.value === null ? "" : String(value.value);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function deriveRoleScopeKeys(
|
|
12
|
+
role: Pick<AppRoleRecord, "collegeScope" | "classScope">,
|
|
13
|
+
) {
|
|
14
|
+
return {
|
|
15
|
+
collegeScopeKey: scopeKey(role.collegeScope),
|
|
16
|
+
classScopeKey: scopeKey(role.classScope),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
7
20
|
export function buildConditionDataPermission(scope: DataOwnershipFields) {
|
|
8
21
|
const rules = Object.entries(scope)
|
|
9
22
|
.filter(([, value]) => Boolean(value))
|
|
@@ -3,15 +3,17 @@ export interface AppRoleRecord {
|
|
|
3
3
|
roleCode: string;
|
|
4
4
|
roleName: string;
|
|
5
5
|
members?: Array<{ label: string; value: string }>;
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
collegeScope?: { label: string; value: string };
|
|
7
|
+
classScope?: { label: string; value: string };
|
|
8
|
+
collegeScopeKey?: string;
|
|
9
|
+
classScopeKey?: string;
|
|
8
10
|
enabled?: { label: string; value: "enabled" | "disabled" };
|
|
9
11
|
lastSyncedAt?: string;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export interface DataOwnershipFields {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
collegeScopeKey?: string;
|
|
16
|
+
classScopeKey?: string;
|
|
17
|
+
ownerDeptScopeKey?: string;
|
|
18
|
+
ownerUserScopeKey?: string;
|
|
17
19
|
}
|
|
@@ -8,16 +8,16 @@ const ticket: TicketRecord = {
|
|
|
8
8
|
formInstanceId: "ticket_001",
|
|
9
9
|
title: "实验室报修",
|
|
10
10
|
status: "accepted",
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
ownerUserScopeKey: "owner_001",
|
|
12
|
+
ownerDeptScopeKey: "dept_lab",
|
|
13
|
+
collegeScopeKey: "college_science",
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
describe("service-ticket permission helpers", () => {
|
|
17
|
-
it("allows admins and
|
|
17
|
+
it("allows admins and hidden scope-key matches", () => {
|
|
18
18
|
assert.equal(canViewTicket(ticket, user({ roleCodes: ["app_admin"] })), true);
|
|
19
19
|
assert.equal(canViewTicket(ticket, user({ userId: "owner_001" })), true);
|
|
20
|
-
assert.equal(canViewTicket(ticket, user({
|
|
20
|
+
assert.equal(canViewTicket(ticket, user({ collegeScopeKeys: ["college_science"] })), true);
|
|
21
21
|
assert.equal(canViewTicket(ticket, user({ departmentIds: ["dept_lab"] })), true);
|
|
22
22
|
});
|
|
23
23
|
|
|
@@ -3,12 +3,14 @@ import { getAvailableTicketActions } from "./state-machine";
|
|
|
3
3
|
|
|
4
4
|
export function canViewTicket(ticket: TicketRecord, operator: TicketOperator) {
|
|
5
5
|
if (operator.roleCodes.includes("app_admin")) return true;
|
|
6
|
-
if (ticket.
|
|
7
|
-
if (ticket.
|
|
8
|
-
|
|
6
|
+
if (ticket.ownerUserScopeKey === operator.userId) return true;
|
|
7
|
+
if (ticket.collegeScopeKey && operator.collegeScopeKeys?.includes(ticket.collegeScopeKey)) {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
if (ticket.classScopeKey && operator.classScopeKeys?.includes(ticket.classScopeKey)) return true;
|
|
9
11
|
if (
|
|
10
|
-
ticket.
|
|
11
|
-
operator.departmentIds?.includes(ticket.
|
|
12
|
+
ticket.ownerDeptScopeKey &&
|
|
13
|
+
operator.departmentIds?.includes(ticket.ownerDeptScopeKey)
|
|
12
14
|
) {
|
|
13
15
|
return true;
|
|
14
16
|
}
|
|
@@ -39,10 +39,10 @@ export function getNextTicketStatus(
|
|
|
39
39
|
|
|
40
40
|
export function canOperateTicket(ticket: TicketRecord, operator: TicketOperator) {
|
|
41
41
|
if (operator.roleCodes.includes("app_admin")) return true;
|
|
42
|
-
if (ticket.
|
|
42
|
+
if (ticket.ownerUserScopeKey && ticket.ownerUserScopeKey === operator.userId) return true;
|
|
43
43
|
if (
|
|
44
|
-
ticket.
|
|
45
|
-
operator.departmentIds?.includes(ticket.
|
|
44
|
+
ticket.ownerDeptScopeKey &&
|
|
45
|
+
operator.departmentIds?.includes(ticket.ownerDeptScopeKey)
|
|
46
46
|
) {
|
|
47
47
|
return true;
|
|
48
48
|
}
|
|
@@ -9,13 +9,13 @@ describe("service-ticket query builder", () => {
|
|
|
9
9
|
keyword: "离心机",
|
|
10
10
|
statuses: ["new", "processing"],
|
|
11
11
|
priorities: ["urgent"],
|
|
12
|
-
|
|
12
|
+
collegeScopeKey: "college_science",
|
|
13
13
|
});
|
|
14
14
|
|
|
15
15
|
assert.equal(group.logic, "AND");
|
|
16
16
|
assert.deepEqual(
|
|
17
17
|
group.rules.map((item) => item.key),
|
|
18
|
-
["status", "priority", "
|
|
18
|
+
["status", "priority", "collegeScopeKey"],
|
|
19
19
|
);
|
|
20
20
|
assert.equal(group.conditions.length, 1);
|
|
21
21
|
assert.equal(group.conditions[0]?.logic, "OR");
|
|
@@ -44,17 +44,17 @@ export function buildTicketFilterGroup(search: TicketSearchState): FilterGroup {
|
|
|
44
44
|
if (search.priorities?.length) {
|
|
45
45
|
group.rules.push(rule("priority", "SelectField", "IN", search.priorities));
|
|
46
46
|
}
|
|
47
|
-
if (search.
|
|
48
|
-
group.rules.push(rule("
|
|
47
|
+
if (search.ownerUserScopeKey) {
|
|
48
|
+
group.rules.push(rule("ownerUserScopeKey", "TextField", "EQ", search.ownerUserScopeKey));
|
|
49
49
|
}
|
|
50
|
-
if (search.
|
|
51
|
-
group.rules.push(rule("
|
|
50
|
+
if (search.ownerDeptScopeKey) {
|
|
51
|
+
group.rules.push(rule("ownerDeptScopeKey", "TextField", "EQ", search.ownerDeptScopeKey));
|
|
52
52
|
}
|
|
53
|
-
if (search.
|
|
54
|
-
group.rules.push(rule("
|
|
53
|
+
if (search.collegeScopeKey) {
|
|
54
|
+
group.rules.push(rule("collegeScopeKey", "TextField", "EQ", search.collegeScopeKey));
|
|
55
55
|
}
|
|
56
|
-
if (search.
|
|
57
|
-
group.rules.push(rule("
|
|
56
|
+
if (search.classScopeKey) {
|
|
57
|
+
group.rules.push(rule("classScopeKey", "TextField", "EQ", search.classScopeKey));
|
|
58
58
|
}
|
|
59
59
|
if (search.keyword?.trim()) {
|
|
60
60
|
const keyword = search.keyword.trim();
|
|
@@ -23,10 +23,12 @@ export interface TicketRecord {
|
|
|
23
23
|
title: string;
|
|
24
24
|
status: TicketStatus;
|
|
25
25
|
priority?: TicketPriority;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
ownerUserScopeKey?: string;
|
|
27
|
+
ownerDeptScopeKey?: string;
|
|
28
|
+
collegeScopeKey?: string;
|
|
29
|
+
classScopeKey?: string;
|
|
30
|
+
college?: { label: string; value: string };
|
|
31
|
+
classGroup?: { label: string; value: string };
|
|
30
32
|
requester?: { label: string; value: string };
|
|
31
33
|
currentOwner?: { label: string; value: string };
|
|
32
34
|
description?: string;
|
|
@@ -48,8 +50,8 @@ export interface TicketOperator {
|
|
|
48
50
|
userId: string;
|
|
49
51
|
userName?: string;
|
|
50
52
|
roleCodes: string[];
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
collegeScopeKeys?: string[];
|
|
54
|
+
classScopeKeys?: string[];
|
|
53
55
|
departmentIds?: string[];
|
|
54
56
|
}
|
|
55
57
|
|
|
@@ -57,8 +59,8 @@ export interface TicketSearchState {
|
|
|
57
59
|
keyword?: string;
|
|
58
60
|
statuses?: TicketStatus[];
|
|
59
61
|
priorities?: TicketPriority[];
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
ownerUserScopeKey?: string;
|
|
63
|
+
ownerDeptScopeKey?: string;
|
|
64
|
+
collegeScopeKey?: string;
|
|
65
|
+
classScopeKey?: string;
|
|
64
66
|
}
|
package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/app-role/schema.ts
CHANGED
|
@@ -19,29 +19,62 @@ export default createFormSchema({
|
|
|
19
19
|
componentName: "TextField",
|
|
20
20
|
label: "角色名称",
|
|
21
21
|
required: true,
|
|
22
|
+
placeholder: "请输入角色名称",
|
|
22
23
|
},
|
|
23
24
|
{
|
|
24
25
|
fieldId: "members",
|
|
25
26
|
componentName: "UserSelectField",
|
|
26
27
|
label: "成员",
|
|
27
28
|
multiple: true,
|
|
29
|
+
placeholder: "请选择角色成员",
|
|
28
30
|
},
|
|
29
31
|
{
|
|
30
|
-
fieldId: "
|
|
32
|
+
fieldId: "collegeScope",
|
|
33
|
+
componentName: "AssociationFormField",
|
|
34
|
+
label: "适用学院",
|
|
35
|
+
placeholder: "请选择适用学院",
|
|
36
|
+
associationForm: {
|
|
37
|
+
appType: process.env.OPENXIANGDA_APP_TYPE || "APP_XXXX",
|
|
38
|
+
formUuid: "FORM_COLLEGE_PROFILE",
|
|
39
|
+
mainFieldId: "collegeName",
|
|
40
|
+
selectorColumns: [
|
|
41
|
+
{ title: "学院名称", dataIndex: "collegeName" },
|
|
42
|
+
{ title: "学院编码", dataIndex: "collegeCode" },
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
fieldId: "classScope",
|
|
48
|
+
componentName: "AssociationFormField",
|
|
49
|
+
label: "适用班级",
|
|
50
|
+
placeholder: "请选择适用班级",
|
|
51
|
+
associationForm: {
|
|
52
|
+
appType: process.env.OPENXIANGDA_APP_TYPE || "APP_XXXX",
|
|
53
|
+
formUuid: "FORM_CLASS_PROFILE",
|
|
54
|
+
mainFieldId: "className",
|
|
55
|
+
selectorColumns: [
|
|
56
|
+
{ title: "班级名称", dataIndex: "className" },
|
|
57
|
+
{ title: "所属学院", dataIndex: "collegeName" },
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
fieldId: "collegeScopeKey",
|
|
31
63
|
componentName: "TextField",
|
|
32
|
-
label: "
|
|
33
|
-
|
|
64
|
+
label: "学院权限键",
|
|
65
|
+
behavior: "HIDDEN",
|
|
34
66
|
},
|
|
35
67
|
{
|
|
36
|
-
fieldId: "
|
|
68
|
+
fieldId: "classScopeKey",
|
|
37
69
|
componentName: "TextField",
|
|
38
|
-
label: "
|
|
39
|
-
|
|
70
|
+
label: "班级权限键",
|
|
71
|
+
behavior: "HIDDEN",
|
|
40
72
|
},
|
|
41
73
|
{
|
|
42
74
|
fieldId: "enabled",
|
|
43
75
|
componentName: "RadioField",
|
|
44
76
|
label: "启用状态",
|
|
77
|
+
placeholder: "请选择启用状态",
|
|
45
78
|
defaultValue: { label: "启用", value: "enabled" },
|
|
46
79
|
options: [
|
|
47
80
|
{ label: "启用", value: "enabled" },
|
|
@@ -52,6 +85,7 @@ export default createFormSchema({
|
|
|
52
85
|
fieldId: "lastSyncedAt",
|
|
53
86
|
componentName: "DateField",
|
|
54
87
|
label: "最近同步时间",
|
|
88
|
+
behavior: "HIDDEN",
|
|
55
89
|
},
|
|
56
90
|
],
|
|
57
91
|
});
|
|
@@ -19,6 +19,7 @@ export default createFormSchema({
|
|
|
19
19
|
componentName: "SelectField",
|
|
20
20
|
label: "客户等级",
|
|
21
21
|
required: true,
|
|
22
|
+
placeholder: "请选择客户等级",
|
|
22
23
|
options: [
|
|
23
24
|
{ label: "重点客户", value: "key" },
|
|
24
25
|
{ label: "普通客户", value: "normal" },
|
|
@@ -30,45 +31,53 @@ export default createFormSchema({
|
|
|
30
31
|
componentName: "UserSelectField",
|
|
31
32
|
label: "负责人",
|
|
32
33
|
required: true,
|
|
34
|
+
placeholder: "请选择负责人",
|
|
33
35
|
},
|
|
34
36
|
{
|
|
35
37
|
fieldId: "ownerDept",
|
|
36
38
|
componentName: "DepartmentSelectField",
|
|
37
39
|
label: "负责部门",
|
|
38
40
|
required: true,
|
|
41
|
+
placeholder: "请选择负责部门",
|
|
39
42
|
},
|
|
40
43
|
{
|
|
41
44
|
fieldId: "contactPhone",
|
|
42
45
|
componentName: "TextField",
|
|
43
46
|
label: "联系电话",
|
|
47
|
+
placeholder: "请输入联系电话",
|
|
44
48
|
rules: [{ preset: "phone", message: "请输入有效手机号" }],
|
|
45
49
|
},
|
|
46
50
|
{
|
|
47
51
|
fieldId: "attachments",
|
|
48
52
|
componentName: "AttachmentField",
|
|
49
53
|
label: "附件",
|
|
54
|
+
placeholder: "请上传客户相关附件",
|
|
50
55
|
maxCount: 5,
|
|
51
56
|
},
|
|
52
57
|
{
|
|
53
58
|
fieldId: "contacts",
|
|
54
59
|
componentName: "SubFormField",
|
|
55
60
|
label: "联系人",
|
|
61
|
+
placeholder: "请添加联系人",
|
|
56
62
|
columns: [
|
|
57
63
|
{
|
|
58
64
|
fieldId: "contactName",
|
|
59
65
|
componentName: "TextField",
|
|
60
66
|
label: "姓名",
|
|
61
67
|
required: true,
|
|
68
|
+
placeholder: "请输入联系人姓名",
|
|
62
69
|
},
|
|
63
70
|
{
|
|
64
71
|
fieldId: "contactRole",
|
|
65
72
|
componentName: "TextField",
|
|
66
73
|
label: "角色",
|
|
74
|
+
placeholder: "请输入联系人角色",
|
|
67
75
|
},
|
|
68
76
|
{
|
|
69
77
|
fieldId: "contactMobile",
|
|
70
78
|
componentName: "TextField",
|
|
71
79
|
label: "手机",
|
|
80
|
+
placeholder: "请输入联系人手机",
|
|
72
81
|
rules: [{ preset: "phone", message: "请输入有效手机号" }],
|
|
73
82
|
},
|
|
74
83
|
],
|
|
@@ -12,12 +12,14 @@ export default createFormSchema({
|
|
|
12
12
|
componentName: "TextField",
|
|
13
13
|
label: "标题",
|
|
14
14
|
required: true,
|
|
15
|
+
placeholder: "请输入工单标题",
|
|
15
16
|
},
|
|
16
17
|
{
|
|
17
18
|
fieldId: "status",
|
|
18
19
|
componentName: "SelectField",
|
|
19
20
|
label: "状态",
|
|
20
21
|
required: true,
|
|
22
|
+
placeholder: "请选择工单状态",
|
|
21
23
|
defaultValue: { label: "新建", value: "new" },
|
|
22
24
|
options: [
|
|
23
25
|
{ label: "新建", value: "new" },
|
|
@@ -33,6 +35,7 @@ export default createFormSchema({
|
|
|
33
35
|
fieldId: "priority",
|
|
34
36
|
componentName: "SelectField",
|
|
35
37
|
label: "优先级",
|
|
38
|
+
placeholder: "请选择优先级",
|
|
36
39
|
defaultValue: { label: "普通", value: "normal" },
|
|
37
40
|
options: [
|
|
38
41
|
{ label: "紧急", value: "urgent" },
|
|
@@ -42,55 +45,90 @@ export default createFormSchema({
|
|
|
42
45
|
],
|
|
43
46
|
},
|
|
44
47
|
{
|
|
45
|
-
fieldId: "
|
|
46
|
-
componentName: "
|
|
47
|
-
label: "
|
|
48
|
-
|
|
48
|
+
fieldId: "currentOwner",
|
|
49
|
+
componentName: "UserSelectField",
|
|
50
|
+
label: "当前处理人",
|
|
51
|
+
placeholder: "请选择当前处理人",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
fieldId: "college",
|
|
55
|
+
componentName: "AssociationFormField",
|
|
56
|
+
label: "所属学院",
|
|
57
|
+
placeholder: "请选择所属学院",
|
|
58
|
+
associationForm: {
|
|
59
|
+
appType: process.env.OPENXIANGDA_APP_TYPE || "APP_XXXX",
|
|
60
|
+
formUuid: "FORM_COLLEGE_PROFILE",
|
|
61
|
+
mainFieldId: "collegeName",
|
|
62
|
+
selectorColumns: [
|
|
63
|
+
{ title: "学院名称", dataIndex: "collegeName" },
|
|
64
|
+
{ title: "学院编码", dataIndex: "collegeCode" },
|
|
65
|
+
],
|
|
66
|
+
},
|
|
49
67
|
},
|
|
50
68
|
{
|
|
51
|
-
fieldId: "
|
|
69
|
+
fieldId: "classGroup",
|
|
70
|
+
componentName: "AssociationFormField",
|
|
71
|
+
label: "所属班级",
|
|
72
|
+
placeholder: "请选择所属班级",
|
|
73
|
+
associationForm: {
|
|
74
|
+
appType: process.env.OPENXIANGDA_APP_TYPE || "APP_XXXX",
|
|
75
|
+
formUuid: "FORM_CLASS_PROFILE",
|
|
76
|
+
mainFieldId: "className",
|
|
77
|
+
selectorColumns: [
|
|
78
|
+
{ title: "班级名称", dataIndex: "className" },
|
|
79
|
+
{ title: "所属学院", dataIndex: "collegeName" },
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
fieldId: "ownerUserScopeKey",
|
|
52
85
|
componentName: "TextField",
|
|
53
|
-
label: "
|
|
54
|
-
|
|
86
|
+
label: "负责人权限键",
|
|
87
|
+
behavior: "HIDDEN",
|
|
55
88
|
},
|
|
56
89
|
{
|
|
57
|
-
fieldId: "
|
|
90
|
+
fieldId: "ownerDeptScopeKey",
|
|
58
91
|
componentName: "TextField",
|
|
59
|
-
label: "
|
|
60
|
-
|
|
92
|
+
label: "负责部门权限键",
|
|
93
|
+
behavior: "HIDDEN",
|
|
61
94
|
},
|
|
62
95
|
{
|
|
63
|
-
fieldId: "
|
|
96
|
+
fieldId: "collegeScopeKey",
|
|
64
97
|
componentName: "TextField",
|
|
65
|
-
label: "
|
|
66
|
-
|
|
98
|
+
label: "学院权限键",
|
|
99
|
+
behavior: "HIDDEN",
|
|
67
100
|
},
|
|
68
101
|
{
|
|
69
|
-
fieldId: "
|
|
70
|
-
componentName: "
|
|
71
|
-
label: "
|
|
102
|
+
fieldId: "classScopeKey",
|
|
103
|
+
componentName: "TextField",
|
|
104
|
+
label: "班级权限键",
|
|
105
|
+
behavior: "HIDDEN",
|
|
72
106
|
},
|
|
73
107
|
{
|
|
74
108
|
fieldId: "requester",
|
|
75
109
|
componentName: "UserSelectField",
|
|
76
110
|
label: "提交人",
|
|
77
111
|
required: true,
|
|
112
|
+
placeholder: "请选择提交人",
|
|
78
113
|
},
|
|
79
114
|
{
|
|
80
115
|
fieldId: "description",
|
|
81
116
|
componentName: "TextAreaField",
|
|
82
117
|
label: "问题描述",
|
|
83
118
|
required: true,
|
|
119
|
+
placeholder: "请描述问题现象、影响范围和期望处理结果",
|
|
84
120
|
},
|
|
85
121
|
{
|
|
86
122
|
fieldId: "lastActionAt",
|
|
87
123
|
componentName: "DateField",
|
|
88
124
|
label: "最近操作时间",
|
|
125
|
+
behavior: "HIDDEN",
|
|
89
126
|
},
|
|
90
127
|
{
|
|
91
128
|
fieldId: "attachments",
|
|
92
129
|
componentName: "AttachmentField",
|
|
93
130
|
label: "附件",
|
|
131
|
+
placeholder: "请上传问题相关附件",
|
|
94
132
|
maxCount: 8,
|
|
95
133
|
},
|
|
96
134
|
],
|
|
@@ -24,10 +24,13 @@ export function TicketDetailDrawer(props: {
|
|
|
24
24
|
<StatusTag status={props.ticket.status} />
|
|
25
25
|
</Descriptions.Item>
|
|
26
26
|
<Descriptions.Item label="负责人">
|
|
27
|
-
{props.ticket.currentOwner?.label ||
|
|
27
|
+
{props.ticket.currentOwner?.label || "-"}
|
|
28
28
|
</Descriptions.Item>
|
|
29
|
-
<Descriptions.Item label="
|
|
30
|
-
{props.ticket.
|
|
29
|
+
<Descriptions.Item label="所属学院">
|
|
30
|
+
{props.ticket.college?.label || "-"}
|
|
31
|
+
</Descriptions.Item>
|
|
32
|
+
<Descriptions.Item label="所属班级">
|
|
33
|
+
{props.ticket.classGroup?.label || "-"}
|
|
31
34
|
</Descriptions.Item>
|
|
32
35
|
</Descriptions>
|
|
33
36
|
<Typography.Paragraph className="bp-ticket-detail__description">
|
|
@@ -14,10 +14,12 @@ const normalizeTicket = (record: any): TicketRecord => ({
|
|
|
14
14
|
title: record.title || record.formData?.title || "未命名工单",
|
|
15
15
|
status: record.status || record.formData?.status?.value || record.formData?.status || "new",
|
|
16
16
|
priority: record.priority?.value || record.formData?.priority?.value,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
ownerUserScopeKey: record.ownerUserScopeKey || record.formData?.ownerUserScopeKey,
|
|
18
|
+
ownerDeptScopeKey: record.ownerDeptScopeKey || record.formData?.ownerDeptScopeKey,
|
|
19
|
+
collegeScopeKey: record.collegeScopeKey || record.formData?.collegeScopeKey,
|
|
20
|
+
classScopeKey: record.classScopeKey || record.formData?.classScopeKey,
|
|
21
|
+
college: record.college || record.formData?.college,
|
|
22
|
+
classGroup: record.classGroup || record.formData?.classGroup,
|
|
21
23
|
requester: record.requester || record.formData?.requester,
|
|
22
24
|
currentOwner: record.currentOwner || record.formData?.currentOwner,
|
|
23
25
|
description: record.description || record.formData?.description,
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
{
|
|
8
8
|
"code": "college_admin",
|
|
9
9
|
"name": "学院管理员",
|
|
10
|
-
"description": "
|
|
10
|
+
"description": "通过业务角色表动态维护,按学院权限键隔离数据"
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
13
|
"code": "class_owner",
|
|
14
14
|
"name": "班级负责人",
|
|
15
|
-
"description": "
|
|
15
|
+
"description": "通过业务角色表动态维护,按班级权限键隔离数据"
|
|
16
16
|
}
|
|
17
17
|
]
|
|
@@ -58,10 +58,10 @@ export async function transitionTicket(
|
|
|
58
58
|
data: {
|
|
59
59
|
status: nextStatus,
|
|
60
60
|
lastActionAt: operatedAt,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
ownerUserScopeKey: ticket.ownerUserScopeKey,
|
|
62
|
+
ownerDeptScopeKey: ticket.ownerDeptScopeKey,
|
|
63
|
+
collegeScopeKey: ticket.collegeScopeKey,
|
|
64
|
+
classScopeKey: ticket.classScopeKey,
|
|
65
65
|
},
|
|
66
66
|
} as never);
|
|
67
67
|
|
|
@@ -16,6 +16,7 @@ export type FormComponentName =
|
|
|
16
16
|
| "ImageField"
|
|
17
17
|
| "AddressField"
|
|
18
18
|
| "CascadeSelectField"
|
|
19
|
+
| "AssociationFormField"
|
|
19
20
|
| "LocationField"
|
|
20
21
|
| "EditorField"
|
|
21
22
|
| "JSONField"
|
|
@@ -33,6 +34,7 @@ export type FieldDefinition = {
|
|
|
33
34
|
componentName: FormComponentName;
|
|
34
35
|
label: string;
|
|
35
36
|
required?: boolean;
|
|
37
|
+
behavior?: "NORMAL" | "READONLY" | "DISABLED" | "HIDDEN";
|
|
36
38
|
rules?: Array<{
|
|
37
39
|
required?: boolean;
|
|
38
40
|
message?: string;
|