cabloy 5.1.207 → 5.1.209

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 (97) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +13 -0
  3. package/README.md +1 -0
  4. package/package.json +1 -1
  5. package/repo-agent-governance/managed-assets.json +9 -9
  6. package/repo-agent-governance/policies/repository.md +1 -1
  7. package/repo-agent-governance/skills/cabloy-workflow/SKILL.md +4 -2
  8. package/repo-agent-governance/skills/cabloy-workflow/evals/evals.json +13 -1
  9. package/repo-docs/backend/unit-testing.md +1 -1
  10. package/repo-docs/frontend/form-guide.md +17 -1
  11. package/repo-docs/frontend/model-architecture.md +4 -0
  12. package/repo-docs/frontend/model-state-guide.md +23 -2
  13. package/repo-docs/frontend/resource-entry-page-deep-dive.md +119 -0
  14. package/repo-docs/frontend/scripts.md +3 -3
  15. package/repo-docs/frontend/zova-form-under-the-hood.md +5 -0
  16. package/repo-docs/fullstack/parallel-worktree-environment.md +3 -3
  17. package/repo-docs/fullstack/quickstart.md +11 -1
  18. package/repo-docs/reference/repo-scripts.md +19 -5
  19. package/repo-e2e/specs/cabloy-basic.spec.ts +1577 -0
  20. package/vona/packages-vona/vona/package.json +1 -1
  21. package/vona/src/suite/a-home/modules/home-user/src/controller/passport.ts +1 -0
  22. package/vona/src/suite/a-home/modules/home-user/src/dto/passport.ts +4 -0
  23. package/vona/src/suite/a-home/modules/home-user/test/passport.test.ts +172 -0
  24. package/vona/src/suite/a-home/modules/home-user/test/role.test.ts +1 -0
  25. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
  26. package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
  27. package/vona/src/suite-vendor/a-vona/modules/a-openapi/package.json +1 -1
  28. package/vona/src/suite-vendor/a-vona/modules/a-openapi/src/types/rest.ts +1 -0
  29. package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
  30. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  31. package/zova/packages-cli/cli/package.json +2 -2
  32. package/zova/packages-cli/cli-set-front/cli/templates/rest/render.ts +2 -0
  33. package/zova/packages-cli/cli-set-front/cli/templates/rest/rest.ts +9 -0
  34. package/zova/packages-cli/cli-set-front/package.json +1 -1
  35. package/zova/packages-zova/zova/package.json +2 -2
  36. package/zova/src/suite/a-home/modules/home-api/src/.metadata/index.ts +17 -17
  37. package/zova/src/suite/a-home/modules/home-api/src/api/openapi/schemas.ts +68 -52
  38. package/zova/src/suite/a-home/modules/home-api/src/api/openapi/types.ts +524 -343
  39. package/zova/src/suite/a-home/modules/home-passport/src/model/passport.ts +45 -11
  40. package/zova/src/suite/a-home/modules/home-passport/test/lib/persistenceIdentity.test.ts +229 -0
  41. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/package.json +9 -1
  42. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/.metadata/index.ts +81 -5
  43. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/bean/bean.draftCoordinator.ts +158 -0
  44. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/component/blockForm/controller.tsx +11 -3
  45. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/component/blockPageEntry/controller.tsx +438 -8
  46. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/config/config.ts +11 -0
  47. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/config/locale/en-us.ts +9 -0
  48. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/config/locale/zh-cn.ts +9 -0
  49. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/lib/draftSchema.ts +649 -0
  50. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/model/draft.ts +59 -0
  51. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/monkey.ts +11 -0
  52. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/types/draft.ts +49 -0
  53. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftDate.test.ts +262 -0
  54. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftLifecycle.test.ts +1073 -0
  55. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftLoader.mjs +13 -0
  56. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftModel.test.ts +572 -0
  57. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftSchema.test.ts +427 -0
  58. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftSchemaSafety.test.ts +200 -0
  59. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
  60. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/page/entry/controller.tsx +6 -1
  61. package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
  62. package/zova/src/suite-vendor/a-zova/modules/a-form/package.json +1 -1
  63. package/zova/src/suite-vendor/a-zova/modules/a-form/src/component/form/controller.tsx +86 -1
  64. package/zova/src/suite-vendor/a-zova/modules/a-form/src/component/formField/controller.tsx +88 -13
  65. package/zova/src/suite-vendor/a-zova/modules/a-form/src/lib/schemaRefresh.ts +751 -0
  66. package/zova/src/suite-vendor/a-zova/modules/a-form/test/lib/schemaRefresh.test.ts +637 -0
  67. package/zova/src/suite-vendor/a-zova/modules/a-form/test/lib/schemaRefreshLifecycle.test.ts +994 -0
  68. package/zova/src/suite-vendor/a-zova/modules/a-model/package.json +1 -1
  69. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.persister.ts +331 -87
  70. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.query.ts +11 -6
  71. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useQuery.ts +3 -1
  72. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useState.ts +17 -1
  73. package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/index.ts +1 -0
  74. package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/modelPersistence.ts +204 -0
  75. package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/persistenceQueue.ts +50 -0
  76. package/zova/src/suite-vendor/a-zova/modules/a-model/src/monkey.ts +5 -0
  77. package/zova/src/suite-vendor/a-zova/modules/a-model/src/service/storage.ts +47 -2
  78. package/zova/src/suite-vendor/a-zova/modules/a-model/src/types/query.ts +3 -0
  79. package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/modelLoader.mjs +34 -0
  80. package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/modelPersistence.test.ts +727 -0
  81. package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/persistenceQueue.test.ts +137 -0
  82. package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/persistenceTransition.test.ts +129 -0
  83. package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
  84. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/monkey.ts +7 -5
  85. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/rest.ts +2 -0
  86. package/zova/src/suite-vendor/a-zova/modules/a-router/package.json +1 -1
  87. package/zova/src/suite-vendor/a-zova/modules/a-router/src/bean/bean.router.ts +14 -0
  88. package/zova/src/suite-vendor/a-zova/modules/a-router/src/lib/routeDiscard.ts +26 -0
  89. package/zova/src/suite-vendor/a-zova/modules/a-router/src/service/routerGuards.ts +6 -3
  90. package/zova/src/suite-vendor/a-zova/modules/a-router/src/types/index.ts +1 -0
  91. package/zova/src/suite-vendor/a-zova/modules/a-router/src/types/routeDiscard.ts +24 -0
  92. package/zova/src/suite-vendor/a-zova/modules/a-router/test/lib/routeDiscard.test.ts +105 -0
  93. package/zova/src/suite-vendor/a-zova/modules/a-routertabs/package.json +1 -1
  94. package/zova/src/suite-vendor/a-zova/modules/a-routertabs/src/model/tabs.ts +202 -30
  95. package/zova/src/suite-vendor/a-zova/modules/a-routertabs/test/lib/tabs.test.ts +524 -0
  96. package/zova/src/suite-vendor/a-zova/modules/a-routertabs/test/lib/tabsLoader.mjs +35 -0
  97. package/zova/src/suite-vendor/a-zova/package.json +6 -6
@@ -25,6 +25,8 @@ export class ModelPassport extends BeanModelBase {
25
25
  accessToken?: string;
26
26
  expireTime?: number;
27
27
  schemaLogin?: SchemaObject;
28
+ private _passportRevision = 0;
29
+ private _identityUpdating = false;
28
30
 
29
31
  protected async __init__() {}
30
32
 
@@ -43,6 +45,28 @@ export class ModelPassport extends BeanModelBase {
43
45
  if (process.env.CLIENT) {
44
46
  this._setLocaleTz();
45
47
  }
48
+ this._syncPersistenceIdentity(true);
49
+ this.$watch(
50
+ () => [this.passport?.instanceId, this.passport?.user?.id, this.sys.env.SITE_ID],
51
+ () => {
52
+ if (this._identityUpdating) return;
53
+ ++this._passportRevision;
54
+ this._syncPersistenceIdentity();
55
+ },
56
+ { flush: 'sync' },
57
+ );
58
+ }
59
+
60
+ private _syncPersistenceIdentity(initial = false, options: { refetchActive?: boolean } = {}) {
61
+ const passport = this.passport;
62
+ this.$modelPersistence.update(
63
+ {
64
+ instanceId: passport?.user ? passport.instanceId : null,
65
+ siteId: this.sys.env.SITE_ID,
66
+ userId: passport?.user?.id,
67
+ },
68
+ { initial, ...options },
69
+ );
46
70
  }
47
71
 
48
72
  get apiSchemasLogin() {
@@ -199,17 +223,20 @@ export class ModelPassport extends BeanModelBase {
199
223
 
200
224
  async refreshCurrent() {
201
225
  if (this.$ssr.cookieDisabledOnServer || !this.accessToken) return;
202
- this.passport = await this.$api.homeUserPassport.current();
226
+ const revision = this._passportRevision;
227
+ const passport = await this.$api.homeUserPassport.current();
228
+ if (revision !== this._passportRevision) return this.passport;
229
+ this._setPassport(passport);
203
230
  this._setLocaleTz();
204
231
  return this.passport;
205
232
  }
206
233
 
207
234
  async requireRelogin(returnTo?: string | boolean, cause?: string) {
208
- this._setPassportJwt();
235
+ this._setPassportJwt(undefined, { refetchActive: false });
209
236
  // page: login
210
237
  await this.app.$gotoLogin(returnTo, cause);
211
238
  // clear: should after goto login page, avoid home-layoutadmin use some cache data
212
- this.$clear(); // not await
239
+ await this.$clear();
213
240
  }
214
241
 
215
242
  private _isTempAuthTokenExpired(
@@ -239,17 +266,24 @@ export class ModelPassport extends BeanModelBase {
239
266
  }
240
267
  }
241
268
 
242
- private _setPassportJwt(data?: ApiApiHomeUserPassportloginResponseBody) {
243
- this._setPassport(data?.passport);
244
- this._setJwt(data?.jwt);
269
+ private _setPassportJwt(
270
+ data?: ApiApiHomeUserPassportloginResponseBody,
271
+ options: { refetchActive?: boolean } = {},
272
+ ) {
273
+ this._identityUpdating = true;
274
+ try {
275
+ this._setPassport(data?.passport);
276
+ this._setJwt(data?.jwt);
277
+ } finally {
278
+ this._identityUpdating = false;
279
+ this._syncPersistenceIdentity(false, options);
280
+ }
245
281
  }
246
282
 
247
283
  private _setPassport(passport?: ApiApiHomeUserPassportloginResponseBody['passport']) {
248
- if (passport) {
249
- this.passport = passport;
250
- } else {
251
- this.passport = undefined;
252
- }
284
+ ++this._passportRevision;
285
+ this.passport = passport;
286
+ if (!this._identityUpdating) this._syncPersistenceIdentity();
253
287
  }
254
288
 
255
289
  private _setJwt(jwt?: ApiApiHomeUserPassportloginResponseBody['jwt']) {
@@ -0,0 +1,229 @@
1
+ import assert from 'node:assert/strict';
2
+ import { readFileSync } from 'node:fs';
3
+ import { stripTypeScriptTypes } from 'node:module';
4
+ import test from 'node:test';
5
+ import { effectScope, reactive, watch } from 'vue';
6
+
7
+ async function createRuntime(initialPassport?: any) {
8
+ const updates: any[] = [];
9
+ const calls: string[] = [];
10
+ const scope = effectScope();
11
+ let current: () => Promise<any> = async () => undefined;
12
+ let gotoLogin: () => Promise<void> = async () => undefined;
13
+ class BeanModelBase {
14
+ sys = { env: { SITE_ID: 'admin' } };
15
+ scope = { config: { accessToken: { expireTimeDelay: 0 } } };
16
+ $ssr = { cookieDisabledOnServer: false };
17
+ app = {
18
+ meta: { cookie: { getItem: () => undefined }, locale: {} },
19
+ $gotoReturnTo() {
20
+ calls.push('navigate');
21
+ },
22
+ async $gotoLogin() {
23
+ calls.push('login');
24
+ await gotoLogin();
25
+ },
26
+ };
27
+
28
+ $api = { homeUserPassport: { current: () => current() } };
29
+ $modelPersistence = {
30
+ update: (identity: any, options: any) => {
31
+ updates.push({ identity, options, token: (this as any).accessToken });
32
+ calls.push('scope');
33
+ },
34
+ };
35
+
36
+ $computed() {}
37
+ $useStateLocal(options: any) {
38
+ if (options.queryKey[0] === 'passport') return initialPassport;
39
+ }
40
+
41
+ $useStateMem(options: any) {
42
+ return this.$useStateLocal(options);
43
+ }
44
+
45
+ $useStateCookie() {
46
+ return 'initial-token';
47
+ }
48
+
49
+ $watch(source: any, callback: any, options: any) {
50
+ return scope.run(() => watch(source, callback, options));
51
+ }
52
+
53
+ async $clear() {
54
+ calls.push('clear');
55
+ }
56
+ }
57
+ const source = readFileSync(new URL('../../src/model/passport.ts', import.meta.url), 'utf8')
58
+ .replace(/^import[\s\S]*?;\n/gm, '')
59
+ .replace(/^@Model[^\n]*\n/gm, '');
60
+ const javascript = stripTypeScriptTypes(source).replace(
61
+ 'export class ModelPassport',
62
+ 'class ModelPassport',
63
+ );
64
+ const ModelPassport = new Function('BeanModelBase', `${javascript}\nreturn ModelPassport;`)(
65
+ BeanModelBase,
66
+ );
67
+ const passport = reactive(new ModelPassport());
68
+ await passport.appInitialize();
69
+ return {
70
+ passport,
71
+ updates,
72
+ calls,
73
+ setCurrent(callback: () => Promise<any>) {
74
+ current = callback;
75
+ },
76
+ setGotoLogin(callback: () => Promise<void>) {
77
+ gotoLogin = callback;
78
+ },
79
+ dispose() {
80
+ scope.stop();
81
+ },
82
+ };
83
+ }
84
+
85
+ function identity(id = 1, instanceId: number | null = 2) {
86
+ return { instanceId, user: { id, name: 'User' }, roles: [] };
87
+ }
88
+
89
+ function loginData(id = 1, instanceId: number | null = 2) {
90
+ return {
91
+ passport: identity(id, instanceId),
92
+ jwt: { accessToken: `token-${id}`, expiresIn: 3600, refreshToken: `refresh-${id}` },
93
+ };
94
+ }
95
+
96
+ function deferred() {
97
+ let resolve!: (value: any) => void;
98
+ const promise = new Promise<any>(done => {
99
+ resolve = done;
100
+ });
101
+ return { promise, resolve };
102
+ }
103
+
104
+ test('Passport initialization supplies null identity without introducing a readiness gate', async () => {
105
+ const runtime = await createRuntime();
106
+ try {
107
+ assert.deepEqual(runtime.updates, [
108
+ {
109
+ identity: { instanceId: null, siteId: 'admin', userId: undefined },
110
+ options: { initial: true },
111
+ token: 'initial-token',
112
+ },
113
+ ]);
114
+ } finally {
115
+ runtime.dispose();
116
+ }
117
+ });
118
+
119
+ test('Passport initial identity is adopted and login publishes only after installing its token', async () => {
120
+ const runtime = await createRuntime(identity());
121
+ try {
122
+ assert.equal(runtime.updates[0].options.initial, true);
123
+ assert.deepEqual(runtime.updates[0].identity, { instanceId: 2, siteId: 'admin', userId: 1 });
124
+ runtime.updates.length = 0;
125
+ runtime.calls.length = 0;
126
+ runtime.passport.afterLogin(loginData(3, 4));
127
+ assert.deepEqual(runtime.updates, [
128
+ {
129
+ identity: { instanceId: 4, siteId: 'admin', userId: 3 },
130
+ options: { initial: false },
131
+ token: 'token-3',
132
+ },
133
+ ]);
134
+ assert.deepEqual(runtime.calls, ['scope', 'navigate']);
135
+ } finally {
136
+ runtime.dispose();
137
+ }
138
+ });
139
+
140
+ test('direct Passport and site changes synchronize the same neutral context', async () => {
141
+ const runtime = await createRuntime(identity());
142
+ try {
143
+ runtime.passport.passport = identity(1, 8);
144
+ assert.deepEqual(runtime.updates.at(-1).identity, {
145
+ instanceId: 8,
146
+ siteId: 'admin',
147
+ userId: 1,
148
+ });
149
+ runtime.passport.sys.env.SITE_ID = 'web';
150
+ assert.deepEqual(runtime.updates.at(-1).identity, { instanceId: 8, siteId: 'web', userId: 1 });
151
+ runtime.passport.passport = undefined;
152
+ assert.deepEqual(runtime.updates.at(-1).identity, {
153
+ instanceId: null,
154
+ siteId: 'web',
155
+ userId: undefined,
156
+ });
157
+ } finally {
158
+ runtime.dispose();
159
+ }
160
+ });
161
+
162
+ test('requireRelogin immediately publishes anonymous identity without refetch before navigation and clear', async () => {
163
+ const runtime = await createRuntime(identity());
164
+ const navigation = deferred();
165
+ runtime.setGotoLogin(() => navigation.promise);
166
+ runtime.updates.length = 0;
167
+ runtime.calls.length = 0;
168
+ try {
169
+ const relogin = runtime.passport.requireRelogin();
170
+ assert.equal(runtime.passport.passport, undefined);
171
+ assert.equal(runtime.passport.accessToken, undefined);
172
+ assert.deepEqual(runtime.updates, [
173
+ {
174
+ identity: { instanceId: null, siteId: 'admin', userId: undefined },
175
+ options: { initial: false, refetchActive: false },
176
+ token: undefined,
177
+ },
178
+ ]);
179
+ assert.deepEqual(runtime.calls, ['scope', 'login']);
180
+ await Promise.resolve();
181
+ assert.deepEqual(runtime.calls, ['scope', 'login']);
182
+ navigation.resolve(undefined);
183
+ await relogin;
184
+ assert.deepEqual(runtime.calls, ['scope', 'login', 'clear']);
185
+
186
+ runtime.passport.afterLogin(loginData(3, 4));
187
+ assert.equal(runtime.updates.at(-1)!.token, 'token-3');
188
+ assert.notEqual(runtime.updates.at(-1)!.options.refetchActive, false);
189
+ runtime.passport.passport = identity(3, 8);
190
+ assert.notEqual(runtime.updates.at(-1)!.options.refetchActive, false);
191
+ runtime.passport.sys.env.SITE_ID = 'web';
192
+ assert.notEqual(runtime.updates.at(-1)!.options.refetchActive, false);
193
+ } finally {
194
+ navigation.resolve(undefined);
195
+ runtime.dispose();
196
+ }
197
+ });
198
+
199
+ test('refreshCurrent updates identity but a late response cannot undo login or logout', async () => {
200
+ const runtime = await createRuntime(identity());
201
+ try {
202
+ runtime.setCurrent(async () => identity(1, 7));
203
+ await runtime.passport.refreshCurrent();
204
+ assert.equal(runtime.passport.passport.instanceId, 7);
205
+ assert.equal(runtime.updates.at(-1).identity.instanceId, 7);
206
+
207
+ const pending = deferred();
208
+ runtime.setCurrent(() => pending.promise);
209
+ const refreshing = runtime.passport.refreshCurrent();
210
+ runtime.passport.afterLogin(loginData(3, 4));
211
+ pending.resolve(identity(1, 7));
212
+ await refreshing;
213
+ assert.equal(runtime.passport.user.id, 3);
214
+
215
+ const outgoing = deferred();
216
+ runtime.setCurrent(() => outgoing.promise);
217
+ const outgoingRefresh = runtime.passport.refreshCurrent();
218
+ runtime.calls.length = 0;
219
+ await runtime.passport.requireRelogin();
220
+ assert.deepEqual(runtime.calls, ['scope', 'login', 'clear']);
221
+ assert.equal(runtime.updates.at(-1).identity.instanceId, null);
222
+ assert.equal(runtime.updates.at(-1).token, undefined);
223
+ outgoing.resolve(identity(3, 4));
224
+ await outgoingRefresh;
225
+ assert.equal(runtime.passport.user, undefined);
226
+ } finally {
227
+ runtime.dispose();
228
+ }
229
+ });
@@ -49,5 +49,13 @@
49
49
  "exports.\\..import"
50
50
  ]
51
51
  },
52
- "title": "basic-pageentry"
52
+ "title": "basic-pageentry",
53
+ "zovaModule": {
54
+ "capabilities": {
55
+ "monkey": true
56
+ },
57
+ "dependencies": {
58
+ "a-zova": "5.0.0"
59
+ }
60
+ }
53
61
  }
@@ -1,4 +1,70 @@
1
1
  // eslint-disable
2
+ /** model: begin */
3
+ export * from '../model/draft.js';
4
+ import { IModelOptionsDraft } from '../model/draft.js';
5
+ import 'zova-module-a-model';
6
+ declare module 'zova-module-a-model' {
7
+
8
+ export interface IModelRecord {
9
+ 'basic-pageentry:draft': IModelOptionsDraft;
10
+ }
11
+
12
+
13
+ }
14
+ declare module 'zova-module-basic-pageentry' {
15
+
16
+ export interface ModelDraft {
17
+ /** @internal */
18
+ get scope(): ScopeModuleBasicPageentry;
19
+ }
20
+
21
+ export interface ModelDraft {
22
+ get $beanFullName(): 'basic-pageentry.model.draft';
23
+ get $onionName(): 'basic-pageentry:draft';
24
+ get $onionOptions(): IModelOptionsDraft;
25
+ }
26
+ }
27
+ /** model: end */
28
+ /** model: begin */
29
+ import type { ModelDraft } from '../model/draft.js';
30
+ import 'zova';
31
+ declare module 'zova' {
32
+ export interface IBeanRecordGeneral {
33
+ 'basic-pageentry.model.draft': ModelDraft;
34
+ }
35
+ }
36
+ /** model: end */
37
+ /** bean: begin */
38
+ export * from '../bean/bean.draftCoordinator.js';
39
+
40
+ import 'zova';
41
+ declare module 'zova' {
42
+
43
+
44
+ }
45
+ declare module 'zova-module-basic-pageentry' {
46
+
47
+ export interface BeanDraftCoordinator {
48
+ /** @internal */
49
+ get scope(): ScopeModuleBasicPageentry;
50
+ }
51
+
52
+ export interface BeanDraftCoordinator {
53
+ get $beanFullName(): 'basic-pageentry.bean.draftCoordinator';
54
+ get $onionName(): 'basic-pageentry:draftCoordinator';
55
+
56
+ }
57
+ }
58
+ /** bean: end */
59
+ /** bean: begin */
60
+ import type { BeanDraftCoordinator } from '../bean/bean.draftCoordinator.js';
61
+ import 'zova';
62
+ declare module 'zova' {
63
+ export interface IBeanRecordGeneral {
64
+ 'basic-pageentry.bean.draftCoordinator': BeanDraftCoordinator;
65
+ }
66
+ }
67
+ /** bean: end */
2
68
  /** controller: begin */
3
69
  export * from '../component/blockForm/controller.jsx';
4
70
  export * from '../component/blockPageEntry/controller.jsx';
@@ -28,9 +94,9 @@ declare module 'zova-module-basic-pageentry' {
28
94
  }
29
95
  /** controller: end */
30
96
  /** controller: begin */
31
- import { ControllerBlockForm } from '../component/blockForm/controller.jsx';
32
- import { ControllerBlockPageEntry } from '../component/blockPageEntry/controller.jsx';
33
- import { ControllerBlockToolbarRow } from '../component/blockToolbarRow/controller.jsx';
97
+ import type { ControllerBlockForm } from '../component/blockForm/controller.jsx';
98
+ import type { ControllerBlockPageEntry } from '../component/blockPageEntry/controller.jsx';
99
+ import type { ControllerBlockToolbarRow } from '../component/blockToolbarRow/controller.jsx';
34
100
  import 'zova';
35
101
  declare module 'zova' {
36
102
  export interface IBeanRecordLocal {
@@ -67,11 +133,18 @@ export interface IZovaComponentRecord {
67
133
  }
68
134
  }
69
135
  /** components: end */
136
+ /** config: begin */
137
+ export * from '../config/config.js';
138
+ import { config } from '../config/config.js';
139
+ /** config: end */
70
140
  /** locale: begin */
71
141
  import { locales } from './locales.js';
72
142
  /** locale: end */
143
+ /** monkey: begin */
144
+ export * from '../monkey.js';
145
+ /** monkey: end */
73
146
  /** scope: begin */
74
- import { BeanScopeBase, type BeanScopeUtil, TypeModuleLocales, TypeLocaleBase } from 'zova';
147
+ import { BeanScopeBase, type BeanScopeUtil, TypeModuleConfig, TypeModuleLocales, TypeLocaleBase } from 'zova';
75
148
  import { Scope } from 'zova-module-a-bean';
76
149
 
77
150
  @Scope()
@@ -79,6 +152,7 @@ export class ScopeModuleBasicPageentry extends BeanScopeBase {}
79
152
 
80
153
  export interface ScopeModuleBasicPageentry {
81
154
  util: BeanScopeUtil;
155
+ config: TypeModuleConfig<typeof config>;
82
156
  locale: TypeModuleLocales<(typeof locales)[TypeLocaleBase]>;
83
157
  }
84
158
 
@@ -88,7 +162,9 @@ declare module 'zova' {
88
162
  'basic-pageentry': ScopeModuleBasicPageentry;
89
163
  }
90
164
 
91
-
165
+ export interface IBeanScopeConfig {
166
+ 'basic-pageentry': ReturnType<typeof config>;
167
+ }
92
168
 
93
169
  export interface IBeanScopeLocale {
94
170
  'basic-pageentry': (typeof locales)[TypeLocaleBase];
@@ -0,0 +1,158 @@
1
+ import type { BeanRouter, IRouteDiscardEvent } from 'zova-module-a-router';
2
+
3
+ import { BeanBase } from 'zova';
4
+ import { Bean } from 'zova-module-a-bean';
5
+
6
+ import type { ModelDraft } from '../model/draft.js';
7
+ import type { IDraftEditor, IDraftEnvelope, IDraftIdentity } from '../types/draft.js';
8
+
9
+ interface IDraftSession {
10
+ generation: number;
11
+ barrier: Promise<void>;
12
+ owner?: ModelDraft;
13
+ }
14
+
15
+ @Bean()
16
+ export class BeanDraftCoordinator extends BeanBase {
17
+ private _sessions = new Map<string, IDraftSession>();
18
+ private _identityPaths = new Map<string, IDraftIdentity>();
19
+ private _offDiscard?: () => void;
20
+ private _offContext?: () => void;
21
+
22
+ protected async __init__() {
23
+ if (!process.env.CLIENT) return;
24
+ this._offContext = this.$modelPersistence.subscribe(() => {
25
+ for (const session of this._sessions.values()) ++session.generation;
26
+ this._sessions.clear();
27
+ this._identityPaths.clear();
28
+ });
29
+ this._offDiscard = this.app.meta.event.on('a-router:routeDiscard', (event, next) => {
30
+ this._discardRoutes(event);
31
+ return next();
32
+ });
33
+ }
34
+
35
+ protected __dispose__() {
36
+ this._offDiscard?.();
37
+ this._offContext?.();
38
+ }
39
+
40
+ identity(
41
+ router: BeanRouter,
42
+ fullPath: string,
43
+ resource?: string,
44
+ mode?: 'create' | 'edit',
45
+ id?: string | number,
46
+ ): IDraftIdentity | undefined {
47
+ const user = this.$passport.user;
48
+ if (
49
+ !process.env.CLIENT ||
50
+ !this.$passport.isAuthenticated ||
51
+ !user ||
52
+ user.anonymous ||
53
+ user.id == null
54
+ ) {
55
+ return;
56
+ }
57
+ const route = router.resolve(fullPath);
58
+ if (!resource) {
59
+ if (route.name === 'rest-resource:entryCreate') {
60
+ mode = 'create';
61
+ } else if (route.name === 'rest-resource:entry' && route.params.formScene === 'edit') {
62
+ mode = 'edit';
63
+ id = String(route.params.id);
64
+ } else {
65
+ return;
66
+ }
67
+ resource = String(route.params.resource);
68
+ }
69
+ if (!mode) return;
70
+ return {
71
+ context: this.$modelPersistence.capture(),
72
+ resource,
73
+ mode,
74
+ id: mode === 'edit' ? String(id) : undefined,
75
+ path: route.path,
76
+ };
77
+ }
78
+
79
+ key(identity: IDraftIdentity) {
80
+ return JSON.stringify([identity.resource, identity.mode, identity.id ?? null, identity.path]);
81
+ }
82
+
83
+ private _session(key: string) {
84
+ let session = this._sessions.get(key);
85
+ if (!session) {
86
+ session = { generation: 0, barrier: Promise.resolve() };
87
+ this._sessions.set(key, session);
88
+ }
89
+ return session;
90
+ }
91
+
92
+ async open(identity: IDraftIdentity): Promise<IDraftEditor | undefined> {
93
+ if (!this.isCurrent(identity)) return;
94
+ const key = this.key(identity);
95
+ this._identityPaths.set(identity.path, identity);
96
+ const session = this._session(key);
97
+ const generation = session.generation;
98
+ await session.barrier;
99
+ if (generation !== session.generation || !this.isCurrent(identity)) return;
100
+ const owner = await this.app.bean._getBeanSelector('basic-pageentry.model.draft', true, key);
101
+ session.owner = owner;
102
+ await owner.load();
103
+ if (generation !== session.generation || !this.isCurrent(identity)) return;
104
+ return { key, generation, owner, identity };
105
+ }
106
+
107
+ current(editor: IDraftEditor) {
108
+ return (
109
+ this._session(editor.key).generation === editor.generation && this.isCurrent(editor.identity)
110
+ );
111
+ }
112
+
113
+ async save(editor: IDraftEditor, identity: IDraftIdentity, envelope: IDraftEnvelope) {
114
+ if (editor.key !== this.key(identity) || !this.current(editor) || !this.isCurrent(identity))
115
+ return;
116
+ const session = this._session(editor.key);
117
+ await session.barrier;
118
+ if (!this.current(editor) || !this.isCurrent(identity)) return;
119
+ await editor.owner.save(envelope);
120
+ }
121
+
122
+ remove(identity: IDraftIdentity): Promise<void> {
123
+ if (!this.isCurrent(identity)) return Promise.resolve();
124
+ const key = this.key(identity);
125
+ const session = this._session(key);
126
+ ++session.generation;
127
+ const cleanup = session.barrier.then(async () => {
128
+ if (!this.isCurrent(identity)) return;
129
+ const owner =
130
+ session.owner ??
131
+ (await this.app.bean._getBeanSelector('basic-pageentry.model.draft', true, key));
132
+ if (!this.isCurrent(identity)) return;
133
+ session.owner = owner;
134
+ await owner.remove();
135
+ });
136
+ session.barrier = cleanup.catch(() => {});
137
+ return cleanup;
138
+ }
139
+
140
+ isCurrent(identity: IDraftIdentity) {
141
+ return (
142
+ this.$passport.isAuthenticated &&
143
+ !this.$passport.user?.anonymous &&
144
+ this.$modelPersistence.current(identity.context)
145
+ );
146
+ }
147
+
148
+ private _discardRoutes(event: IRouteDiscardEvent) {
149
+ for (const item of event.items) {
150
+ const path = event.router.resolve(item.fullPath).path;
151
+ const known = this._identityPaths.get(path);
152
+ const identity =
153
+ known && this.isCurrent(known) ? known : this.identity(event.router, item.fullPath);
154
+ if (!identity || !this.isCurrent(identity)) continue;
155
+ event.waitUntil(this.remove(identity));
156
+ }
157
+ }
158
+ }
@@ -38,12 +38,19 @@ export class ControllerBlockForm extends BeanControllerBase {
38
38
  class={this.$props.class}
39
39
  controllerRef={ref => {
40
40
  this.formRef = ref;
41
- $$pageEntry.formRef = ref;
41
+ $$pageEntry.formReady(ref);
42
42
  }}
43
43
  formTag="div"
44
- data={$$pageEntry.formData}
44
+ data={$$pageEntry.editorData}
45
45
  schema={$$pageEntry.formSchema}
46
46
  schemaScene={$$pageEntry.schemaScene}
47
+ localePresentationRefresh={{
48
+ identity: JSON.stringify([
49
+ $$pageEntry.resource,
50
+ $$pageEntry.entryId ?? null,
51
+ $$pageEntry.formScene,
52
+ ]),
53
+ }}
47
54
  formMeta={$$pageEntry.formMeta}
48
55
  formProvider={$$pageEntry.formProvider}
49
56
  blocks={this.$props.blocks}
@@ -56,8 +63,9 @@ export class ControllerBlockForm extends BeanControllerBase {
56
63
  });
57
64
  }}
58
65
  onChanged={data => {
59
- $$pageEntry.setPageMeta(data, true);
66
+ $$pageEntry.formChanged(data);
60
67
  }}
68
+ onSchemaRefreshed={data => $$pageEntry.formSchemaRefreshed(data)}
61
69
  ></ZForm>
62
70
  );
63
71
  }