create-windy 0.6.21 → 0.7.0
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/dist/cli.js +11 -4
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/apps/server/src/audit/policy.ts +6 -1
- package/template/apps/server/src/auth/bootstrap-admin.ts +10 -1
- package/template/apps/server/src/auth/composition.ts +5 -0
- package/template/apps/server/src/auth/credential-authorization.test.ts +257 -0
- package/template/apps/server/src/auth/credential-routes.test.ts +1 -0
- package/template/apps/server/src/auth/credential-service.ts +47 -12
- package/template/apps/server/src/auth/drizzle-repository.ts +13 -5
- package/template/apps/server/src/auth/drizzle-session-operations.ts +1 -0
- package/template/apps/server/src/auth/repository.ts +9 -6
- package/template/apps/server/src/auth/routes.test-support.ts +1 -0
- package/template/apps/server/src/auth/runtime.ts +4 -0
- package/template/apps/server/src/auth/service-options.ts +2 -0
- package/template/apps/server/src/auth/service.ts +2 -0
- package/template/apps/server/src/auth/user-credential-access.ts +37 -0
- package/template/apps/server/src/foundation-activation.test.ts +24 -1
- package/template/apps/server/src/foundation.ts +0 -2
- package/template/apps/server/src/index.ts +1 -0
- package/template/apps/server/src/runtime-development.ts +2 -0
- package/template/apps/server/src/runtime.test.ts +13 -4
- package/template/apps/server/src/system/department-organization.ts +58 -59
- package/template/apps/server/src/system/department-read-routes.ts +54 -0
- package/template/apps/server/src/system/department-routes.test.ts +27 -19
- package/template/apps/server/src/system/department-routes.ts +95 -33
- package/template/apps/server/src/system/department-transition-safety.test.ts +148 -0
- package/template/apps/server/src/system/department-validation.ts +79 -0
- package/template/apps/server/src/system/drizzle-repository.ts +14 -0
- package/template/apps/server/src/system/drizzle-system-configs.ts +2 -0
- package/template/apps/server/src/system/identity-read-model.ts +105 -0
- package/template/apps/server/src/system/identity-role-assignment.test.ts +199 -0
- package/template/apps/server/src/system/identity-role-assignment.ts +148 -0
- package/template/apps/server/src/system/identity-route-config.ts +29 -7
- package/template/apps/server/src/system/identity-write-security.test.ts +127 -0
- package/template/apps/server/src/system/identity-write-validation.ts +170 -0
- package/template/apps/server/src/system/organization-read-security.test.ts +221 -0
- package/template/apps/server/src/system/redis-health.test.ts +1 -1
- package/template/apps/server/src/system/repository.ts +29 -2
- package/template/apps/server/src/system/resource-routes.ts +116 -72
- package/template/apps/server/src/system/route-audit.test.ts +117 -0
- package/template/apps/server/src/system/route-helpers.ts +14 -0
- package/template/apps/server/src/system/route-types.ts +11 -2
- package/template/apps/server/src/system/routes.test.ts +0 -68
- package/template/apps/server/src/system/routes.ts +29 -2
- package/template/apps/server/src/system/user-account-policy.test.ts +3 -1
- package/template/apps/server/src/system/user-account-policy.ts +26 -9
- package/template/apps/web/src/pages/system/SystemResourceRoutePage.vue +27 -13
- package/template/apps/web/src/pages/system/SystemResourceRoutePage.webtest.ts +68 -0
- package/template/apps/web/src/pages/system/components/PagedSelectionList.vue +131 -0
- package/template/apps/web/src/pages/system/components/PagedSelectionList.webtest.ts +32 -0
- package/template/apps/web/src/pages/system/components/SystemRecordDetailDialog.vue +67 -0
- package/template/apps/web/src/pages/system/components/SystemResourceEditor.vue +114 -38
- package/template/apps/web/src/pages/system/components/SystemResourceEditor.webtest.ts +180 -0
- package/template/apps/web/src/pages/system/components/SystemResourceFilters.vue +8 -0
- package/template/apps/web/src/pages/system/components/SystemResourceForm.vue +8 -2
- package/template/apps/web/src/pages/system/components/SystemResourcePage.department.webtest.ts +89 -0
- package/template/apps/web/src/pages/system/components/SystemResourcePage.vue +42 -46
- package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +12 -55
- package/template/apps/web/src/pages/system/components/SystemResourceTable.vue +14 -2
- package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationForm.vue +121 -85
- package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationForm.webtest.ts +57 -0
- package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationTable.vue +227 -0
- package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationTable.webtest.ts +62 -0
- package/template/apps/web/src/pages/system/components/department/DepartmentTransitionDialog.vue +72 -52
- package/template/apps/web/src/pages/system/components/role-policy/RolePermissionTree.vue +150 -79
- package/template/apps/web/src/pages/system/components/role-policy/RolePolicyForm.vue +98 -8
- package/template/apps/web/src/pages/system/components/role-policy/RolePolicyForm.webtest.ts +108 -0
- package/template/apps/web/src/pages/system/components/selection-options.ts +71 -0
- package/template/apps/web/src/pages/system/components/selection-options.webtest.ts +49 -0
- package/template/apps/web/src/pages/system/components/user-account/CreateDepartmentDialog.vue +19 -27
- package/template/apps/web/src/pages/system/components/user-account/CreateDepartmentDialog.webtest.ts +2 -6
- package/template/apps/web/src/pages/system/components/user-account/CreateRoleDialog.vue +128 -0
- package/template/apps/web/src/pages/system/components/user-account/CreateRoleDialog.webtest.ts +76 -0
- package/template/apps/web/src/pages/system/components/user-account/RolePermissionSummary.vue +61 -3
- package/template/apps/web/src/pages/system/components/user-account/UserAccountForm.vue +144 -90
- package/template/apps/web/src/pages/system/components/user-account/UserAccountForm.webtest.ts +95 -0
- package/template/apps/web/src/pages/system/components/user-account/UserListFilters.vue +168 -0
- package/template/apps/web/src/pages/system/components/user-account/UserRoleSelector.vue +33 -57
- package/template/apps/web/src/pages/system/components/user-account/UserRoleSelector.webtest.ts +4 -4
- package/template/apps/web/src/pages/system/components/user-account/save-user-account.ts +4 -1
- package/template/apps/web/src/pages/system/components/user-account/user-account-draft.ts +81 -0
- package/template/apps/web/src/pages/system/composables/useDepartmentTransitions.ts +6 -10
- package/template/apps/web/src/pages/system/composables/useResourceEditor.ts +72 -0
- package/template/apps/web/src/pages/system/composables/useResourceEditor.webtest.ts +110 -0
- package/template/apps/web/src/pages/system/composables/useSystemResource.ts +27 -1
- package/template/apps/web/src/pages/system/resource-config.ts +2 -0
- package/template/apps/web/src/services/system-api.ts +25 -0
- package/template/docs/architecture/foundation-infrastructure.md +12 -25
- package/template/docs/development/crud-generator.md +1 -1
- package/template/docs/operations/legacy-data-migration.md +73 -0
- package/template/docs/platform/bulk-data-jobs.md +1 -1
- package/template/docs/platform/module-configuration.md +2 -2
- package/template/docs/platform/organization-membership.md +37 -6
- package/template/docs/platform/scheduled-task-executor.md +1 -1
- package/template/package.json +2 -1
- package/template/packages/config/index.test.ts +1 -5
- package/template/packages/config/package.json +2 -2
- package/template/packages/config/src/platform-boundary.test.ts +39 -0
- package/template/packages/config/src/platform.ts +0 -28
- package/template/packages/database/drizzle/0063_license_project_lifecycle.sql +28 -0
- package/template/packages/database/drizzle/meta/0063_snapshot.json +15053 -0
- package/template/packages/database/drizzle/meta/_journal.json +7 -0
- package/template/packages/database/package.json +2 -2
- package/template/packages/database/src/agent-runtime-schema.test.ts +5 -1
- package/template/packages/database/src/schema/index.ts +1 -0
- package/template/packages/database/src/schema/license-issuance-requests.ts +59 -0
- package/template/packages/database/src/schema/license-v1.ts +5 -0
- package/template/packages/jobs/package.json +2 -2
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/system-api-permissions.ts +14 -0
- package/template/packages/modules/src/system-features.ts +0 -11
- package/template/packages/modules/src/system-module-catalog.test.ts +4 -0
- package/template/packages/modules/src/system-module-catalog.ts +0 -1
- package/template/packages/server-sdk/index.ts +1 -49
- package/template/packages/server-sdk/package.json +3 -2
- package/template/packages/server-sdk/src/ai-runtime-readiness-exports.ts +49 -0
- package/template/packages/shared/index.ts +2 -1
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/license-console-capabilities.ts +163 -0
- package/template/packages/shared/src/license-console.ts +94 -0
- package/template/packages/shared/src/license.ts +5 -0
- package/template/docs/adapters/ruoyi-vue-compatibility.md +0 -93
- package/template/packages/shared/src/ruoyi.ts +0 -98
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
2
|
+
import { flushPromises, mount, type VueWrapper } from "@vue/test-utils";
|
|
3
|
+
import { defineComponent, h, ref } from "vue";
|
|
4
|
+
import { createMemoryHistory, createRouter, RouterView } from "vue-router";
|
|
5
|
+
import FormDialog from "@/components/common/FormDialog.vue";
|
|
6
|
+
import UnsavedChangesDialog from "@/components/common/UnsavedChangesDialog.vue";
|
|
7
|
+
import SystemResourceEditor from "./SystemResourceEditor.vue";
|
|
8
|
+
import SystemResourceForm from "./SystemResourceForm.vue";
|
|
9
|
+
import { systemResourceConfigs } from "../resource-config";
|
|
10
|
+
const mounted: VueWrapper[] = [];
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
for (const wrapper of mounted.splice(0)) wrapper.unmount();
|
|
13
|
+
});
|
|
14
|
+
async function setup(
|
|
15
|
+
save = vi
|
|
16
|
+
.fn<(_payload: Record<string, unknown>) => Promise<boolean>>()
|
|
17
|
+
.mockResolvedValue(true),
|
|
18
|
+
) {
|
|
19
|
+
const EditorPage = defineComponent({
|
|
20
|
+
setup() {
|
|
21
|
+
const open = ref(true);
|
|
22
|
+
return () =>
|
|
23
|
+
h(SystemResourceEditor, {
|
|
24
|
+
open: open.value,
|
|
25
|
+
"onUpdate:open": (value: boolean) => {
|
|
26
|
+
open.value = value;
|
|
27
|
+
},
|
|
28
|
+
config: systemResourceConfigs.dictionaries,
|
|
29
|
+
defaults: { code: "original", name: "原名称" },
|
|
30
|
+
save,
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
const router = createRouter({
|
|
35
|
+
history: createMemoryHistory(),
|
|
36
|
+
routes: [
|
|
37
|
+
{ path: "/edit/:tab?", component: EditorPage },
|
|
38
|
+
{ path: "/other", component: { template: "<div>另一页面</div>" } },
|
|
39
|
+
],
|
|
40
|
+
});
|
|
41
|
+
await router.push("/edit/one");
|
|
42
|
+
await router.isReady();
|
|
43
|
+
const wrapper = mount(
|
|
44
|
+
defineComponent(() => () => h(RouterView)),
|
|
45
|
+
{ global: { plugins: [router] } },
|
|
46
|
+
);
|
|
47
|
+
mounted.push(wrapper);
|
|
48
|
+
await flushPromises();
|
|
49
|
+
const editor = wrapper.getComponent(SystemResourceEditor);
|
|
50
|
+
const input = editor
|
|
51
|
+
.getComponent(SystemResourceForm)
|
|
52
|
+
.get('input[placeholder="例如 invoice_status"]');
|
|
53
|
+
return { wrapper, editor, input, router, save };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe("编辑容器未保存保护", () => {
|
|
57
|
+
test("关闭时取消保留输入,保存失败保留对话框和草稿", async () => {
|
|
58
|
+
const { editor, input, save } = await setup(
|
|
59
|
+
vi.fn().mockResolvedValue(false),
|
|
60
|
+
);
|
|
61
|
+
await input.setValue("draft");
|
|
62
|
+
const closing = editor.getComponent(FormDialog).props("beforeClose")!();
|
|
63
|
+
await flushPromises();
|
|
64
|
+
const confirmation = editor.getComponent(UnsavedChangesDialog);
|
|
65
|
+
expect(confirmation.props("open")).toBe(true);
|
|
66
|
+
confirmation.vm.$emit("save");
|
|
67
|
+
await flushPromises();
|
|
68
|
+
expect(save).toHaveBeenCalledWith(
|
|
69
|
+
expect.objectContaining({ code: "draft" }),
|
|
70
|
+
);
|
|
71
|
+
expect(confirmation.props("open")).toBe(true);
|
|
72
|
+
expect(editor.props("open")).toBe(true);
|
|
73
|
+
expect((input.element as HTMLInputElement).value).toBe("draft");
|
|
74
|
+
confirmation.vm.$emit("cancel");
|
|
75
|
+
expect(await closing).toBe(false);
|
|
76
|
+
expect(editor.props("open")).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("恢复加载基线后不再拦截关闭和刷新", async () => {
|
|
80
|
+
const { editor, input } = await setup();
|
|
81
|
+
await input.setValue("draft");
|
|
82
|
+
const dirtyUnload = new Event("beforeunload", { cancelable: true });
|
|
83
|
+
window.dispatchEvent(dirtyUnload);
|
|
84
|
+
expect(dirtyUnload.defaultPrevented).toBe(true);
|
|
85
|
+
await input.setValue("original");
|
|
86
|
+
const cleanUnload = new Event("beforeunload", { cancelable: true });
|
|
87
|
+
window.dispatchEvent(cleanUnload);
|
|
88
|
+
expect(cleanUnload.defaultPrevented).toBe(false);
|
|
89
|
+
expect(await editor.getComponent(FormDialog).props("beforeClose")!()).toBe(
|
|
90
|
+
true,
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("路由更新或离开都先确认;保存成功才继续导航", async () => {
|
|
95
|
+
const { editor, input, router } = await setup();
|
|
96
|
+
await input.setValue("draft");
|
|
97
|
+
const switching = router.push("/edit/two");
|
|
98
|
+
await flushPromises();
|
|
99
|
+
editor.getComponent(UnsavedChangesDialog).vm.$emit("cancel");
|
|
100
|
+
await switching;
|
|
101
|
+
expect(router.currentRoute.value.path).toBe("/edit/one");
|
|
102
|
+
const leaving = router.push("/other");
|
|
103
|
+
await flushPromises();
|
|
104
|
+
editor.getComponent(UnsavedChangesDialog).vm.$emit("save");
|
|
105
|
+
await leaving;
|
|
106
|
+
expect(router.currentRoute.value.path).toBe("/other");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("异步保存期间显示loading并阻止重复提交及关闭", async () => {
|
|
110
|
+
let finish: ((value: boolean) => void) | undefined;
|
|
111
|
+
const save = vi.fn(
|
|
112
|
+
() =>
|
|
113
|
+
new Promise<boolean>((resolve) => {
|
|
114
|
+
finish = resolve;
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
const { editor, input } = await setup(save);
|
|
118
|
+
await input.setValue("draft");
|
|
119
|
+
const form = editor.getComponent(FormDialog);
|
|
120
|
+
form.vm.$emit("submit");
|
|
121
|
+
await flushPromises();
|
|
122
|
+
expect(form.props("loading")).toBe(true);
|
|
123
|
+
form.vm.$emit("submit");
|
|
124
|
+
expect(await form.props("beforeClose")!()).toBe(false);
|
|
125
|
+
expect(save).toHaveBeenCalledTimes(1);
|
|
126
|
+
finish?.(true);
|
|
127
|
+
await flushPromises();
|
|
128
|
+
expect(editor.props("open")).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
test("先完成嵌套弹窗确认,再确认和保存包含新选择的账户草稿", async () => {
|
|
131
|
+
let finishChild: ((value: boolean) => void) | undefined;
|
|
132
|
+
const childBeforeClose = vi
|
|
133
|
+
.fn<() => Promise<boolean>>()
|
|
134
|
+
.mockImplementationOnce(
|
|
135
|
+
() =>
|
|
136
|
+
new Promise((resolve) => {
|
|
137
|
+
finishChild = resolve;
|
|
138
|
+
}),
|
|
139
|
+
)
|
|
140
|
+
.mockResolvedValue(true);
|
|
141
|
+
const payload: Record<string, unknown> = { departmentId: "old_department" };
|
|
142
|
+
const NestedForm = defineComponent({
|
|
143
|
+
emits: ["submit"],
|
|
144
|
+
setup(_, { expose, emit }) {
|
|
145
|
+
expose({
|
|
146
|
+
dirty: true,
|
|
147
|
+
beforeClose: childBeforeClose,
|
|
148
|
+
markSaved: vi.fn(),
|
|
149
|
+
submit: () => emit("submit", { ...payload }),
|
|
150
|
+
});
|
|
151
|
+
return () => h("div", "嵌套新建部门草稿");
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
const save = vi.fn().mockResolvedValue(true);
|
|
155
|
+
const wrapper = mount(SystemResourceEditor, {
|
|
156
|
+
props: {
|
|
157
|
+
open: true,
|
|
158
|
+
config: systemResourceConfigs.dictionaries,
|
|
159
|
+
defaults: {},
|
|
160
|
+
save,
|
|
161
|
+
},
|
|
162
|
+
global: { stubs: { SystemResourceForm: NestedForm } },
|
|
163
|
+
});
|
|
164
|
+
mounted.push(wrapper);
|
|
165
|
+
await flushPromises();
|
|
166
|
+
const closing = wrapper.getComponent(FormDialog).props("beforeClose")!();
|
|
167
|
+
await flushPromises();
|
|
168
|
+
expect(wrapper.getComponent(UnsavedChangesDialog).props("open")).toBe(
|
|
169
|
+
false,
|
|
170
|
+
);
|
|
171
|
+
expect(save).not.toHaveBeenCalled();
|
|
172
|
+
payload.departmentId = "created_department";
|
|
173
|
+
finishChild?.(true);
|
|
174
|
+
await flushPromises();
|
|
175
|
+
expect(wrapper.getComponent(UnsavedChangesDialog).props("open")).toBe(true);
|
|
176
|
+
wrapper.getComponent(UnsavedChangesDialog).vm.$emit("save");
|
|
177
|
+
expect(await closing).toBe(true);
|
|
178
|
+
expect(save).toHaveBeenCalledWith({ departmentId: "created_department" });
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Button } from "@/components/ui/button";
|
|
3
3
|
import { Input } from "@/components/ui/input";
|
|
4
4
|
import { SearchIcon } from "@lucide/vue";
|
|
5
|
+
import UserListFilters from "./user-account/UserListFilters.vue";
|
|
5
6
|
import type { SystemResourceConfig } from "../resource-config";
|
|
6
7
|
// @windy-module system.audit begin
|
|
7
8
|
import AuditLogFilters from "./AuditLogFilters.vue";
|
|
@@ -23,6 +24,9 @@ defineProps<{ config: SystemResourceConfig }>();
|
|
|
23
24
|
const keyword = defineModel<string>("keyword", { required: true });
|
|
24
25
|
const emit = defineEmits<{
|
|
25
26
|
search: [];
|
|
27
|
+
identityChange: [
|
|
28
|
+
filters: { departmentId?: string; roleId?: string; status?: string },
|
|
29
|
+
];
|
|
26
30
|
preset: [value: string];
|
|
27
31
|
auditChange: [filters: AuditLogFilterValue];
|
|
28
32
|
auditExportOptions: [options: AuditExportOptions];
|
|
@@ -30,6 +34,10 @@ const emit = defineEmits<{
|
|
|
30
34
|
</script>
|
|
31
35
|
|
|
32
36
|
<template>
|
|
37
|
+
<UserListFilters
|
|
38
|
+
v-if="config.key === 'users'"
|
|
39
|
+
@change="emit('identityChange', $event)"
|
|
40
|
+
/>
|
|
33
41
|
<!-- @windy-module system.audit begin -->
|
|
34
42
|
<AuditLogFilters
|
|
35
43
|
v-if="config.usesCustomFilters"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { watch, shallowRef } from "vue";
|
|
2
|
+
import { computed, watch, shallowRef } from "vue";
|
|
3
3
|
import { Input } from "@/components/ui/input";
|
|
4
4
|
import type { ResourceField } from "../resource-config";
|
|
5
5
|
import type { SystemRecord } from "@/services/system-api";
|
|
@@ -15,11 +15,17 @@ const emit = defineEmits<{
|
|
|
15
15
|
}>();
|
|
16
16
|
|
|
17
17
|
const form = shallowRef<Record<string, unknown>>({});
|
|
18
|
+
const baseline = shallowRef("");
|
|
19
|
+
const dirty = computed(() => JSON.stringify(form.value) !== baseline.value);
|
|
20
|
+
function markSaved() {
|
|
21
|
+
baseline.value = JSON.stringify(form.value);
|
|
22
|
+
}
|
|
18
23
|
|
|
19
24
|
watch(
|
|
20
25
|
() => [props.item, props.defaults] as const,
|
|
21
26
|
() => {
|
|
22
27
|
form.value = { ...props.defaults, ...(props.item || {}) };
|
|
28
|
+
markSaved();
|
|
23
29
|
},
|
|
24
30
|
{ immediate: true },
|
|
25
31
|
);
|
|
@@ -42,7 +48,7 @@ function submit() {
|
|
|
42
48
|
emit("submit", form.value);
|
|
43
49
|
}
|
|
44
50
|
|
|
45
|
-
defineExpose({ submit });
|
|
51
|
+
defineExpose({ submit, dirty, markSaved });
|
|
46
52
|
</script>
|
|
47
53
|
|
|
48
54
|
<template>
|
package/template/apps/web/src/pages/system/components/SystemResourcePage.department.webtest.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { afterEach, expect, test, vi } from "vitest";
|
|
2
|
+
import { flushPromises, mount } from "@vue/test-utils";
|
|
3
|
+
import { systemResourceConfigs } from "../resource-config";
|
|
4
|
+
import SystemResourcePage from "./SystemResourcePage.vue";
|
|
5
|
+
import DepartmentOrganizationTable from "./department/DepartmentOrganizationTable.vue";
|
|
6
|
+
import DepartmentOrganizationForm from "./department/DepartmentOrganizationForm.vue";
|
|
7
|
+
import DepartmentTransitionDialog from "./department/DepartmentTransitionDialog.vue";
|
|
8
|
+
const api = vi.hoisted(() => ({
|
|
9
|
+
list: vi.fn(),
|
|
10
|
+
organization: vi.fn(),
|
|
11
|
+
preview: vi.fn(),
|
|
12
|
+
transitionDepartment: vi.fn(),
|
|
13
|
+
}));
|
|
14
|
+
vi.mock("@/services/system-api", async () => ({
|
|
15
|
+
...(await vi.importActual<typeof import("@/services/system-api")>(
|
|
16
|
+
"@/services/system-api",
|
|
17
|
+
)),
|
|
18
|
+
listSystemResource: api.list,
|
|
19
|
+
listDepartmentOrganization: api.organization,
|
|
20
|
+
previewDepartmentTransition: api.preview,
|
|
21
|
+
transitionDepartment: api.transitionDepartment,
|
|
22
|
+
}));
|
|
23
|
+
afterEach(() => vi.clearAllMocks());
|
|
24
|
+
test("部门停用和删除使用显式迁移目标", async () => {
|
|
25
|
+
const department = {
|
|
26
|
+
id: "dept_sales",
|
|
27
|
+
code: "sales",
|
|
28
|
+
name: "销售部",
|
|
29
|
+
parentId: "dept_platform",
|
|
30
|
+
status: "active",
|
|
31
|
+
};
|
|
32
|
+
api.organization.mockResolvedValue({
|
|
33
|
+
items: [department],
|
|
34
|
+
total: 1,
|
|
35
|
+
page: 1,
|
|
36
|
+
pageSize: 10,
|
|
37
|
+
});
|
|
38
|
+
api.list.mockResolvedValue({
|
|
39
|
+
items: [department],
|
|
40
|
+
total: 1,
|
|
41
|
+
page: 1,
|
|
42
|
+
pageSize: 10,
|
|
43
|
+
});
|
|
44
|
+
api.preview.mockResolvedValue({
|
|
45
|
+
childDepartmentCount: 0,
|
|
46
|
+
userCount: 1,
|
|
47
|
+
roleCount: 0,
|
|
48
|
+
});
|
|
49
|
+
api.transitionDepartment.mockResolvedValue({
|
|
50
|
+
department: { ...department, status: "disabled" },
|
|
51
|
+
movedChildren: 0,
|
|
52
|
+
movedUsers: 1,
|
|
53
|
+
updatedRoles: 0,
|
|
54
|
+
});
|
|
55
|
+
const wrapper = mount(SystemResourcePage, {
|
|
56
|
+
props: { config: systemResourceConfigs.departments },
|
|
57
|
+
});
|
|
58
|
+
await flushPromises();
|
|
59
|
+
|
|
60
|
+
wrapper
|
|
61
|
+
.getComponent(DepartmentOrganizationTable)
|
|
62
|
+
.vm.$emit("edit", department);
|
|
63
|
+
await flushPromises();
|
|
64
|
+
wrapper.getComponent(DepartmentOrganizationForm).vm.$emit("submit", {
|
|
65
|
+
...department,
|
|
66
|
+
status: "disabled",
|
|
67
|
+
_transitionTargetDepartmentId: "dept_platform",
|
|
68
|
+
});
|
|
69
|
+
await flushPromises();
|
|
70
|
+
expect(api.transitionDepartment).toHaveBeenCalledWith("dept_sales", {
|
|
71
|
+
action: "disable",
|
|
72
|
+
targetDepartmentId: "dept_platform",
|
|
73
|
+
updates: { name: "销售部", code: "sales", parentId: "dept_platform" },
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
wrapper
|
|
77
|
+
.getComponent(DepartmentOrganizationTable)
|
|
78
|
+
.vm.$emit("remove", department);
|
|
79
|
+
await flushPromises();
|
|
80
|
+
wrapper
|
|
81
|
+
.getComponent(DepartmentTransitionDialog)
|
|
82
|
+
.vm.$emit("confirm", "dept_platform");
|
|
83
|
+
await flushPromises();
|
|
84
|
+
expect(api.transitionDepartment).toHaveBeenLastCalledWith("dept_sales", {
|
|
85
|
+
action: "delete",
|
|
86
|
+
targetDepartmentId: "dept_platform",
|
|
87
|
+
});
|
|
88
|
+
wrapper.unmount();
|
|
89
|
+
});
|
|
@@ -15,7 +15,9 @@ import DictionaryBulkPanel from "./dictionary-bulk/DictionaryBulkPanel.vue";
|
|
|
15
15
|
import FeatureLicensePolicyDialog from "./FeatureLicensePolicyDialog.vue";
|
|
16
16
|
import SystemResourceFilters from "./SystemResourceFilters.vue";
|
|
17
17
|
import { useFeatureLicenseEditor } from "../composables/useFeatureLicenseEditor";
|
|
18
|
-
import {
|
|
18
|
+
import { useResourceEditor } from "../composables/useResourceEditor";
|
|
19
|
+
import DepartmentOrganizationTable from "./department/DepartmentOrganizationTable.vue";
|
|
20
|
+
import SystemRecordDetailDialog from "./SystemRecordDetailDialog.vue";
|
|
19
21
|
import DepartmentTransitionDialog from "./department/DepartmentTransitionDialog.vue";
|
|
20
22
|
import { useDepartmentTransitions } from "../composables/useDepartmentTransitions";
|
|
21
23
|
import SystemResourceActions from "./SystemResourceActions.vue";
|
|
@@ -57,9 +59,7 @@ const resource = useSystemResource({
|
|
|
57
59
|
});
|
|
58
60
|
const toast = useAppToast();
|
|
59
61
|
|
|
60
|
-
const editorOpen = shallowRef(false);
|
|
61
62
|
const confirmOpen = shallowRef(false);
|
|
62
|
-
const editingItem = shallowRef<SystemRecord | undefined>();
|
|
63
63
|
const viewingItem = shallowRef<SystemRecord | undefined>();
|
|
64
64
|
const detailOpen = shallowRef(false);
|
|
65
65
|
const pendingDelete = shallowRef<SystemRecord | undefined>();
|
|
@@ -70,6 +70,13 @@ const departmentTransitions = useDepartmentTransitions({
|
|
|
70
70
|
reload: resource.reload,
|
|
71
71
|
mutated: () => emit("mutated"),
|
|
72
72
|
});
|
|
73
|
+
const editor = useResourceEditor({
|
|
74
|
+
key: props.config.key,
|
|
75
|
+
save: resource.save,
|
|
76
|
+
disable: departmentTransitions.disable,
|
|
77
|
+
mutated: () => emit("mutated"),
|
|
78
|
+
});
|
|
79
|
+
const { open: editorOpen, item: editingItem } = editor;
|
|
73
80
|
const {
|
|
74
81
|
open: licenseEditorOpen,
|
|
75
82
|
item: licenseEditorItem,
|
|
@@ -85,7 +92,7 @@ const {
|
|
|
85
92
|
onFailed: (description) =>
|
|
86
93
|
toast.error("License 范围更新失败", { description }),
|
|
87
94
|
});
|
|
88
|
-
const formDefaults =
|
|
95
|
+
const formDefaults = shallowRef(props.config.createDefaults());
|
|
89
96
|
const displayedItems = computed(() =>
|
|
90
97
|
resource.items.value.map((item) => {
|
|
91
98
|
const inlineToggle = props.config.inlineToggle;
|
|
@@ -100,13 +107,11 @@ onMounted(() => {
|
|
|
100
107
|
});
|
|
101
108
|
|
|
102
109
|
function openCreate() {
|
|
103
|
-
|
|
104
|
-
|
|
110
|
+
formDefaults.value = props.config.createDefaults();
|
|
111
|
+
editor.edit();
|
|
105
112
|
}
|
|
106
|
-
|
|
107
113
|
function openEdit(item: SystemRecord) {
|
|
108
|
-
|
|
109
|
-
editorOpen.value = true;
|
|
114
|
+
editor.edit(item);
|
|
110
115
|
}
|
|
111
116
|
|
|
112
117
|
function openDelete(item: SystemRecord) {
|
|
@@ -119,41 +124,6 @@ function openDetail(item: SystemRecord) {
|
|
|
119
124
|
detailOpen.value = true;
|
|
120
125
|
}
|
|
121
126
|
|
|
122
|
-
async function submit(payload: Record<string, unknown>) {
|
|
123
|
-
if (
|
|
124
|
-
props.config.key === "departments" &&
|
|
125
|
-
editingItem.value &&
|
|
126
|
-
editingItem.value.status === "active" &&
|
|
127
|
-
payload.status === "disabled"
|
|
128
|
-
) {
|
|
129
|
-
const targetDepartmentId =
|
|
130
|
-
typeof payload._transitionTargetDepartmentId === "string"
|
|
131
|
-
? payload._transitionTargetDepartmentId
|
|
132
|
-
: undefined;
|
|
133
|
-
const disabled = await departmentTransitions.disable(
|
|
134
|
-
editingItem.value,
|
|
135
|
-
targetDepartmentId,
|
|
136
|
-
);
|
|
137
|
-
if (disabled) editorOpen.value = false;
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
const normalizedPayload = { ...payload };
|
|
141
|
-
delete normalizedPayload._transitionTargetDepartmentId;
|
|
142
|
-
const saved =
|
|
143
|
-
props.config.key === "users"
|
|
144
|
-
? await saveUserAccount(
|
|
145
|
-
normalizedPayload,
|
|
146
|
-
editingItem.value?.id,
|
|
147
|
-
resource.save,
|
|
148
|
-
toast,
|
|
149
|
-
)
|
|
150
|
-
: await resource.save(normalizedPayload, editingItem.value?.id);
|
|
151
|
-
if (saved) {
|
|
152
|
-
editorOpen.value = false;
|
|
153
|
-
emit("mutated");
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
127
|
async function toggleInline(item: SystemRecord, checked: boolean) {
|
|
158
128
|
const inlineToggle = props.config.inlineToggle;
|
|
159
129
|
if (!inlineToggle || pendingToggleId.value) return;
|
|
@@ -245,6 +215,7 @@ async function importData(file: File) {
|
|
|
245
215
|
@preset="applySearchPreset"
|
|
246
216
|
@audit-change="resource.setAuditFilters"
|
|
247
217
|
@audit-export-options="resource.setExportOptions"
|
|
218
|
+
@identity-change="resource.setIdentityFilters"
|
|
248
219
|
/>
|
|
249
220
|
</CardHeader>
|
|
250
221
|
<CardContent class="space-y-4">
|
|
@@ -253,6 +224,17 @@ async function importData(file: File) {
|
|
|
253
224
|
:error="resource.error.value"
|
|
254
225
|
@retry="resource.reload"
|
|
255
226
|
/>
|
|
227
|
+
<DepartmentOrganizationTable
|
|
228
|
+
v-else-if="config.key === 'departments'"
|
|
229
|
+
:items="displayedItems"
|
|
230
|
+
:keyword="resource.q.value"
|
|
231
|
+
:loading="resource.loading.value"
|
|
232
|
+
:editable="resource.canUpdate.value"
|
|
233
|
+
:deletable="resource.canDelete.value"
|
|
234
|
+
@edit="openEdit"
|
|
235
|
+
@remove="openDelete"
|
|
236
|
+
@view="openDetail"
|
|
237
|
+
/>
|
|
256
238
|
<SystemResourceTable
|
|
257
239
|
v-else
|
|
258
240
|
:columns="config.columns"
|
|
@@ -262,7 +244,9 @@ async function importData(file: File) {
|
|
|
262
244
|
:inline-toggle="config.inlineToggle"
|
|
263
245
|
:toggle-pending-id="pendingToggleId"
|
|
264
246
|
:loading="resource.loading.value"
|
|
265
|
-
:viewable="
|
|
247
|
+
:viewable="
|
|
248
|
+
auditViewable || config.key === 'users' || config.key === 'roles'
|
|
249
|
+
"
|
|
266
250
|
:page="resource.page.value"
|
|
267
251
|
:page-size="resource.pageSize.value"
|
|
268
252
|
@edit="openEdit"
|
|
@@ -272,6 +256,7 @@ async function importData(file: File) {
|
|
|
272
256
|
@view="openDetail"
|
|
273
257
|
/>
|
|
274
258
|
<DataPagination
|
|
259
|
+
v-if="config.key !== 'departments'"
|
|
275
260
|
:page="resource.page.value"
|
|
276
261
|
:page-size="resource.pageSize.value"
|
|
277
262
|
:total="resource.total.value"
|
|
@@ -308,7 +293,8 @@ async function importData(file: File) {
|
|
|
308
293
|
:permissions="permissions"
|
|
309
294
|
:department-create-mode="departmentCreateMode"
|
|
310
295
|
:loading="resource.saving.value || departmentTransitions.loading.value"
|
|
311
|
-
|
|
296
|
+
:save="editor.submit"
|
|
297
|
+
:error="resource.error.value"
|
|
312
298
|
/>
|
|
313
299
|
|
|
314
300
|
<DepartmentTransitionDialog
|
|
@@ -337,6 +323,16 @@ async function importData(file: File) {
|
|
|
337
323
|
@save="saveLicensePolicy"
|
|
338
324
|
/>
|
|
339
325
|
|
|
326
|
+
<SystemRecordDetailDialog
|
|
327
|
+
v-if="
|
|
328
|
+
config.key === 'users' ||
|
|
329
|
+
config.key === 'departments' ||
|
|
330
|
+
config.key === 'roles'
|
|
331
|
+
"
|
|
332
|
+
v-model:open="detailOpen"
|
|
333
|
+
:config="config"
|
|
334
|
+
:item="viewingItem"
|
|
335
|
+
/>
|
|
340
336
|
<!-- @windy-module system.audit begin -->
|
|
341
337
|
<AuditLogDetailDialog
|
|
342
338
|
v-if="config.key === 'auditLogs'"
|
|
@@ -4,14 +4,14 @@ import { systemResourceConfigs } from "../resource-config";
|
|
|
4
4
|
import SystemResourcePage from "./SystemResourcePage.vue";
|
|
5
5
|
import FeatureLicensePolicyDialog from "./FeatureLicensePolicyDialog.vue";
|
|
6
6
|
import SystemResourceTable from "./SystemResourceTable.vue";
|
|
7
|
-
import DepartmentOrganizationForm from "./department/DepartmentOrganizationForm.vue";
|
|
8
|
-
import DepartmentTransitionDialog from "./department/DepartmentTransitionDialog.vue";
|
|
9
7
|
|
|
10
8
|
const api = vi.hoisted(() => ({
|
|
11
9
|
list: vi.fn(),
|
|
12
10
|
update: vi.fn(),
|
|
13
11
|
quickCreateDepartment: vi.fn(),
|
|
14
12
|
transitionDepartment: vi.fn(),
|
|
13
|
+
organization: vi.fn(),
|
|
14
|
+
preview: vi.fn(),
|
|
15
15
|
loadPasswordPolicy: vi.fn(),
|
|
16
16
|
}));
|
|
17
17
|
|
|
@@ -25,6 +25,8 @@ vi.mock("@/services/system-api", async () => {
|
|
|
25
25
|
updateSystemResource: api.update,
|
|
26
26
|
quickCreateDepartment: api.quickCreateDepartment,
|
|
27
27
|
transitionDepartment: api.transitionDepartment,
|
|
28
|
+
listDepartmentOrganization: api.organization,
|
|
29
|
+
previewDepartmentTransition: api.preview,
|
|
28
30
|
};
|
|
29
31
|
});
|
|
30
32
|
|
|
@@ -255,7 +257,7 @@ describe("SystemResourcePage", () => {
|
|
|
255
257
|
});
|
|
256
258
|
// @windy-module system.audit end
|
|
257
259
|
|
|
258
|
-
test("
|
|
260
|
+
test("用户编辑完整加载组织并分页选择部门,有创建权限时显示快捷新建", async () => {
|
|
259
261
|
api.list.mockImplementation((resource: string) =>
|
|
260
262
|
Promise.resolve(
|
|
261
263
|
resource === "departments"
|
|
@@ -284,61 +286,16 @@ describe("SystemResourcePage", () => {
|
|
|
284
286
|
|
|
285
287
|
expect(document.body.textContent).toContain("所属部门");
|
|
286
288
|
expect(document.body.textContent).toContain("新建部门");
|
|
289
|
+
expect(document.body.textContent).toContain("每页 10 条");
|
|
290
|
+
expect(document.body.textContent).toContain("销售部 · sales");
|
|
287
291
|
expect(api.list).toHaveBeenCalledWith(
|
|
288
292
|
"departments",
|
|
289
|
-
expect.objectContaining({
|
|
293
|
+
expect.objectContaining({
|
|
294
|
+
pageSize: 10,
|
|
295
|
+
status: undefined,
|
|
296
|
+
sort: "order,name,id",
|
|
297
|
+
}),
|
|
290
298
|
);
|
|
291
299
|
wrapper.unmount();
|
|
292
300
|
});
|
|
293
|
-
|
|
294
|
-
test("部门停用和删除使用显式迁移目标", async () => {
|
|
295
|
-
const department = {
|
|
296
|
-
id: "dept_sales",
|
|
297
|
-
code: "sales",
|
|
298
|
-
name: "销售部",
|
|
299
|
-
parentId: "dept_platform",
|
|
300
|
-
status: "active",
|
|
301
|
-
};
|
|
302
|
-
api.list.mockResolvedValue({
|
|
303
|
-
items: [department],
|
|
304
|
-
total: 1,
|
|
305
|
-
page: 1,
|
|
306
|
-
pageSize: 10,
|
|
307
|
-
});
|
|
308
|
-
api.transitionDepartment.mockResolvedValue({
|
|
309
|
-
department: { ...department, status: "disabled" },
|
|
310
|
-
movedChildren: 0,
|
|
311
|
-
movedUsers: 1,
|
|
312
|
-
updatedRoles: 0,
|
|
313
|
-
});
|
|
314
|
-
const wrapper = mount(SystemResourcePage, {
|
|
315
|
-
props: { config: systemResourceConfigs.departments },
|
|
316
|
-
});
|
|
317
|
-
await flushPromises();
|
|
318
|
-
|
|
319
|
-
wrapper.getComponent(SystemResourceTable).vm.$emit("edit", department);
|
|
320
|
-
await flushPromises();
|
|
321
|
-
wrapper.getComponent(DepartmentOrganizationForm).vm.$emit("submit", {
|
|
322
|
-
...department,
|
|
323
|
-
status: "disabled",
|
|
324
|
-
_transitionTargetDepartmentId: "dept_platform",
|
|
325
|
-
});
|
|
326
|
-
await flushPromises();
|
|
327
|
-
expect(api.transitionDepartment).toHaveBeenCalledWith("dept_sales", {
|
|
328
|
-
action: "disable",
|
|
329
|
-
targetDepartmentId: "dept_platform",
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
wrapper.getComponent(SystemResourceTable).vm.$emit("remove", department);
|
|
333
|
-
await flushPromises();
|
|
334
|
-
wrapper
|
|
335
|
-
.getComponent(DepartmentTransitionDialog)
|
|
336
|
-
.vm.$emit("confirm", "dept_platform");
|
|
337
|
-
await flushPromises();
|
|
338
|
-
expect(api.transitionDepartment).toHaveBeenLastCalledWith("dept_sales", {
|
|
339
|
-
action: "delete",
|
|
340
|
-
targetDepartmentId: "dept_platform",
|
|
341
|
-
});
|
|
342
|
-
wrapper.unmount();
|
|
343
|
-
});
|
|
344
301
|
});
|
|
@@ -69,6 +69,12 @@ function formatValue(item: SystemRecord, column: ResourceColumn): string {
|
|
|
69
69
|
return auditAction(value)?.label || String(value || "-");
|
|
70
70
|
if (column.format === "date-time") return formatDateTime(value);
|
|
71
71
|
if (column.format === "governed-value") return formatGovernedValue(value);
|
|
72
|
+
if (column.key === "status")
|
|
73
|
+
return value === "active"
|
|
74
|
+
? "启用"
|
|
75
|
+
: value === "disabled"
|
|
76
|
+
? "停用"
|
|
77
|
+
: String(value || "—");
|
|
72
78
|
if (typeof value === "boolean") return value ? "是" : "否";
|
|
73
79
|
if (Array.isArray(value)) return value.join(", ");
|
|
74
80
|
if (value && typeof value === "object") {
|
|
@@ -80,7 +86,9 @@ function formatValue(item: SystemRecord, column: ResourceColumn): string {
|
|
|
80
86
|
JSON.stringify(value),
|
|
81
87
|
);
|
|
82
88
|
}
|
|
83
|
-
return value
|
|
89
|
+
return value === undefined || value === null || value === ""
|
|
90
|
+
? "-"
|
|
91
|
+
: String(value);
|
|
84
92
|
}
|
|
85
93
|
|
|
86
94
|
function formatGovernedValue(value: unknown): string {
|
|
@@ -232,7 +240,11 @@ function licenseVersionLabel(version: string): string {
|
|
|
232
240
|
v-if="viewable"
|
|
233
241
|
variant="outline"
|
|
234
242
|
size="sm"
|
|
235
|
-
:aria-label="
|
|
243
|
+
:aria-label="
|
|
244
|
+
columns.some((column) => column.format === 'audit-action')
|
|
245
|
+
? `查看审计日志 ${item.id} 详情`
|
|
246
|
+
: `查看 ${String(item.displayName || item.name || item.id)} 详情`
|
|
247
|
+
"
|
|
236
248
|
@click="emit('view', item)"
|
|
237
249
|
>
|
|
238
250
|
查看详情
|