@blueprint-ts/core 4.1.0-beta.4 → 4.1.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/vue/forms/BaseForm.d.ts +4 -0
- package/dist/vue/forms/BaseForm.js +40 -43
- package/dist/vue/forms/index.d.ts +4 -2
- package/dist/vue/forms/index.js +2 -1
- package/dist/vue/forms/persistence/StrictPersistenceRestorePolicy.d.ts +4 -0
- package/dist/vue/forms/persistence/StrictPersistenceRestorePolicy.js +42 -0
- package/dist/vue/forms/persistence/index.d.ts +3 -0
- package/dist/vue/forms/persistence/index.js +2 -0
- package/dist/vue/forms/persistence/types.d.ts +23 -0
- package/dist/vue/forms/persistence/types.js +1 -0
- package/dist/vue/forms/persistence/utils.d.ts +2 -0
- package/dist/vue/forms/persistence/utils.js +77 -0
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type PersistenceDriver } from '../../persistenceDrivers/types/PersistenceDriver';
|
|
2
2
|
import { PropertyAwareArray, type PropertyAwareField } from './PropertyAwareArray';
|
|
3
3
|
import { PropertyAwareObject } from './PropertyAwareObject';
|
|
4
|
+
import { type PersistenceDebugEvent, type PersistenceRestorePolicy } from './persistence/types';
|
|
4
5
|
import { type ValidationGroups, type ValidationRules } from './validation';
|
|
5
6
|
type ErrorMessages = string[];
|
|
6
7
|
interface ErrorObject {
|
|
@@ -82,6 +83,9 @@ export declare abstract class BaseForm<RequestBody extends object, FormBody exte
|
|
|
82
83
|
* Child classes can override this method to return a different driver.
|
|
83
84
|
*/
|
|
84
85
|
protected getPersistenceDriver(_suffix: string | undefined): PersistenceDriver;
|
|
86
|
+
protected getPersistenceRestorePolicy(): PersistenceRestorePolicy<FormBody>;
|
|
87
|
+
protected shouldLogPersistenceDebug(): boolean;
|
|
88
|
+
protected logPersistenceDebug(event: PersistenceDebugEvent<FormBody>): void;
|
|
85
89
|
/**
|
|
86
90
|
* Helper: recursively computes the dirty state for a value based on the original.
|
|
87
91
|
* For plain arrays we compare the entire array (a single flag), not each element.
|
|
@@ -12,6 +12,7 @@ import { camelCase, upperFirst, cloneDeep, debounce, isEqual } from 'lodash-es';
|
|
|
12
12
|
import { NonPersistentDriver } from '../../persistenceDrivers/NonPersistentDriver';
|
|
13
13
|
import { PropertyAwareArray } from './PropertyAwareArray';
|
|
14
14
|
import { PropertyAwareObject, PROPERTY_AWARE_OBJECT_MARKER } from './PropertyAwareObject';
|
|
15
|
+
import { StrictPersistenceRestorePolicy } from './persistence/StrictPersistenceRestorePolicy';
|
|
15
16
|
import { BaseRule } from './validation/rules/BaseRule';
|
|
16
17
|
import { ValidationMode } from './validation';
|
|
17
18
|
function isRecord(value) {
|
|
@@ -140,39 +141,6 @@ function restorePropertyAwareStructure(defaults, value) {
|
|
|
140
141
|
}
|
|
141
142
|
return restoreSerializedPropertyAwareValue(value);
|
|
142
143
|
}
|
|
143
|
-
function normalizePropertyAwareEqualityValue(value) {
|
|
144
|
-
if (value instanceof PropertyAwareArray) {
|
|
145
|
-
return Array.from(value, (item) => normalizePropertyAwareEqualityValue(item));
|
|
146
|
-
}
|
|
147
|
-
if (isPropertyAwareObject(value) || isSerializedPropertyAwareObject(value)) {
|
|
148
|
-
const normalized = {};
|
|
149
|
-
for (const [key, child] of Object.entries(value)) {
|
|
150
|
-
if (key === PROPERTY_AWARE_OBJECT_MARKER) {
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
normalized[key] = normalizePropertyAwareEqualityValue(child);
|
|
154
|
-
}
|
|
155
|
-
return normalized;
|
|
156
|
-
}
|
|
157
|
-
if (Array.isArray(value)) {
|
|
158
|
-
return value.map((item) => normalizePropertyAwareEqualityValue(item));
|
|
159
|
-
}
|
|
160
|
-
if (isRecord(value)) {
|
|
161
|
-
const normalized = {};
|
|
162
|
-
for (const [key, child] of Object.entries(value)) {
|
|
163
|
-
normalized[key] = normalizePropertyAwareEqualityValue(child);
|
|
164
|
-
}
|
|
165
|
-
return normalized;
|
|
166
|
-
}
|
|
167
|
-
return value;
|
|
168
|
-
}
|
|
169
|
-
/**
|
|
170
|
-
* Compare values while ignoring persistence-only markers on property-aware structures.
|
|
171
|
-
* This avoids false negatives when comparing persisted state to defaults.
|
|
172
|
-
*/
|
|
173
|
-
function propertyAwareDeepEqual(a, b) {
|
|
174
|
-
return isEqual(normalizePropertyAwareEqualityValue(a), normalizePropertyAwareEqualityValue(b));
|
|
175
|
-
}
|
|
176
144
|
/**
|
|
177
145
|
* A generic base class for forms.
|
|
178
146
|
*
|
|
@@ -191,6 +159,20 @@ export class BaseForm {
|
|
|
191
159
|
getPersistenceDriver(_suffix) {
|
|
192
160
|
return new NonPersistentDriver();
|
|
193
161
|
}
|
|
162
|
+
getPersistenceRestorePolicy() {
|
|
163
|
+
return new StrictPersistenceRestorePolicy();
|
|
164
|
+
}
|
|
165
|
+
shouldLogPersistenceDebug() {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
logPersistenceDebug(event) {
|
|
169
|
+
if (!this.shouldLogPersistenceDebug()) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const suffixLabel = event.persistSuffix ? ` (${event.persistSuffix})` : '';
|
|
173
|
+
const details = event.details ? ` ${JSON.stringify(event.details)}` : '';
|
|
174
|
+
console.debug(`[BaseForm persistence] ${event.formName}${suffixLabel}: ${event.action} (${event.reason})${details}`);
|
|
175
|
+
}
|
|
194
176
|
/**
|
|
195
177
|
* Helper: recursively computes the dirty state for a value based on the original.
|
|
196
178
|
* For plain arrays we compare the entire array (a single flag), not each element.
|
|
@@ -324,6 +306,7 @@ export class BaseForm {
|
|
|
324
306
|
}
|
|
325
307
|
}
|
|
326
308
|
constructor(defaults, options) {
|
|
309
|
+
var _a;
|
|
327
310
|
this.options = options;
|
|
328
311
|
this._errors = reactive({});
|
|
329
312
|
this._asyncErrors = reactive({});
|
|
@@ -342,21 +325,35 @@ export class BaseForm {
|
|
|
342
325
|
let initialData;
|
|
343
326
|
const driver = this.getPersistenceDriver(options === null || options === void 0 ? void 0 : options.persistSuffix);
|
|
344
327
|
if (persist) {
|
|
345
|
-
const persisted = driver.get(this.constructor.name);
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
328
|
+
const persisted = (_a = driver.get(this.constructor.name)) !== null && _a !== void 0 ? _a : null;
|
|
329
|
+
const restoreDecision = this.getPersistenceRestorePolicy().resolve({
|
|
330
|
+
formName: this.constructor.name,
|
|
331
|
+
persistSuffix: options === null || options === void 0 ? void 0 : options.persistSuffix,
|
|
332
|
+
defaults,
|
|
333
|
+
persisted
|
|
334
|
+
});
|
|
335
|
+
this.logPersistenceDebug({
|
|
336
|
+
formName: this.constructor.name,
|
|
337
|
+
persistSuffix: options === null || options === void 0 ? void 0 : options.persistSuffix,
|
|
338
|
+
action: restoreDecision.action,
|
|
339
|
+
reason: restoreDecision.reason,
|
|
340
|
+
details: restoreDecision.details
|
|
341
|
+
});
|
|
342
|
+
if (restoreDecision.action === 'restore' && restoreDecision.persisted) {
|
|
343
|
+
initialData = restorePropertyAwareStructure(defaults, restoreDecision.persisted.state);
|
|
344
|
+
this.original = restorePropertyAwareStructure(defaults, cloneDeep(restoreDecision.persisted.original));
|
|
345
|
+
this.dirty = reactive(restoreDecision.persisted.dirty);
|
|
346
|
+
this.touched = reactive(restoreDecision.persisted.touched || {});
|
|
351
347
|
}
|
|
352
348
|
else {
|
|
353
|
-
console.log('Discarding persisted data for ' + this.constructor.name + " because it doesn't match the defaults.");
|
|
354
349
|
initialData = defaults;
|
|
355
350
|
this.original = restorePropertyAwareStructure(defaults, cloneDeep(defaults));
|
|
356
351
|
const init = this.initDirtyTouched(defaults);
|
|
357
352
|
this.dirty = init.dirty;
|
|
358
353
|
this.touched = init.touched;
|
|
359
|
-
|
|
354
|
+
if (restoreDecision.action === 'discard') {
|
|
355
|
+
driver.remove(this.constructor.name);
|
|
356
|
+
}
|
|
360
357
|
}
|
|
361
358
|
}
|
|
362
359
|
else {
|
|
@@ -379,7 +376,7 @@ export class BaseForm {
|
|
|
379
376
|
set: (newVal) => {
|
|
380
377
|
const next = Array.isArray(newVal) ? Array.from(newVal) : [];
|
|
381
378
|
this.replacePropertyAwareArray(key, next);
|
|
382
|
-
this.dirty[key] =
|
|
379
|
+
this.dirty[key] = this.computeDirtyState(this.state[key], this.original[key]);
|
|
383
380
|
this.markFieldUpdated(key, driver);
|
|
384
381
|
}
|
|
385
382
|
});
|
|
@@ -1193,7 +1190,7 @@ export class BaseForm {
|
|
|
1193
1190
|
if (currentVal instanceof PropertyAwareArray) {
|
|
1194
1191
|
const values = newVal instanceof PropertyAwareArray || Array.isArray(newVal) ? Array.from(newVal) : [];
|
|
1195
1192
|
this.replacePropertyAwareArray(key, values);
|
|
1196
|
-
this.dirty[key] =
|
|
1193
|
+
this.dirty[key] = this.computeDirtyState(this.state[key], this.original[key]);
|
|
1197
1194
|
this.touched[key] = true;
|
|
1198
1195
|
continue;
|
|
1199
1196
|
}
|
|
@@ -6,5 +6,7 @@ import { SessionStorageDriver } from '../../persistenceDrivers/SessionStorageDri
|
|
|
6
6
|
import { type PersistenceDriver } from '../../persistenceDrivers/types/PersistenceDriver';
|
|
7
7
|
import { PropertyAwareArray, type PropertyAwareField, type PropertyAware } from './PropertyAwareArray';
|
|
8
8
|
import { PropertyAwareObject } from './PropertyAwareObject';
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
import { StrictPersistenceRestorePolicy } from './persistence';
|
|
10
|
+
import type { PersistenceDebugEvent, PersistenceRestoreContext, PersistenceRestorePolicy, PersistenceRestoreResult } from './persistence';
|
|
11
|
+
export { BaseForm, propertyAwareToRaw, PropertyAwareArray, PropertyAwareObject, NonPersistentDriver, SessionStorageDriver, LocalStorageDriver, StrictPersistenceRestorePolicy };
|
|
12
|
+
export type { PersistedForm, PersistenceDriver, PropertyAwareField, PropertyAware, PersistenceDebugEvent, PersistenceRestoreContext, PersistenceRestorePolicy, PersistenceRestoreResult };
|
package/dist/vue/forms/index.js
CHANGED
|
@@ -4,4 +4,5 @@ import { NonPersistentDriver } from '../../persistenceDrivers/NonPersistentDrive
|
|
|
4
4
|
import { SessionStorageDriver } from '../../persistenceDrivers/SessionStorageDriver';
|
|
5
5
|
import { PropertyAwareArray } from './PropertyAwareArray';
|
|
6
6
|
import { PropertyAwareObject } from './PropertyAwareObject';
|
|
7
|
-
|
|
7
|
+
import { StrictPersistenceRestorePolicy } from './persistence';
|
|
8
|
+
export { BaseForm, propertyAwareToRaw, PropertyAwareArray, PropertyAwareObject, NonPersistentDriver, SessionStorageDriver, LocalStorageDriver, StrictPersistenceRestorePolicy };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type PersistenceRestoreContext, type PersistenceRestorePolicy, type PersistenceRestoreResult } from './types';
|
|
2
|
+
export declare class StrictPersistenceRestorePolicy<FormBody extends object> implements PersistenceRestorePolicy<FormBody> {
|
|
3
|
+
resolve(context: PersistenceRestoreContext<FormBody>): PersistenceRestoreResult<FormBody>;
|
|
4
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { collectPropertyAwareMismatchPaths, propertyAwareDeepEqual } from './utils';
|
|
2
|
+
function isRecord(value) {
|
|
3
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
function isPersistedFormLike(value) {
|
|
6
|
+
if (!isRecord(value)) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
return 'state' in value && 'original' in value && 'dirty' in value;
|
|
10
|
+
}
|
|
11
|
+
export class StrictPersistenceRestorePolicy {
|
|
12
|
+
resolve(context) {
|
|
13
|
+
const { defaults, persisted } = context;
|
|
14
|
+
if (persisted === null) {
|
|
15
|
+
return {
|
|
16
|
+
action: 'ignore',
|
|
17
|
+
reason: 'no_persisted_state'
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
if (!isPersistedFormLike(persisted)) {
|
|
21
|
+
return {
|
|
22
|
+
action: 'discard',
|
|
23
|
+
reason: 'invalid_persisted_state'
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (propertyAwareDeepEqual(defaults, persisted.original)) {
|
|
27
|
+
return {
|
|
28
|
+
action: 'restore',
|
|
29
|
+
reason: 'defaults_match',
|
|
30
|
+
persisted
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
action: 'discard',
|
|
35
|
+
reason: 'defaults_mismatch',
|
|
36
|
+
persisted,
|
|
37
|
+
details: {
|
|
38
|
+
mismatchPaths: collectPropertyAwareMismatchPaths(defaults, persisted.original)
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type PersistedForm } from '../types/PersistedForm';
|
|
2
|
+
export interface PersistenceRestoreContext<FormBody extends object> {
|
|
3
|
+
formName: string;
|
|
4
|
+
persistSuffix?: string | undefined;
|
|
5
|
+
defaults: FormBody;
|
|
6
|
+
persisted: PersistedForm<FormBody> | null;
|
|
7
|
+
}
|
|
8
|
+
export interface PersistenceRestoreResult<FormBody extends object> {
|
|
9
|
+
action: 'restore' | 'discard' | 'ignore';
|
|
10
|
+
reason: string;
|
|
11
|
+
persisted?: PersistedForm<FormBody> | undefined;
|
|
12
|
+
details?: Record<string, unknown> | undefined;
|
|
13
|
+
}
|
|
14
|
+
export interface PersistenceDebugEvent<FormBody extends object> {
|
|
15
|
+
formName: string;
|
|
16
|
+
persistSuffix?: string | undefined;
|
|
17
|
+
action: PersistenceRestoreResult<FormBody>['action'];
|
|
18
|
+
reason: string;
|
|
19
|
+
details?: Record<string, unknown> | undefined;
|
|
20
|
+
}
|
|
21
|
+
export interface PersistenceRestorePolicy<FormBody extends object> {
|
|
22
|
+
resolve(context: PersistenceRestoreContext<FormBody>): PersistenceRestoreResult<FormBody>;
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { isEqual } from 'lodash-es';
|
|
2
|
+
import { PropertyAwareArray } from '../PropertyAwareArray';
|
|
3
|
+
import { PropertyAwareObject, PROPERTY_AWARE_OBJECT_MARKER } from '../PropertyAwareObject';
|
|
4
|
+
function isRecord(value) {
|
|
5
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
function isPropertyAwareObject(value) {
|
|
8
|
+
return value instanceof PropertyAwareObject;
|
|
9
|
+
}
|
|
10
|
+
function isSerializedPropertyAwareObject(value) {
|
|
11
|
+
return isRecord(value) && value[PROPERTY_AWARE_OBJECT_MARKER] === true;
|
|
12
|
+
}
|
|
13
|
+
function normalizePropertyAwareEqualityValue(value) {
|
|
14
|
+
if (value instanceof PropertyAwareArray) {
|
|
15
|
+
return Array.from(value, (item) => normalizePropertyAwareEqualityValue(item));
|
|
16
|
+
}
|
|
17
|
+
if (isPropertyAwareObject(value) || isSerializedPropertyAwareObject(value)) {
|
|
18
|
+
const normalized = {};
|
|
19
|
+
for (const [key, child] of Object.entries(value)) {
|
|
20
|
+
if (key === PROPERTY_AWARE_OBJECT_MARKER) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
normalized[key] = normalizePropertyAwareEqualityValue(child);
|
|
24
|
+
}
|
|
25
|
+
return normalized;
|
|
26
|
+
}
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
return value.map((item) => normalizePropertyAwareEqualityValue(item));
|
|
29
|
+
}
|
|
30
|
+
if (isRecord(value)) {
|
|
31
|
+
const normalized = {};
|
|
32
|
+
for (const [key, child] of Object.entries(value)) {
|
|
33
|
+
normalized[key] = normalizePropertyAwareEqualityValue(child);
|
|
34
|
+
}
|
|
35
|
+
return normalized;
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
export function propertyAwareDeepEqual(a, b) {
|
|
40
|
+
return isEqual(normalizePropertyAwareEqualityValue(a), normalizePropertyAwareEqualityValue(b));
|
|
41
|
+
}
|
|
42
|
+
export function collectPropertyAwareMismatchPaths(a, b, path = '', maxPaths = 10) {
|
|
43
|
+
const mismatches = [];
|
|
44
|
+
const visit = (left, right, currentPath) => {
|
|
45
|
+
if (mismatches.length >= maxPaths) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const normalizedLeft = normalizePropertyAwareEqualityValue(left);
|
|
49
|
+
const normalizedRight = normalizePropertyAwareEqualityValue(right);
|
|
50
|
+
if (isEqual(normalizedLeft, normalizedRight)) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (Array.isArray(normalizedLeft) && Array.isArray(normalizedRight)) {
|
|
54
|
+
if (normalizedLeft.length !== normalizedRight.length) {
|
|
55
|
+
mismatches.push(currentPath || '(root)');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
normalizedLeft.forEach((item, index) => {
|
|
59
|
+
visit(item, normalizedRight[index], currentPath ? `${currentPath}.${index}` : String(index));
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (isRecord(normalizedLeft) && isRecord(normalizedRight)) {
|
|
64
|
+
const keys = new Set([...Object.keys(normalizedLeft), ...Object.keys(normalizedRight)]);
|
|
65
|
+
for (const key of keys) {
|
|
66
|
+
visit(normalizedLeft[key], normalizedRight[key], currentPath ? `${currentPath}.${key}` : key);
|
|
67
|
+
if (mismatches.length >= maxPaths) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
mismatches.push(currentPath || '(root)');
|
|
74
|
+
};
|
|
75
|
+
visit(a, b, path);
|
|
76
|
+
return mismatches;
|
|
77
|
+
}
|