cabloy 5.1.208 → 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 +6 -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/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
|
@@ -14,7 +14,7 @@ import { celEnvBase, isNil } from '@cabloy/utils';
|
|
|
14
14
|
import { SchemaObject } from 'openapi3-ts/oas31';
|
|
15
15
|
import { TableIdentity } from 'table-identity';
|
|
16
16
|
import { VNode } from 'vue';
|
|
17
|
-
import { BeanControllerBase, deepEqual } from 'zova';
|
|
17
|
+
import { BeanControllerBase, deepEqual, deepExtend } from 'zova';
|
|
18
18
|
import { ZovaJsx } from 'zova-jsx';
|
|
19
19
|
import { Controller } from 'zova-module-a-bean';
|
|
20
20
|
import {
|
|
@@ -23,8 +23,19 @@ import {
|
|
|
23
23
|
TypeFormOnSubmitData,
|
|
24
24
|
} from 'zova-module-a-form';
|
|
25
25
|
import { $QueriesEnsureLoaded } from 'zova-module-a-model';
|
|
26
|
+
import { ZButton } from 'zova-module-basic-button';
|
|
26
27
|
import { ModelResource } from 'zova-module-rest-resource';
|
|
27
28
|
|
|
29
|
+
import type { BeanDraftCoordinator } from '../../bean/bean.draftCoordinator.js';
|
|
30
|
+
import type { IDraftEditor, IDraftEnvelope, IDraftIdentity } from '../../types/draft.js';
|
|
31
|
+
|
|
32
|
+
import {
|
|
33
|
+
createDraftSchema,
|
|
34
|
+
draftBaselineSignature,
|
|
35
|
+
projectDraftValues,
|
|
36
|
+
restoreDraftValues,
|
|
37
|
+
} from '../../lib/draftSchema.js';
|
|
38
|
+
|
|
28
39
|
declare module 'zova-module-a-openapi' {
|
|
29
40
|
export interface IResourceBlockRecord {
|
|
30
41
|
'basic-pageentry:blockPageEntry'?: ControllerBlockPageEntryProps;
|
|
@@ -37,6 +48,7 @@ export interface ControllerBlockPageEntryProps extends IResourceBlockOptionsBase
|
|
|
37
48
|
id?: TableIdentity;
|
|
38
49
|
formScene?: TypeFormScene;
|
|
39
50
|
pageTitleKey?: string;
|
|
51
|
+
draft?: boolean;
|
|
40
52
|
}
|
|
41
53
|
|
|
42
54
|
@Controller()
|
|
@@ -56,6 +68,24 @@ export class ControllerBlockPageEntry<TData extends {} = {}> extends BeanControl
|
|
|
56
68
|
formProvider: IFormProvider;
|
|
57
69
|
formSchema?: SchemaObject;
|
|
58
70
|
formData?: TData;
|
|
71
|
+
editorData?: TData;
|
|
72
|
+
draftCandidate?: IDraftEnvelope;
|
|
73
|
+
draftActive = false;
|
|
74
|
+
draftError?: 'storage' | 'incompatible' | 'recover';
|
|
75
|
+
private _baseline?: TData;
|
|
76
|
+
private _dirty = false;
|
|
77
|
+
private _draftStarted = false;
|
|
78
|
+
private _draftDisposed = false;
|
|
79
|
+
private _draftSession = 0;
|
|
80
|
+
private _draftInputRevision = 0;
|
|
81
|
+
private _draftSaveRevision = 0;
|
|
82
|
+
private _draftRecoveryRevision = 0;
|
|
83
|
+
private _draftRemovalPending = false;
|
|
84
|
+
private _draftDiscardState?: { candidate: boolean; inputRevision: number };
|
|
85
|
+
private _draftCoordinator?: BeanDraftCoordinator;
|
|
86
|
+
private _draftIdentity?: IDraftIdentity;
|
|
87
|
+
private _draftEditor?: IDraftEditor;
|
|
88
|
+
private _offDraftDiscard?: () => void;
|
|
59
89
|
|
|
60
90
|
jsxZova: ZovaJsx;
|
|
61
91
|
jsxCelScope: IPageEntryScope;
|
|
@@ -89,14 +119,331 @@ export class ControllerBlockPageEntry<TData extends {} = {}> extends BeanControl
|
|
|
89
119
|
() => this.$$modelResource.getFormApiSchemas(this.formMeta)?.sdk,
|
|
90
120
|
() => this.queryData,
|
|
91
121
|
);
|
|
92
|
-
this.
|
|
122
|
+
this._setEditorBaseline(this.formData);
|
|
93
123
|
this.$watch(
|
|
94
124
|
() => this.formData,
|
|
95
125
|
(newValue, oldValue) => {
|
|
96
|
-
if (
|
|
97
|
-
|
|
126
|
+
if (
|
|
127
|
+
deepEqual(newValue, oldValue) ||
|
|
128
|
+
this._dirty ||
|
|
129
|
+
(!newValue && this.formMeta.editMode === 'create')
|
|
130
|
+
) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
this._setEditorBaseline(newValue);
|
|
98
134
|
},
|
|
99
135
|
);
|
|
136
|
+
this.$ssr.handleDirectOrOnHydrated(() => {
|
|
137
|
+
// Client navigation can finish loading the edit schema after the form mounts.
|
|
138
|
+
this.$watch(
|
|
139
|
+
() => this._draftEnabled && !!this.$pageRoute,
|
|
140
|
+
ready => {
|
|
141
|
+
if (ready) void this._startDraft();
|
|
142
|
+
},
|
|
143
|
+
{ immediate: true },
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
protected __dispose__() {
|
|
149
|
+
this._draftDisposed = true;
|
|
150
|
+
++this._draftSession;
|
|
151
|
+
this._offDraftDiscard?.();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private _setEditorBaseline(data: TData | undefined) {
|
|
155
|
+
this._baseline = data ? deepExtend({}, data) : undefined;
|
|
156
|
+
this.editorData = this._baseline;
|
|
157
|
+
this.setPageMeta(data, false);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
formReady(form: BeanControllerFormBase) {
|
|
161
|
+
this.formRef = form;
|
|
162
|
+
this.$ssr.handleDirectOrOnHydrated(() => {
|
|
163
|
+
void this._startDraft();
|
|
164
|
+
if (form && this._draftEditor && this._dirty && !this.draftCandidate) {
|
|
165
|
+
void this._saveDraft(form.formState.values as TData);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
formChanged(data: TData) {
|
|
171
|
+
++this._draftInputRevision;
|
|
172
|
+
++this._draftRecoveryRevision;
|
|
173
|
+
this._dirty = true;
|
|
174
|
+
this.draftCandidate = undefined;
|
|
175
|
+
if (this.draftError !== 'storage') this.draftError = undefined;
|
|
176
|
+
this.setPageMeta(data, true);
|
|
177
|
+
void this._saveDraft(data);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
formSchemaRefreshed(data: TData) {
|
|
181
|
+
if (!this._dirty) return;
|
|
182
|
+
this.setPageMeta(data, true);
|
|
183
|
+
if (!this.draftCandidate) void this._saveDraft(data);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private _draftCurrent(session: number, editor: IDraftEditor) {
|
|
187
|
+
return (
|
|
188
|
+
!this._draftDisposed &&
|
|
189
|
+
session === this._draftSession &&
|
|
190
|
+
editor === this._draftEditor &&
|
|
191
|
+
!!this._draftCoordinator?.current(editor)
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async retryDraftStorage() {
|
|
196
|
+
if (this._draftRemovalPending) {
|
|
197
|
+
if (this._draftDiscardState) {
|
|
198
|
+
await this.discardDraft();
|
|
199
|
+
} else if (await this._removeDraft()) {
|
|
200
|
+
if (this._dirty && this.formRef) {
|
|
201
|
+
await this._saveDraft(this.formRef.formState.values as TData);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
} else if (!this._draftEditor) {
|
|
205
|
+
this._draftStarted = false;
|
|
206
|
+
await this._startDraft();
|
|
207
|
+
} else if (this._dirty && !this.draftCandidate && this.formRef) {
|
|
208
|
+
await this._saveDraft(this.formRef.formState.values as TData);
|
|
209
|
+
} else {
|
|
210
|
+
// A failed initial read needs a fresh load, not an empty replacement.
|
|
211
|
+
this._draftEditor = undefined;
|
|
212
|
+
this._draftStarted = false;
|
|
213
|
+
await this._startDraft();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private _getDraftSchema() {
|
|
218
|
+
if (!this.formSchema) return;
|
|
219
|
+
return createDraftSchema(this.formSchema, {
|
|
220
|
+
scene: this.schemaScene,
|
|
221
|
+
loadProperties: (schema, scene) => this.$sdk.loadSchemaProperties(schema, scene),
|
|
222
|
+
resolveSchema: reference => this.$sdk.getSchema(reference).data,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private get _draftEnabled() {
|
|
227
|
+
const config = this.scope.config.draft;
|
|
228
|
+
return (
|
|
229
|
+
this.formMeta.formMode === 'edit' &&
|
|
230
|
+
config.enabled &&
|
|
231
|
+
this.$props.draft !== false &&
|
|
232
|
+
config.resources[this.resource!] !== false &&
|
|
233
|
+
this._getDraftSchema()?.enabled
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private async _startDraft() {
|
|
238
|
+
if (
|
|
239
|
+
!process.env.CLIENT ||
|
|
240
|
+
this._draftDisposed ||
|
|
241
|
+
this._draftStarted ||
|
|
242
|
+
!this._draftEnabled ||
|
|
243
|
+
!this.$pageRoute
|
|
244
|
+
) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
this._draftStarted = true;
|
|
248
|
+
const session = ++this._draftSession;
|
|
249
|
+
try {
|
|
250
|
+
const coordinator = await this.app.bean._getBean(
|
|
251
|
+
'basic-pageentry.bean.draftCoordinator',
|
|
252
|
+
true,
|
|
253
|
+
);
|
|
254
|
+
const identity = coordinator.identity(
|
|
255
|
+
this.$router,
|
|
256
|
+
this.$pageRoute.fullPath,
|
|
257
|
+
this.resource,
|
|
258
|
+
this.formMeta.editMode === 'create' ? 'create' : 'edit',
|
|
259
|
+
this.entryId,
|
|
260
|
+
);
|
|
261
|
+
if (!identity || this._draftDisposed || session !== this._draftSession) return;
|
|
262
|
+
this._draftCoordinator = coordinator;
|
|
263
|
+
this._draftIdentity = identity;
|
|
264
|
+
const editor = await coordinator.open(identity);
|
|
265
|
+
if (!editor || this._draftDisposed || session !== this._draftSession) return;
|
|
266
|
+
this._draftEditor = editor;
|
|
267
|
+
this._offDraftDiscard?.();
|
|
268
|
+
this._offDraftDiscard = this.app.meta.event.on('a-router:routeDiscard', (event, next) => {
|
|
269
|
+
if (event.items.some(item => event.router.resolve(item.fullPath).path === identity.path)) {
|
|
270
|
+
++this._draftSession;
|
|
271
|
+
this._draftEditor = undefined;
|
|
272
|
+
this.draftCandidate = undefined;
|
|
273
|
+
this.draftActive = false;
|
|
274
|
+
this.draftError = undefined;
|
|
275
|
+
}
|
|
276
|
+
return next();
|
|
277
|
+
});
|
|
278
|
+
this.draftError = editor.owner.failed ? 'storage' : undefined;
|
|
279
|
+
// Input that arrived during discovery owns the draft, not the late snapshot.
|
|
280
|
+
if (this._draftInputRevision > 0) {
|
|
281
|
+
if (this.formRef) await this._saveDraft(this.formRef.formState.values as TData);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const snapshot = editor.owner.snapshot;
|
|
285
|
+
if (snapshot) {
|
|
286
|
+
this.draftCandidate = snapshot;
|
|
287
|
+
if (
|
|
288
|
+
snapshot.version !== 1 ||
|
|
289
|
+
!Number.isFinite(snapshot.savedAt) ||
|
|
290
|
+
snapshot.savedAt + this.scope.config.draft.maxAge <= Date.now()
|
|
291
|
+
) {
|
|
292
|
+
if (await this._removeDraft()) {
|
|
293
|
+
if (this._dirty && this.formRef) {
|
|
294
|
+
await this._saveDraft(this.formRef.formState.values as TData);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
} else if (
|
|
298
|
+
!editor.owner.failed &&
|
|
299
|
+
snapshot.schemaSignature !== this._getDraftSchema()?.signature
|
|
300
|
+
) {
|
|
301
|
+
this.draftError = 'incompatible';
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
} catch {
|
|
305
|
+
if (!this._draftDisposed && session === this._draftSession) {
|
|
306
|
+
this.draftError = 'storage';
|
|
307
|
+
this._draftStarted = false;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private async _saveDraft(data: TData) {
|
|
313
|
+
const revision = ++this._draftSaveRevision;
|
|
314
|
+
const session = this._draftSession;
|
|
315
|
+
const editor = this._draftEditor;
|
|
316
|
+
const identity = this._draftIdentity;
|
|
317
|
+
const coordinator = this._draftCoordinator;
|
|
318
|
+
const schema = this._getDraftSchema();
|
|
319
|
+
if (
|
|
320
|
+
!editor ||
|
|
321
|
+
!identity ||
|
|
322
|
+
!coordinator ||
|
|
323
|
+
!schema?.enabled ||
|
|
324
|
+
!this._draftEnabled ||
|
|
325
|
+
!this._draftCurrent(session, editor)
|
|
326
|
+
) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
const envelope: IDraftEnvelope = {
|
|
331
|
+
version: 1,
|
|
332
|
+
savedAt: Date.now(),
|
|
333
|
+
schemaSignature: schema.signature,
|
|
334
|
+
baselineSignature:
|
|
335
|
+
identity.mode === 'edit' ? draftBaselineSignature(schema, this._baseline) : undefined,
|
|
336
|
+
projection: projectDraftValues(schema, data, this._baseline),
|
|
337
|
+
};
|
|
338
|
+
await coordinator.save(editor, identity, envelope);
|
|
339
|
+
if (!this._draftCurrent(session, editor) || revision !== this._draftSaveRevision) return;
|
|
340
|
+
this.draftError = editor.owner.failed ? 'storage' : undefined;
|
|
341
|
+
this.draftActive = !editor.owner.failed;
|
|
342
|
+
} catch {
|
|
343
|
+
if (this._draftCurrent(session, editor) && revision === this._draftSaveRevision) {
|
|
344
|
+
this.draftError = 'storage';
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async recoverDraft() {
|
|
350
|
+
const candidate = this.draftCandidate;
|
|
351
|
+
const editor = this._draftEditor;
|
|
352
|
+
const session = this._draftSession;
|
|
353
|
+
const inputRevision = this._draftInputRevision;
|
|
354
|
+
const recoveryRevision = ++this._draftRecoveryRevision;
|
|
355
|
+
if (!candidate || !editor || !this.formRef || !this._draftEnabled) return;
|
|
356
|
+
const current = () =>
|
|
357
|
+
this._draftCurrent(session, editor) &&
|
|
358
|
+
this.draftCandidate === candidate &&
|
|
359
|
+
inputRevision === this._draftInputRevision &&
|
|
360
|
+
recoveryRevision === this._draftRecoveryRevision;
|
|
361
|
+
if (!current()) return;
|
|
362
|
+
try {
|
|
363
|
+
if (
|
|
364
|
+
this._dirty &&
|
|
365
|
+
!(await this.$performCommand('basic-commands:confirm', {
|
|
366
|
+
text: this.scope.locale.DraftReplaceConfirm(),
|
|
367
|
+
}))
|
|
368
|
+
) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (!current()) return;
|
|
372
|
+
let baseline = this._baseline;
|
|
373
|
+
if (this._draftIdentity?.mode === 'edit') {
|
|
374
|
+
const result = await this.queryData!.refetch({ throwOnError: true });
|
|
375
|
+
baseline = result.data as TData | undefined;
|
|
376
|
+
}
|
|
377
|
+
if (!current()) return;
|
|
378
|
+
const schema = this._getDraftSchema();
|
|
379
|
+
if (
|
|
380
|
+
!baseline ||
|
|
381
|
+
!schema?.enabled ||
|
|
382
|
+
candidate.schemaSignature !== schema.signature ||
|
|
383
|
+
candidate.savedAt + this.scope.config.draft.maxAge <= Date.now() ||
|
|
384
|
+
(this._draftIdentity?.mode === 'edit' &&
|
|
385
|
+
candidate.baselineSignature !== draftBaselineSignature(schema, baseline))
|
|
386
|
+
) {
|
|
387
|
+
this.draftError = 'incompatible';
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const values = restoreDraftValues(schema, candidate.projection, baseline);
|
|
391
|
+
if (!values) {
|
|
392
|
+
this.draftError = 'incompatible';
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
this._baseline = deepExtend({}, baseline);
|
|
396
|
+
this.formRef.reset(values, { keepDefaultValues: true });
|
|
397
|
+
this.draftCandidate = undefined;
|
|
398
|
+
this.draftError = undefined;
|
|
399
|
+
this.draftActive = true;
|
|
400
|
+
this._dirty = true;
|
|
401
|
+
this.setPageMeta(values, true);
|
|
402
|
+
} catch (error: any) {
|
|
403
|
+
if (!current()) return;
|
|
404
|
+
this.draftError = [403, 404].includes(error?.code ?? error?.response?.status)
|
|
405
|
+
? 'incompatible'
|
|
406
|
+
: 'recover';
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async discardDraft() {
|
|
411
|
+
const state = this._draftDiscardState ?? {
|
|
412
|
+
candidate: !!this.draftCandidate,
|
|
413
|
+
inputRevision: this._draftInputRevision,
|
|
414
|
+
};
|
|
415
|
+
this._draftDiscardState = state;
|
|
416
|
+
if (!(await this._removeDraft())) return;
|
|
417
|
+
this._draftDiscardState = undefined;
|
|
418
|
+
if (!state.candidate && state.inputRevision === this._draftInputRevision && this._baseline) {
|
|
419
|
+
this.formRef?.reset(this._baseline);
|
|
420
|
+
this._dirty = false;
|
|
421
|
+
this.setPageMeta(this._baseline, false);
|
|
422
|
+
} else if (this._dirty && this.formRef) {
|
|
423
|
+
await this._saveDraft(this.formRef.formState.values as TData);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private async _removeDraft() {
|
|
428
|
+
if (!this._draftIdentity || !this._draftCoordinator) return true;
|
|
429
|
+
const session = ++this._draftSession;
|
|
430
|
+
this._draftRemovalPending = true;
|
|
431
|
+
this._draftEditor = undefined;
|
|
432
|
+
try {
|
|
433
|
+
await this._draftCoordinator.remove(this._draftIdentity);
|
|
434
|
+
if (this._draftDisposed || session !== this._draftSession) return false;
|
|
435
|
+
const editor = await this._draftCoordinator.open(this._draftIdentity);
|
|
436
|
+
if (!editor || this._draftDisposed || session !== this._draftSession) return false;
|
|
437
|
+
this._draftEditor = editor;
|
|
438
|
+
this._draftRemovalPending = false;
|
|
439
|
+
this.draftCandidate = undefined;
|
|
440
|
+
this.draftActive = false;
|
|
441
|
+
this.draftError = undefined;
|
|
442
|
+
return true;
|
|
443
|
+
} catch {
|
|
444
|
+
if (!this._draftDisposed && session === this._draftSession) this.draftError = 'storage';
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
100
447
|
}
|
|
101
448
|
|
|
102
449
|
get resource() {
|
|
@@ -165,7 +512,10 @@ export class ControllerBlockPageEntry<TData extends {} = {}> extends BeanControl
|
|
|
165
512
|
|
|
166
513
|
async submitData(data: TypeFormOnSubmitData<TData>) {
|
|
167
514
|
const mutationSubmit = this.$$modelResource.getFormMutationSubmit(this.formMeta, this.entryId);
|
|
168
|
-
|
|
515
|
+
if (!mutationSubmit) throw new Error('Resource form has no submit mutation');
|
|
516
|
+
this.entryIdCreated = (await mutationSubmit.mutateAsync(data.value as any)) as TableIdentity;
|
|
517
|
+
this._dirty = false;
|
|
518
|
+
await this._removeDraft();
|
|
169
519
|
this.setPageMeta(data.value, false);
|
|
170
520
|
}
|
|
171
521
|
|
|
@@ -176,10 +526,90 @@ export class ControllerBlockPageEntry<TData extends {} = {}> extends BeanControl
|
|
|
176
526
|
}
|
|
177
527
|
|
|
178
528
|
protected render() {
|
|
179
|
-
|
|
180
|
-
|
|
529
|
+
const data = this.formMeta.editMode === 'create' ? this.editorData : this.formData;
|
|
530
|
+
if (!data) {
|
|
531
|
+
return (
|
|
532
|
+
<div>
|
|
533
|
+
{this._renderDraft()}
|
|
534
|
+
{this._renderDraftStorage()}
|
|
535
|
+
<div>{this.scope.locale.EntryNotExist()}</div>
|
|
536
|
+
</div>
|
|
537
|
+
);
|
|
181
538
|
}
|
|
182
|
-
return
|
|
539
|
+
return (
|
|
540
|
+
<div class={this.$props.class}>
|
|
541
|
+
{this._renderDraft()}
|
|
542
|
+
{this._renderDraftStorage()}
|
|
543
|
+
{this._renderFormWrapper()}
|
|
544
|
+
</div>
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
private get _draftVisible() {
|
|
549
|
+
return (
|
|
550
|
+
process.env.CLIENT &&
|
|
551
|
+
!this._draftDisposed &&
|
|
552
|
+
!this.$ssr.isRuntimeSsrPreHydration &&
|
|
553
|
+
this._draftEnabled &&
|
|
554
|
+
this.$passport.isAuthenticated &&
|
|
555
|
+
!!this._draftIdentity &&
|
|
556
|
+
this._draftCoordinator?.isCurrent(this._draftIdentity)
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
private _renderDraft() {
|
|
561
|
+
if (!this._draftVisible || !this.draftCandidate) return;
|
|
562
|
+
const locale = this.scope.locale;
|
|
563
|
+
const error =
|
|
564
|
+
this.draftError === 'incompatible'
|
|
565
|
+
? locale.DraftIncompatible()
|
|
566
|
+
: this.draftError === 'recover'
|
|
567
|
+
? locale.DraftRecoverFailed()
|
|
568
|
+
: undefined;
|
|
569
|
+
return (
|
|
570
|
+
<div class="alert mb-3" role="status" data-testid="resource-draft">
|
|
571
|
+
<div>
|
|
572
|
+
<div>{error ?? locale.DraftAvailable()}</div>
|
|
573
|
+
{Number.isFinite(this.draftCandidate.savedAt) && (
|
|
574
|
+
<time datetime={new Date(this.draftCandidate.savedAt).toISOString()}>
|
|
575
|
+
{new Date(this.draftCandidate.savedAt).toLocaleString(this.app.meta.locale.current)}
|
|
576
|
+
</time>
|
|
577
|
+
)}
|
|
578
|
+
</div>
|
|
579
|
+
{this.formData && this.formRef && (
|
|
580
|
+
<ZButton
|
|
581
|
+
type="button"
|
|
582
|
+
class="btn btn-sm btn-primary"
|
|
583
|
+
onPerform={() => this.recoverDraft()}
|
|
584
|
+
>
|
|
585
|
+
{locale.DraftRecover()}
|
|
586
|
+
</ZButton>
|
|
587
|
+
)}
|
|
588
|
+
<ZButton type="button" class="btn btn-sm" onPerform={() => this.discardDraft()}>
|
|
589
|
+
{locale.DraftDiscard()}
|
|
590
|
+
</ZButton>
|
|
591
|
+
</div>
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
private _renderDraftStorage() {
|
|
596
|
+
if (!this._draftVisible || this.draftError !== 'storage') return;
|
|
597
|
+
return (
|
|
598
|
+
<div
|
|
599
|
+
class="mb-3 flex items-center gap-2 text-sm"
|
|
600
|
+
role="status"
|
|
601
|
+
data-testid="resource-draft-storage"
|
|
602
|
+
>
|
|
603
|
+
<span>{this.scope.locale.DraftStorageFailed()}</span>
|
|
604
|
+
<ZButton
|
|
605
|
+
type="button"
|
|
606
|
+
class="btn btn-ghost btn-xs"
|
|
607
|
+
onPerform={() => this.retryDraftStorage()}
|
|
608
|
+
>
|
|
609
|
+
{this.scope.locale.DraftRetry()}
|
|
610
|
+
</ZButton>
|
|
611
|
+
</div>
|
|
612
|
+
);
|
|
183
613
|
}
|
|
184
614
|
|
|
185
615
|
private _renderFormWrapper() {
|
|
@@ -1,3 +1,12 @@
|
|
|
1
1
|
export default {
|
|
2
2
|
EntryNotExist: 'The entry does not exist',
|
|
3
|
+
DraftAvailable: 'Unsaved draft available',
|
|
4
|
+
DraftSaved: 'Draft saved locally',
|
|
5
|
+
DraftRecover: 'Recover draft',
|
|
6
|
+
DraftDiscard: 'Discard draft',
|
|
7
|
+
DraftRetry: 'Retry draft storage',
|
|
8
|
+
DraftStorageFailed: 'Draft storage is unavailable. Your form can still be submitted normally.',
|
|
9
|
+
DraftIncompatible: 'This draft cannot be recovered because the form or record has changed.',
|
|
10
|
+
DraftRecoverFailed: 'The record could not be checked. Try recovery again.',
|
|
11
|
+
DraftReplaceConfirm: 'Recovering this draft will replace your current unsaved input. Continue?',
|
|
3
12
|
};
|
|
@@ -1,3 +1,12 @@
|
|
|
1
1
|
export default {
|
|
2
2
|
EntryNotExist: '该条目不存在',
|
|
3
|
+
DraftAvailable: '发现未提交的草稿',
|
|
4
|
+
DraftSaved: '草稿已保存到本地',
|
|
5
|
+
DraftRecover: '恢复草稿',
|
|
6
|
+
DraftDiscard: '丢弃草稿',
|
|
7
|
+
DraftRetry: '重试草稿存储',
|
|
8
|
+
DraftStorageFailed: '草稿存储不可用,仍可正常编辑和提交表单。',
|
|
9
|
+
DraftIncompatible: '表单或记录已变化,无法恢复此草稿。',
|
|
10
|
+
DraftRecoverFailed: '暂时无法检查记录,请重试恢复。',
|
|
11
|
+
DraftReplaceConfirm: '恢复草稿将替换当前未提交的输入,是否继续?',
|
|
3
12
|
};
|