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.
- package/.cabloy-version +1 -1
- package/CHANGELOG.md +13 -0
- package/README.md +1 -0
- package/package.json +1 -1
- package/repo-agent-governance/managed-assets.json +9 -9
- package/repo-agent-governance/policies/repository.md +1 -1
- package/repo-agent-governance/skills/cabloy-workflow/SKILL.md +4 -2
- package/repo-agent-governance/skills/cabloy-workflow/evals/evals.json +13 -1
- package/repo-docs/backend/unit-testing.md +1 -1
- package/repo-docs/frontend/form-guide.md +17 -1
- package/repo-docs/frontend/model-architecture.md +4 -0
- package/repo-docs/frontend/model-state-guide.md +23 -2
- package/repo-docs/frontend/resource-entry-page-deep-dive.md +119 -0
- package/repo-docs/frontend/scripts.md +3 -3
- package/repo-docs/frontend/zova-form-under-the-hood.md +5 -0
- package/repo-docs/fullstack/parallel-worktree-environment.md +3 -3
- package/repo-docs/fullstack/quickstart.md +11 -1
- package/repo-docs/reference/repo-scripts.md +19 -5
- package/repo-e2e/specs/cabloy-basic.spec.ts +1577 -0
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/src/suite/a-home/modules/home-user/src/controller/passport.ts +1 -0
- package/vona/src/suite/a-home/modules/home-user/src/dto/passport.ts +4 -0
- package/vona/src/suite/a-home/modules/home-user/test/passport.test.ts +172 -0
- package/vona/src/suite/a-home/modules/home-user/test/role.test.ts +1 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
- package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
- package/vona/src/suite-vendor/a-vona/modules/a-openapi/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-openapi/src/types/rest.ts +1 -0
- package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/packages-cli/cli/package.json +2 -2
- package/zova/packages-cli/cli-set-front/cli/templates/rest/render.ts +2 -0
- package/zova/packages-cli/cli-set-front/cli/templates/rest/rest.ts +9 -0
- package/zova/packages-cli/cli-set-front/package.json +1 -1
- package/zova/packages-zova/zova/package.json +2 -2
- package/zova/src/suite/a-home/modules/home-api/src/.metadata/index.ts +17 -17
- package/zova/src/suite/a-home/modules/home-api/src/api/openapi/schemas.ts +68 -52
- package/zova/src/suite/a-home/modules/home-api/src/api/openapi/types.ts +524 -343
- package/zova/src/suite/a-home/modules/home-passport/src/model/passport.ts +45 -11
- package/zova/src/suite/a-home/modules/home-passport/test/lib/persistenceIdentity.test.ts +229 -0
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/package.json +9 -1
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/.metadata/index.ts +81 -5
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/bean/bean.draftCoordinator.ts +158 -0
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/component/blockForm/controller.tsx +11 -3
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/component/blockPageEntry/controller.tsx +438 -8
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/config/config.ts +11 -0
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/config/locale/en-us.ts +9 -0
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/config/locale/zh-cn.ts +9 -0
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/lib/draftSchema.ts +649 -0
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/model/draft.ts +59 -0
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/monkey.ts +11 -0
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/types/draft.ts +49 -0
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftDate.test.ts +262 -0
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftLifecycle.test.ts +1073 -0
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftLoader.mjs +13 -0
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftModel.test.ts +572 -0
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftSchema.test.ts +427 -0
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftSchemaSafety.test.ts +200 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/page/entry/controller.tsx +6 -1
- package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/modules/a-form/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-form/src/component/form/controller.tsx +86 -1
- package/zova/src/suite-vendor/a-zova/modules/a-form/src/component/formField/controller.tsx +88 -13
- package/zova/src/suite-vendor/a-zova/modules/a-form/src/lib/schemaRefresh.ts +751 -0
- package/zova/src/suite-vendor/a-zova/modules/a-form/test/lib/schemaRefresh.test.ts +637 -0
- package/zova/src/suite-vendor/a-zova/modules/a-form/test/lib/schemaRefreshLifecycle.test.ts +994 -0
- package/zova/src/suite-vendor/a-zova/modules/a-model/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.persister.ts +331 -87
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.query.ts +11 -6
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useQuery.ts +3 -1
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useState.ts +17 -1
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/index.ts +1 -0
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/modelPersistence.ts +204 -0
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/persistenceQueue.ts +50 -0
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/monkey.ts +5 -0
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/service/storage.ts +47 -2
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/types/query.ts +3 -0
- package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/modelLoader.mjs +34 -0
- package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/modelPersistence.test.ts +727 -0
- package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/persistenceQueue.test.ts +137 -0
- package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/persistenceTransition.test.ts +129 -0
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/monkey.ts +7 -5
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/rest.ts +2 -0
- package/zova/src/suite-vendor/a-zova/modules/a-router/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-router/src/bean/bean.router.ts +14 -0
- package/zova/src/suite-vendor/a-zova/modules/a-router/src/lib/routeDiscard.ts +26 -0
- package/zova/src/suite-vendor/a-zova/modules/a-router/src/service/routerGuards.ts +6 -3
- package/zova/src/suite-vendor/a-zova/modules/a-router/src/types/index.ts +1 -0
- package/zova/src/suite-vendor/a-zova/modules/a-router/src/types/routeDiscard.ts +24 -0
- package/zova/src/suite-vendor/a-zova/modules/a-router/test/lib/routeDiscard.test.ts +105 -0
- package/zova/src/suite-vendor/a-zova/modules/a-routertabs/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-routertabs/src/model/tabs.ts +202 -30
- package/zova/src/suite-vendor/a-zova/modules/a-routertabs/test/lib/tabs.test.ts +524 -0
- package/zova/src/suite-vendor/a-zova/modules/a-routertabs/test/lib/tabsLoader.mjs +35 -0
- 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
|
-
|
|
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();
|
|
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(
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
+
});
|
|
@@ -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
|
+
}
|
package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/component/blockForm/controller.tsx
CHANGED
|
@@ -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.
|
|
41
|
+
$$pageEntry.formReady(ref);
|
|
42
42
|
}}
|
|
43
43
|
formTag="div"
|
|
44
|
-
data={$$pageEntry.
|
|
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.
|
|
66
|
+
$$pageEntry.formChanged(data);
|
|
60
67
|
}}
|
|
68
|
+
onSchemaRefreshed={data => $$pageEntry.formSchemaRefreshed(data)}
|
|
61
69
|
></ZForm>
|
|
62
70
|
);
|
|
63
71
|
}
|