cabloy 5.1.169 → 5.1.170

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 (73) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +9 -0
  3. package/package.json +1 -1
  4. package/repo-e2e/specs/cabloy-basic.spec.ts +45 -0
  5. package/vona/packages-cli/cli/package.json +1 -1
  6. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/dto/{{resourceName}}SelectResItem.tsx_ +4 -0
  7. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/dto/{{resourceName}}SelectResItem.tsx_ +4 -0
  8. package/vona/packages-cli/cli-set-api/package.json +1 -1
  9. package/vona/pnpm-lock.yaml +77 -127
  10. package/vona/src/suite/a-training/modules/training-student/src/dto/studentSelectResItem.tsx +4 -0
  11. package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +5 -0
  12. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/package.json +54 -0
  13. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/.metadata/index.ts +346 -0
  14. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/.metadata/this.ts +2 -0
  15. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/bean/meta.index.ts +12 -0
  16. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/bean/meta.redlock.ts +11 -0
  17. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/bean/meta.version.ts +20 -0
  18. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/controller/layoutprofile.ts +38 -0
  19. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/dto/layoutProfile.tsx +26 -0
  20. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/dto/layoutProfileLoad.tsx +13 -0
  21. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/dto/layoutProfileSave.tsx +31 -0
  22. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/entity/layoutprofile.ts +20 -0
  23. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/index.ts +2 -0
  24. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/model/layoutprofile.ts +10 -0
  25. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/service/layoutprofile.ts +64 -0
  26. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/types/index.ts +1 -0
  27. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/types/layoutprofile.ts +21 -0
  28. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/test/layoutprofile.test.ts +258 -0
  29. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/tsconfig.build.json +11 -0
  30. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/tsconfig.json +7 -0
  31. package/vona/src/suite-vendor/a-cabloy/package.json +2 -1
  32. package/vona/src/suite-vendor/a-cabloy/tsconfig.json +3 -0
  33. package/zova/packages-zova/zova/package.json +2 -2
  34. package/zova/pnpm-lock.yaml +21 -6
  35. package/zova/src/suite/a-home/modules/home-base/src/service/ssrLayout.ts +20 -10
  36. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockPage/controller.tsx +44 -1
  37. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockTable/controller.tsx +1 -0
  38. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockToolbarBulk/controller.tsx +40 -25
  39. package/zova/src/suite/cabloy-basic/modules/basic-page/src/lib/selection.ts +11 -0
  40. package/zova/src/suite/cabloy-basic/modules/basic-page/test/lib/selection.test.ts +23 -0
  41. package/zova/src/suite/cabloy-basic/modules/basic-table/src/.metadata/component/actionColumnConfig.ts +31 -0
  42. package/zova/src/suite/cabloy-basic/modules/basic-table/src/.metadata/index.ts +25 -12
  43. package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/actionColumnConfig/controller.tsx +260 -0
  44. package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/table/render.tsx +3 -3
  45. package/zova/src/suite/cabloy-basic/modules/basic-table/src/config/locale/en-us.ts +10 -0
  46. package/zova/src/suite/cabloy-basic/modules/basic-table/src/config/locale/zh-cn.ts +10 -0
  47. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/cli/openapi.config.ts +9 -0
  48. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/package.json +53 -0
  49. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/.metadata/index.ts +140 -0
  50. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/.metadata/this.ts +2 -0
  51. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/api/layoutprofile.ts +67 -0
  52. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/api/openapi/baseURL.ts +5 -0
  53. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/api/openapi/index.ts +3 -0
  54. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/api/openapi/schemas.ts +663 -0
  55. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/api/openapi/types.ts +10150 -0
  56. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/apiSchema/layoutprofile.ts +25 -0
  57. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/index.ts +1 -0
  58. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/model/layoutprofile.ts +76 -0
  59. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/tsconfig.build.json +13 -0
  60. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/tsconfig.json +5 -0
  61. package/zova/src/suite-vendor/a-cabloy/package.json +2 -1
  62. package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
  63. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/resource/tableActionBulk.ts +3 -0
  64. package/zova/src/suite-vendor/a-zova/modules/a-ssr/package.json +1 -1
  65. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/lib/bodyReadyObserver.ts +13 -11
  66. package/zova/src/suite-vendor/a-zova/modules/a-table/package.json +1 -1
  67. package/zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/controller.tsx +64 -2
  68. package/zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/render.tsx +2 -3
  69. package/zova/src/suite-vendor/a-zova/modules/a-table/src/lib/beanControllerTableBase.ts +10 -0
  70. package/zova/src/suite-vendor/a-zova/modules/a-table/src/types/index.ts +1 -0
  71. package/zova/src/suite-vendor/a-zova/modules/a-table/src/types/tableLayout.ts +101 -0
  72. package/zova/src/suite-vendor/a-zova/modules/a-table/test/lib/tableLayout.test.ts +112 -0
  73. package/zova/src/suite-vendor/a-zova/package.json +4 -4
@@ -0,0 +1,258 @@
1
+ import type { TableIdentity } from 'table-identity';
2
+
3
+ import { catchError } from '@cabloy/utils';
4
+ import assert from 'node:assert';
5
+ import { randomUUID } from 'node:crypto';
6
+ import { describe, it } from 'node:test';
7
+ import { app } from 'vona-mock';
8
+
9
+ import type { ILayoutProfile } from '../src/types/layoutprofile.ts';
10
+
11
+ const actionPath = '/layoutprofile';
12
+
13
+ function createTestId() {
14
+ return randomUUID().slice(0, 12);
15
+ }
16
+
17
+ function createProfile(name: string): ILayoutProfile {
18
+ return {
19
+ version: 1,
20
+ schemaFingerprint: `schema-${name}`,
21
+ columns: [
22
+ { key: 'name', visible: true, width: 'auto' },
23
+ { key: 'level', visible: false, width: 180 },
24
+ ],
25
+ };
26
+ }
27
+
28
+ async function performAs(accessToken: string, method: string, path: string, options = {}) {
29
+ return await app.bean.executor.newCtxIsolate(async () => {
30
+ return await (app.bean.executor.performAction as any)(method, path, {
31
+ ...options,
32
+ authToken: accessToken,
33
+ innerAccess: false,
34
+ });
35
+ });
36
+ }
37
+
38
+ async function registerAndSignin(name: string) {
39
+ const user = await app.bean.user.register({ name }, true);
40
+ const token = await app.bean.passport.signinMock(name as any);
41
+ return { token: token.accessToken, user };
42
+ }
43
+
44
+ async function deleteOwnedProfiles(userIds: readonly TableIdentity[], layoutKey: string) {
45
+ for (const userId of userIds) {
46
+ await app.scope('a-layoutprofile').model.layoutprofile.delete({ userId, layoutKey });
47
+ }
48
+ }
49
+
50
+ async function deleteOwnedUsers(userIds: readonly TableIdentity[]) {
51
+ for (const userId of userIds) {
52
+ await app.scope('home-user').model.roleUser.delete({ userId });
53
+ await app.bean.user.removeById(userId);
54
+ }
55
+ }
56
+
57
+ async function deleteOwnedTestData(userIds: readonly TableIdentity[], layoutKey: string) {
58
+ await deleteOwnedProfiles(userIds, layoutKey);
59
+ await deleteOwnedUsers(userIds);
60
+ }
61
+
62
+ describe('layoutprofile.test.ts', { concurrency: false }, () => {
63
+ it('rejects anonymous profile operations', async () => {
64
+ await app.bean.executor.mockCtx(async () => {
65
+ for (const [method, path, options] of [
66
+ ['get', `${actionPath}/load`, { query: { layoutKey: 'anonymous' } }],
67
+ [
68
+ 'post',
69
+ `${actionPath}/save`,
70
+ { body: { layoutKey: 'anonymous', profile: createProfile('anonymous') } },
71
+ ],
72
+ ['delete', `${actionPath}/reset`, { query: { layoutKey: 'anonymous' } }],
73
+ ] as const) {
74
+ const [, error] = await catchError(() =>
75
+ (app.bean.executor.performAction as any)(method, path, {
76
+ ...options,
77
+ innerAccess: false,
78
+ }),
79
+ );
80
+ assert.equal(error?.code, 401);
81
+ }
82
+ });
83
+ });
84
+
85
+ it('isolates profiles by authenticated user and supports reset', async () => {
86
+ await app.bean.executor.mockCtx(async () => {
87
+ const suffix = createTestId();
88
+ const layoutKey = `/training/student/${suffix}`;
89
+ let userA: TableIdentity | undefined;
90
+ let userB: TableIdentity | undefined;
91
+ try {
92
+ const customerA = await registerAndSignin(`layout-a-${suffix}`);
93
+ const customerB = await registerAndSignin(`layout-b-${suffix}`);
94
+ userA = customerA.user.id;
95
+ userB = customerB.user.id;
96
+ const profileA = createProfile('a');
97
+ const profileAUpdated = {
98
+ ...createProfile('a-updated'),
99
+ columns: [{ key: 'name', visible: false, width: 320 }],
100
+ } satisfies ILayoutProfile;
101
+ const profileB = createProfile('b');
102
+
103
+ assert.equal(
104
+ await performAs(customerA.token, 'get', `${actionPath}/load`, {
105
+ query: { layoutKey },
106
+ }),
107
+ undefined,
108
+ );
109
+ assert.deepEqual(
110
+ await performAs(customerA.token, 'post', `${actionPath}/save`, {
111
+ body: { layoutKey, profile: profileA },
112
+ }),
113
+ profileA,
114
+ );
115
+ const persistedA = await app.scope('a-layoutprofile').model.layoutprofile.get({
116
+ userId: userA,
117
+ layoutKey,
118
+ });
119
+ assert.equal(String(persistedA?.userId), String(userA));
120
+ assert.deepEqual(persistedA?.profile, profileA);
121
+
122
+ assert.equal(
123
+ await performAs(customerB.token, 'get', `${actionPath}/load`, {
124
+ query: { layoutKey },
125
+ }),
126
+ undefined,
127
+ );
128
+ assert.deepEqual(
129
+ await performAs(customerB.token, 'post', `${actionPath}/save`, {
130
+ body: { layoutKey, profile: profileB },
131
+ }),
132
+ profileB,
133
+ );
134
+ assert.deepEqual(
135
+ await performAs(customerA.token, 'post', `${actionPath}/save`, {
136
+ body: { layoutKey, profile: profileAUpdated },
137
+ }),
138
+ profileAUpdated,
139
+ );
140
+ const profilesA = await app.scope('a-layoutprofile').model.layoutprofile.select({
141
+ where: { userId: userA, layoutKey },
142
+ });
143
+ assert.equal(profilesA.length, 1);
144
+ assert.deepEqual(profilesA[0].profile, profileAUpdated);
145
+
146
+ assert.deepEqual(
147
+ await performAs(customerB.token, 'get', `${actionPath}/load`, {
148
+ query: { layoutKey },
149
+ }),
150
+ profileB,
151
+ );
152
+ assert.equal(
153
+ await performAs(customerB.token, 'delete', `${actionPath}/reset`, {
154
+ query: { layoutKey },
155
+ }),
156
+ null,
157
+ );
158
+ assert.equal(
159
+ await performAs(customerB.token, 'get', `${actionPath}/load`, {
160
+ query: { layoutKey },
161
+ }),
162
+ undefined,
163
+ );
164
+ assert.deepEqual(
165
+ await performAs(customerA.token, 'get', `${actionPath}/load`, {
166
+ query: { layoutKey },
167
+ }),
168
+ profileAUpdated,
169
+ );
170
+ } finally {
171
+ const userIds = [userA, userB].filter((id): id is TableIdentity => id !== undefined);
172
+ await deleteOwnedTestData(userIds, layoutKey);
173
+ }
174
+ });
175
+ });
176
+
177
+ it('serializes competing first saves for one user and layout key', async () => {
178
+ const suffix = createTestId();
179
+ const layoutKey = `/layout-concurrency/${suffix}`;
180
+ const fixture = await app.bean.executor.mockCtx(async () => {
181
+ const customer = await registerAndSignin(`layout-concurrency-${suffix}`);
182
+ return { token: customer.token, userId: customer.user.id };
183
+ });
184
+ const profiles = [createProfile('concurrency-a'), createProfile('concurrency-b')];
185
+ try {
186
+ const results = await Promise.allSettled(
187
+ profiles.map(profile =>
188
+ app.bean.executor.mockCtx(async () => {
189
+ return await performAs(fixture.token, 'post', `${actionPath}/save`, {
190
+ body: { layoutKey, profile },
191
+ });
192
+ }),
193
+ ),
194
+ );
195
+ assert.equal(results.filter(result => result.status === 'fulfilled').length, profiles.length);
196
+
197
+ const persisted = await app.bean.executor.mockCtx(async () => {
198
+ return await app.scope('a-layoutprofile').model.layoutprofile.select({
199
+ where: { userId: fixture.userId, layoutKey },
200
+ });
201
+ });
202
+ assert.equal(persisted.length, 1);
203
+ assert.ok(
204
+ profiles.some(profile => JSON.stringify(profile) === JSON.stringify(persisted[0].profile)),
205
+ );
206
+
207
+ const loaded = await app.bean.executor.mockCtx(async () => {
208
+ return await performAs(fixture.token, 'get', `${actionPath}/load`, {
209
+ query: { layoutKey },
210
+ });
211
+ });
212
+ assert.deepEqual(loaded, persisted[0].profile);
213
+ } finally {
214
+ await app.bean.executor.mockCtx(async () => {
215
+ await deleteOwnedTestData([fixture.userId], layoutKey);
216
+ });
217
+ }
218
+ });
219
+
220
+ it('validates the layout key and profile structure at the API boundary', async () => {
221
+ await app.bean.executor.mockCtx(async () => {
222
+ const suffix = createTestId();
223
+ const customer = await registerAndSignin(`layout-validation-${suffix}`);
224
+ const layoutKey = `/layout-validation/${suffix}`;
225
+ try {
226
+ for (const body of [
227
+ { layoutKey: '', profile: createProfile('invalid-key') },
228
+ { layoutKey, profile: { ...createProfile('version'), version: 2 } },
229
+ {
230
+ layoutKey,
231
+ profile: {
232
+ ...createProfile('width'),
233
+ columns: [{ key: 'name', visible: true, width: 0 }],
234
+ },
235
+ },
236
+ {
237
+ layoutKey,
238
+ profile: {
239
+ ...createProfile('visible'),
240
+ columns: [{ key: 'name', visible: 'yes', width: 'auto' }],
241
+ },
242
+ },
243
+ ]) {
244
+ const [, error] = await catchError(() =>
245
+ performAs(customer.token, 'post', `${actionPath}/save`, { body }),
246
+ );
247
+ assert.equal(
248
+ error?.code,
249
+ 422,
250
+ `unexpected validation result for ${JSON.stringify(body)}: ${JSON.stringify(error)}`,
251
+ );
252
+ }
253
+ } finally {
254
+ await deleteOwnedTestData([customer.user.id], layoutKey);
255
+ }
256
+ });
257
+ });
258
+ });
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../../tsconfig.base",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "declaration": true,
6
+ "declarationMap": true,
7
+ "emitDeclarationOnly": true,
8
+ "outDir": "dist"
9
+ },
10
+ "include": ["src", "src/.metadata/*.ts", "src/**/*.json"]
11
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "../../tsconfig.base",
3
+ "compilerOptions": {
4
+ "noEmit": true
5
+ },
6
+ "include": ["src", "src/.metadata/*.ts", "src/**/*.json", "test"]
7
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-suite-a-cabloy",
3
- "version": "5.1.52",
3
+ "version": "5.1.53",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "author": "",
@@ -8,6 +8,7 @@
8
8
  "type": "module",
9
9
  "dependencies": {
10
10
  "vona-module-a-cabloy": "workspace:^",
11
+ "vona-module-a-layoutprofile": "^5.0.1",
11
12
  "vona-module-a-markdown": "^5.0.3",
12
13
  "vona-module-a-rbac": "^5.0.6",
13
14
  "vona-module-a-socket": "workspace:^",
@@ -28,6 +28,9 @@
28
28
  },
29
29
  {
30
30
  "path": "modules/a-rbac/tsconfig.build.json"
31
+ },
32
+ {
33
+ "path": "modules/a-layoutprofile/tsconfig.build.json"
31
34
  }
32
35
  ],
33
36
  "exclude": ["modules"]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova",
3
- "version": "5.1.161",
3
+ "version": "5.1.162",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "A vue3 framework with ioc",
6
6
  "keywords": [
@@ -46,7 +46,7 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "zova-core": "^5.1.89",
49
- "zova-suite-a-zova": "^5.1.160"
49
+ "zova-suite-a-zova": "^5.1.161"
50
50
  },
51
51
  "devDependencies": {
52
52
  "clean-package": "^2.2.0",
@@ -171,6 +171,9 @@ importers:
171
171
  zova-module-a-interceptor:
172
172
  specifier: workspace:^
173
173
  version: link:src/suite-vendor/a-zova/modules/a-interceptor
174
+ zova-module-a-layoutprofile:
175
+ specifier: workspace:^
176
+ version: link:src/suite-vendor/a-cabloy/modules/a-layoutprofile
174
177
  zova-module-a-logger:
175
178
  specifier: workspace:^
176
179
  version: link:src/suite-vendor/a-zova/modules/a-logger
@@ -870,7 +873,7 @@ importers:
870
873
  specifier: ^5.1.89
871
874
  version: link:../zova-core
872
875
  zova-suite-a-zova:
873
- specifier: ^5.1.158
876
+ specifier: ^5.1.160
874
877
  version: link:../../src/suite-vendor/a-zova
875
878
  devDependencies:
876
879
  clean-package:
@@ -973,13 +976,25 @@ importers:
973
976
 
974
977
  src/suite-vendor/a-cabloy:
975
978
  dependencies:
979
+ zova-module-a-layoutprofile:
980
+ specifier: ^5.0.0
981
+ version: link:modules/a-layoutprofile
976
982
  zova-module-a-rbac:
977
983
  specifier: ^5.0.1
978
984
  version: link:modules/a-rbac
979
985
  zova-module-rest-resource:
980
- specifier: ^5.1.52
986
+ specifier: ^5.1.54
981
987
  version: link:modules/rest-resource
982
988
 
989
+ src/suite-vendor/a-cabloy/modules/a-layoutprofile:
990
+ devDependencies:
991
+ clean-package:
992
+ specifier: ^2.2.0
993
+ version: 2.2.0
994
+ rimraf:
995
+ specifier: ^6.1.3
996
+ version: 6.1.3
997
+
983
998
  src/suite-vendor/a-cabloy/modules/a-rbac:
984
999
  devDependencies:
985
1000
  clean-package:
@@ -1058,7 +1073,7 @@ importers:
1058
1073
  specifier: ^5.1.26
1059
1074
  version: link:modules/a-app
1060
1075
  zova-module-a-bean:
1061
- specifier: ^5.1.33
1076
+ specifier: ^5.1.35
1062
1077
  version: link:modules/a-bean
1063
1078
  zova-module-a-behavior:
1064
1079
  specifier: ^5.1.26
@@ -1097,10 +1112,10 @@ importers:
1097
1112
  specifier: ^5.1.53
1098
1113
  version: link:modules/a-openapi
1099
1114
  zova-module-a-router:
1100
- specifier: ^5.1.35
1115
+ specifier: ^5.1.37
1101
1116
  version: link:modules/a-router
1102
1117
  zova-module-a-routerstack:
1103
- specifier: ^5.1.27
1118
+ specifier: ^5.1.29
1104
1119
  version: link:modules/a-routerstack
1105
1120
  zova-module-a-routertabs:
1106
1121
  specifier: ^5.1.34
@@ -1118,7 +1133,7 @@ importers:
1118
1133
  specifier: ^5.1.35
1119
1134
  version: link:modules/a-style
1120
1135
  zova-module-a-table:
1121
- specifier: ^5.1.40
1136
+ specifier: ^5.1.42
1122
1137
  version: link:modules/a-table
1123
1138
  zova-module-a-zod:
1124
1139
  specifier: ^5.1.37
@@ -37,17 +37,26 @@ export class ServiceSsrLayout extends BeanBase {
37
37
  `<script id="__leftDrawerOpenJS">
38
38
  ${this.options?.sidebarLeftOpenPCCapability ? this._getJsHandlerSidebar() : ''}
39
39
  ${this._getJsHandlerPageContainer()}
40
- window.ssr_body_ready_handler=()=>{
41
- ${this.options?.sidebarLeftOpenPCCapability ? 'window.ssr_body_ready_handler_sidebar();' : ''}
42
- window.ssr_body_ready_handler_pageContainer();
40
+ window.ssr_body_ready_handler=__targets=>{
41
+ ${this.options?.sidebarLeftOpenPCCapability ? 'window.ssr_body_ready_handler_sidebar(__targets);' : ''}
42
+ window.ssr_body_ready_handler_pageContainer(__targets);
43
43
  };
44
44
  window.ssr_body_ready_condition=()=>{
45
+ const __domDrawerContainer=document.querySelector('#q-app>.drawer');
45
46
  const __domPageContainer=document.querySelector('#q-app>.drawer>.drawer-content');
46
- return __domPageContainer;
47
+ ${this.options?.sidebarLeftOpenPCCapability ? "const __domDrawer=document.querySelector('#q-app>.drawer>.drawer-side');" : ''}
48
+ return __domDrawerContainer&&__domPageContainer${this.options?.sidebarLeftOpenPCCapability ? '&&__domDrawer' : ''}?{
49
+ drawerContainer:__domDrawerContainer,
50
+ pageContainer:__domPageContainer,
51
+ ${this.options?.sidebarLeftOpenPCCapability ? 'drawer:__domDrawer,' : ''}
52
+ }:undefined;
47
53
  };
48
- window.ssr_body_ready_callback=()=>{
49
- window.ssr_body_ready_handler();
50
- document.querySelector('#__leftDrawerOpenJS').remove();
54
+ window.ssr_body_ready_callback=__targets=>{
55
+ try{
56
+ window.ssr_body_ready_handler(__targets);
57
+ }finally{
58
+ document.querySelector('#__leftDrawerOpenJS')?.remove();
59
+ }
51
60
  };
52
61
  </script>`.replaceAll('\n', '') + getBodyReadyObserverScript();
53
62
  }
@@ -61,7 +70,7 @@ export class ServiceSsrLayout extends BeanBase {
61
70
  }
62
71
 
63
72
  private _getJsHandlerSidebar() {
64
- return `window.ssr_body_ready_handler_sidebar=()=>{
73
+ return `window.ssr_body_ready_handler_sidebar=__targets=>{
65
74
  const __belowBreakpoint=document.documentElement.clientWidth <= ${this.options?.sidebarBreakpoint};
66
75
  let __leftDrawerOpen;
67
76
  if(__belowBreakpoint){
@@ -70,10 +79,11 @@ export class ServiceSsrLayout extends BeanBase {
70
79
  const __leftDrawerOpenPC=window.ssr_load_local('sidebarLeftOpenPC');
71
80
  __leftDrawerOpen=__leftDrawerOpenPC!==undefined?__leftDrawerOpenPC:${this.options?.sidebarLeftOpenPCFallback ?? false};
72
81
  }
73
- const __domDrawerContainer=document.querySelector('#q-app>.drawer');
74
- const __domDrawer=document.querySelector('#q-app>.drawer>.drawer-side');
82
+ const __domDrawerContainer=__targets?.drawerContainer;
83
+ const __domDrawer=__targets?.drawer;
75
84
  const sidebarWidth = '${this.scope.config.layout.sidebar.width}px';
76
85
  const navbarHeight = '${this.scope.config.layout.navbar.height}px';
86
+ if(!__domDrawerContainer||!__domDrawer)return;
77
87
  if(__leftDrawerOpen){
78
88
  __domDrawer.style.transform='translateX(0px)';
79
89
  __domDrawer.style.width=sidebarWidth;
@@ -1,6 +1,7 @@
1
1
  import type { RowSelectionState, SortingState } from '@tanstack/vue-table';
2
2
  import type { TableIdentity } from 'table-identity';
3
3
  import type { IComponentOptions } from 'zova';
4
+ import type { ModelLayoutProfile } from 'zova-module-a-layoutprofile';
4
5
  import type {
5
6
  IJsxRenderContextPage,
6
7
  IPageScope,
@@ -19,7 +20,7 @@ import { BeanControllerBase, deepEqual } from 'zova';
19
20
  import { ZovaJsx } from 'zova-jsx';
20
21
  import { Controller } from 'zova-module-a-bean';
21
22
  import { $QueriesEnsureLoaded } from 'zova-module-a-model';
22
- import { BeanControllerTableBase } from 'zova-module-a-table';
23
+ import { BeanControllerTableBase, ITableLayout } from 'zova-module-a-table';
23
24
  import { ModelResource } from 'zova-module-rest-resource';
24
25
 
25
26
  import {
@@ -62,6 +63,7 @@ export class ControllerBlockPage<TData extends {} = {}> extends BeanControllerBa
62
63
  tableRef: BeanControllerTableBase<TData> | undefined;
63
64
  private _tableMetaRefreshPending = false;
64
65
  private _tableMetaRefreshTask: Promise<void> | undefined;
66
+ $$modelLayoutProfile: ModelLayoutProfile | undefined;
65
67
 
66
68
  jsxZova: ZovaJsx;
67
69
  jsxCelScope: IPageScope;
@@ -91,6 +93,14 @@ export class ControllerBlockPage<TData extends {} = {}> extends BeanControllerBa
91
93
  true,
92
94
  this.resource,
93
95
  );
96
+ const layoutKey = this.layoutKey;
97
+ if (layoutKey) {
98
+ this.$$modelLayoutProfile = await this.bean._getBeanSelector(
99
+ 'a-layoutprofile.model.layoutProfile',
100
+ true,
101
+ layoutKey,
102
+ );
103
+ }
94
104
  // jsx
95
105
  this._prepareJsx();
96
106
  // query
@@ -120,6 +130,7 @@ export class ControllerBlockPage<TData extends {} = {}> extends BeanControllerBa
120
130
  await $QueriesEnsureLoaded(
121
131
  () => this.$$modelResource.apiSchemasSelect.sdk,
122
132
  () => this.queryData,
133
+ () => this.queryLayoutProfile,
123
134
  );
124
135
  this._refreshSelectedRows((this.data ?? []) as unknown as Record<string, unknown>[]);
125
136
  this._notifySelectionChange();
@@ -144,6 +155,38 @@ export class ControllerBlockPage<TData extends {} = {}> extends BeanControllerBa
144
155
  return this.$props.resource;
145
156
  }
146
157
 
158
+ get queryLayoutProfile() {
159
+ return this.$$modelLayoutProfile?.load();
160
+ }
161
+
162
+ get tableLayout(): ITableLayout | undefined {
163
+ return this.queryLayoutProfile?.data as ITableLayout | undefined;
164
+ }
165
+
166
+ get layoutKey() {
167
+ return this.$pageRoute?.path;
168
+ }
169
+
170
+ get columnConfigSeed() {
171
+ const layout = this.tableRef?.layout;
172
+ const properties = this.tableRef?.layoutProperties;
173
+ if (!layout || !properties || !this.$$modelLayoutProfile || !this.$passport.isAuthenticated)
174
+ return;
175
+ return { layout, properties };
176
+ }
177
+
178
+ async saveColumnConfig(layout: ITableLayout): Promise<void> {
179
+ if (!this.$$modelLayoutProfile) return;
180
+ await this.$$modelLayoutProfile.save().mutateAsync(layout as any);
181
+ await this._requestTableMetaRefresh();
182
+ }
183
+
184
+ async resetColumnConfig(): Promise<void> {
185
+ if (!this.$$modelLayoutProfile) return;
186
+ await this.$$modelLayoutProfile.reset().mutateAsync();
187
+ await this._requestTableMetaRefresh();
188
+ }
189
+
147
190
  private _normalizeSelectedIds(ids: readonly TableIdentity[]) {
148
191
  const selectedIds: TableIdentity[] = [];
149
192
  const keys = new Set<string>();
@@ -50,6 +50,7 @@ export class ControllerBlockTable<TData extends {} = {}> extends BeanControllerB
50
50
  rowSelection={$$page.rowSelection}
51
51
  onRowSelectionChange={updater => $$page.onRowSelectionChange(updater)}
52
52
  tableScope={$$page.jsxCelScope}
53
+ layout={$$page.tableLayout}
53
54
  ></ZTable>
54
55
  </div>
55
56
  );
@@ -9,7 +9,10 @@ import { VNode } from 'vue';
9
9
  import { BeanControllerBase, Use } from 'zova';
10
10
  import { Controller } from 'zova-module-a-bean';
11
11
 
12
- import { resolveTableActionBulkDynamicProps } from '../../lib/selection.js';
12
+ import {
13
+ groupTableActionBulkActions,
14
+ resolveTableActionBulkDynamicProps,
15
+ } from '../../lib/selection.js';
13
16
 
14
17
  declare module 'zova-module-a-openapi' {
15
18
  export interface IResourceBlockRecord {
@@ -65,35 +68,48 @@ export class ControllerBlockToolbarBulk extends BeanControllerBase {
65
68
  const { $$page } = this.$$renderContext;
66
69
  const domActions = this._renderActions();
67
70
  const selectionAvailable = $$page.selectionAvailable;
68
- if ((!domActions || domActions.length === 0) && !selectionAvailable) return;
71
+ if (domActions.start.length === 0 && domActions.end.length === 0 && !selectionAvailable) return;
69
72
  return (
70
73
  <div class={this.$props.class}>
71
- <div class="join" role="toolbar" aria-label={this.scope.locale.BulkActions()}>
72
- {$$page.selectionToggleAvailable && (
73
- <button type="button" class="btn join-item" onClick={() => $$page.toggleSelection()}>
74
- {$$page.selectionVisible ? this.scope.locale.Done() : this.scope.locale.Select()}
75
- </button>
74
+ <div
75
+ class="flex flex-wrap items-center gap-3"
76
+ role="toolbar"
77
+ aria-label={this.scope.locale.BulkActions()}
78
+ >
79
+ {(selectionAvailable || domActions.start.length > 0) && (
80
+ <div class="join">
81
+ {$$page.selectionToggleAvailable && (
82
+ <button
83
+ type="button"
84
+ class="btn join-item"
85
+ onClick={() => $$page.toggleSelection()}
86
+ >
87
+ {$$page.selectionVisible ? this.scope.locale.Done() : this.scope.locale.Select()}
88
+ </button>
89
+ )}
90
+ {$$page.selectionEnabled && (
91
+ <span
92
+ class="join-item border-base-300 bg-base-200 px-3 py-2"
93
+ role="status"
94
+ aria-live="polite"
95
+ >
96
+ {this.scope.locale.SelectedItems($$page.selection.count)}
97
+ </span>
98
+ )}
99
+ {domActions.start}
100
+ </div>
76
101
  )}
77
- {$$page.selectionEnabled && (
78
- <span
79
- class="join-item border-base-300 bg-base-200 px-3 py-2"
80
- role="status"
81
- aria-live="polite"
82
- >
83
- {this.scope.locale.SelectedItems($$page.selection.count)}
84
- </span>
85
- )}
86
- {domActions}
102
+ {domActions.end.length > 0 && <div class="ml-auto join">{domActions.end}</div>}
87
103
  </div>
88
104
  </div>
89
105
  );
90
106
  }
91
107
 
92
- private _renderActions() {
108
+ private _renderActions(): Record<'start' | 'end', VNode[]> {
93
109
  const { $jsx, $celScope } = this.$$renderContext;
94
110
  const actions = this.$props.actions;
95
- if (!actions || actions.length === 0) return;
96
- const domActions: VNode[] = [];
111
+ if (!actions || actions.length === 0) return { start: [], end: [] };
112
+ const items: { item: VNode; placement?: unknown }[] = [];
97
113
  actions.forEach((action, index) => {
98
114
  const actionName = action.name;
99
115
  const permissionHint = action.options?.permission;
@@ -127,12 +143,11 @@ export class ControllerBlockToolbarBulk extends BeanControllerBase {
127
143
  });
128
144
  const domAction = $jsx.render(action.render!, options, $celScope, this.$$renderContext);
129
145
  if (!domAction) return;
130
- if (Array.isArray(domAction)) {
131
- domActions.push(...domAction);
132
- } else {
133
- domActions.push(domAction);
146
+ const renderedActions = Array.isArray(domAction) ? domAction : [domAction];
147
+ for (const item of renderedActions) {
148
+ items.push({ item, placement: action.options?.placement });
134
149
  }
135
150
  });
136
- return domActions;
151
+ return groupTableActionBulkActions(items);
137
152
  }
138
153
  }
@@ -33,6 +33,7 @@ export function resolvePickerSelectionMax(
33
33
  }
34
34
 
35
35
  export type TypeTableActionBulkDynamicDisabledReason = 'maxExceeded' | 'selectionUnavailable';
36
+ export type TypeTableActionBulkPlacement = 'start' | 'end';
36
37
 
37
38
  export interface IResourceTableActionBulkDynamicProps {
38
39
  dynamicSelection: IResourceTableSelectionPayload;
@@ -41,6 +42,16 @@ export interface IResourceTableActionBulkDynamicProps {
41
42
  selectedMaxIds?: number;
42
43
  }
43
44
 
45
+ export function groupTableActionBulkActions<T>(
46
+ items: readonly { item: T; placement?: unknown }[],
47
+ ): Record<TypeTableActionBulkPlacement, T[]> {
48
+ const groups: Record<TypeTableActionBulkPlacement, T[]> = { start: [], end: [] };
49
+ for (const { item, placement } of items) {
50
+ groups[placement === 'end' ? 'end' : 'start'].push(item);
51
+ }
52
+ return groups;
53
+ }
54
+
44
55
  export function resolveTableActionBulkDynamicProps(
45
56
  options: IResourceTableActionBulkOptionsBase | undefined,
46
57
  selection: IResourceTableSelectionPayload,