@configura/web-api 1.6.1-alpha.1 → 1.6.1-alpha.2
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/CfgProduct.d.ts +8 -14
- package/dist/CfgProduct.js +43 -20
- package/dist/productConfiguration/CfgFeature.js +1 -14
- package/dist/productConfiguration/CfgOption.d.ts +9 -0
- package/dist/productConfiguration/CfgOption.js +24 -0
- package/dist/productConfiguration/filters.d.ts +1 -1
- package/dist/productConfiguration/filters.js +6 -3
- package/dist/syncGroups/SyncGroupsApplier.js +2 -2
- package/dist/syncGroups/SyncGroupsHandler.d.ts +12 -3
- package/dist/syncGroups/SyncGroupsHandler.js +298 -17
- package/dist/syncGroups/SyncGroupsState.d.ts +11 -5
- package/dist/syncGroups/SyncGroupsState.js +58 -55
- package/dist/syncGroups/SyncGroupsTransaction.d.ts +5 -5
- package/dist/syncGroups/SyncGroupsTransaction.js +7 -7
- package/dist/utilitiesCatalogueData.js +7 -5
- package/package.json +3 -3
package/dist/CfgProduct.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AggregatedLoadingObservable, LengthUnit, Observable, SingleArgCallback } from "@configura/web-utilities";
|
|
2
|
-
import { AdditionalProductConfiguration, CatalogueParams, MeasureParam, MtrlApplication, Prices, Transform } from "./CatalogueAPI.js";
|
|
2
|
+
import { AdditionalProductConfiguration, AdditionalProductRef, CatalogueParams, MeasureParam, MtrlApplication, Prices, Transform } from "./CatalogueAPI.js";
|
|
3
3
|
import { CfgMeasureDefinition } from "./CfgMeasure.js";
|
|
4
4
|
import { _CfgFeatureInternal } from "./productConfiguration/CfgFeature.js";
|
|
5
5
|
import { ProductConfigurationBubbleMode } from "./productConfiguration/CfgOption.js";
|
|
@@ -19,13 +19,6 @@ export declare type CfgProductSettings = {
|
|
|
19
19
|
* Activating this will make setApi throw an Error if more or less than one is selected.
|
|
20
20
|
*/
|
|
21
21
|
strictSelectOneSelectionCount: boolean;
|
|
22
|
-
/**
|
|
23
|
-
* Activating this will make setAPI throw an error if the number of actually selected options
|
|
24
|
-
* on Features (excluding Group) are not exactly equal to the number of options passed in.
|
|
25
|
-
* Note: This check is not always reliable for Options with multiple Features each, which we
|
|
26
|
-
* believe is a rare use case.
|
|
27
|
-
*/
|
|
28
|
-
strictSetApiSelectionMatch: boolean;
|
|
29
22
|
/**
|
|
30
23
|
* Controls if SyncGroups are applied Faster or Stricter.
|
|
31
24
|
*
|
|
@@ -83,13 +76,10 @@ export declare class _CfgProductInternal {
|
|
|
83
76
|
private readonly _rawUnit;
|
|
84
77
|
private _rawProductData;
|
|
85
78
|
readonly loadingObservable: AggregatedLoadingObservable;
|
|
86
|
-
readonly refKey: string | undefined;
|
|
87
|
-
readonly refDescription: string | undefined;
|
|
88
79
|
readonly parent: _CfgProductInternal | undefined;
|
|
89
|
-
|
|
90
|
-
anchor: MeasureParam | undefined;
|
|
80
|
+
private _additionalProductRef;
|
|
91
81
|
private readonly _syncGroupHandler;
|
|
92
|
-
static make: (productLoaderRaw: ProductLoader, productLoaderForGroupedLoad: ProductLoader | undefined, lang: string, catId: CatalogueParams, partNumber: string, settings: CfgProductSettings, optional: boolean, loadingObservable: AggregatedLoadingObservable,
|
|
82
|
+
static make: (productLoaderRaw: ProductLoader, productLoaderForGroupedLoad: ProductLoader | undefined, lang: string, catId: CatalogueParams, partNumber: string, settings: CfgProductSettings, optional: boolean, loadingObservable: AggregatedLoadingObservable, parent: _CfgProductInternal | undefined, root: _CfgProductInternal | undefined, additionalProductRef: AdditionalProductRef | undefined) => Promise<_CfgProductInternal>;
|
|
93
83
|
private constructor();
|
|
94
84
|
readonly root: _CfgProductInternal;
|
|
95
85
|
private _destroyed;
|
|
@@ -102,6 +92,7 @@ export declare class _CfgProductInternal {
|
|
|
102
92
|
readonly isAdditionalProduct: boolean;
|
|
103
93
|
clone(parent?: _CfgProductInternal, root?: _CfgProductInternal): Promise<_CfgProductInternal>;
|
|
104
94
|
destroy: () => void;
|
|
95
|
+
_updateAdditionalProdRef(p: AdditionalProductRef): void;
|
|
105
96
|
get description(): string | undefined;
|
|
106
97
|
get rootNodeSources(): RootNodeSource[] | undefined;
|
|
107
98
|
get mtrlApplications(): MtrlApplication[] | undefined;
|
|
@@ -111,6 +102,9 @@ export declare class _CfgProductInternal {
|
|
|
111
102
|
private _measureDefinitions;
|
|
112
103
|
get measureDefinitions(): CfgMeasureDefinition[];
|
|
113
104
|
private _unit;
|
|
105
|
+
get refKey(): string | undefined;
|
|
106
|
+
get transform(): Transform | undefined;
|
|
107
|
+
get anchor(): MeasureParam | undefined;
|
|
114
108
|
/** @throws an error if the actual unit sent by the server was not a LengthUnit */
|
|
115
109
|
get unit(): LengthUnit;
|
|
116
110
|
get aggregatedPrice(): CfgPrice;
|
|
@@ -142,7 +136,7 @@ export declare class _CfgProductInternal {
|
|
|
142
136
|
_configurationHasChanged: (freshRef: CfgProductConfiguration, bubbleMode: ProductConfigurationBubbleMode) => Promise<void>;
|
|
143
137
|
getApiSelection: () => AdditionalProductConfiguration;
|
|
144
138
|
setApiSelection: (s: AdditionalProductConfiguration, doValidate: boolean, productLoaderForGroupedLoad?: ProductLoader | undefined) => Promise<boolean>;
|
|
145
|
-
copyFrom: (
|
|
139
|
+
copyFrom: (source: _CfgProductInternal, doValidate: boolean, productLoaderForGroupedLoad?: ProductLoader | undefined) => Promise<boolean>;
|
|
146
140
|
private _setApiSelectionWithOtherProduct;
|
|
147
141
|
get syncGroupHandler(): SyncGroupsHandler | undefined;
|
|
148
142
|
structureCompare: (other: _CfgProductInternal, strictOrder?: boolean, descriptionMatch?: boolean) => boolean;
|
package/dist/CfgProduct.js
CHANGED
|
@@ -14,12 +14,11 @@ import { CfgProductConfiguration } from "./productConfiguration/CfgProductConfig
|
|
|
14
14
|
import { collectAdditionalProductRefs } from "./productConfiguration/utilitiesProductConfiguration.js";
|
|
15
15
|
import { wrapWithCache } from "./productLoader.js";
|
|
16
16
|
import { SyncGroupsHandler } from "./syncGroups/SyncGroupsHandler.js";
|
|
17
|
-
import { comparePricesObjects, correctDefaultsOnCatalogueParams, isSameProductRef, makeProductKey, } from "./utilitiesCatalogueData.js";
|
|
17
|
+
import { comparePricesObjects, correctDefaultsOnCatalogueParams, isSameCatalogueParams, isSameProductRef, makeProductKey, } from "./utilitiesCatalogueData.js";
|
|
18
18
|
function completeSettings(incompleteSettings) {
|
|
19
|
-
var _a
|
|
19
|
+
var _a;
|
|
20
20
|
return {
|
|
21
21
|
strictSelectOneSelectionCount: (_a = incompleteSettings === null || incompleteSettings === void 0 ? void 0 : incompleteSettings.strictSelectOneSelectionCount) !== null && _a !== void 0 ? _a : false,
|
|
22
|
-
strictSetApiSelectionMatch: (_b = incompleteSettings === null || incompleteSettings === void 0 ? void 0 : incompleteSettings.strictSetApiSelectionMatch) !== null && _b !== void 0 ? _b : false,
|
|
23
22
|
syncGroupsApplyMode: incompleteSettings === null || incompleteSettings === void 0 ? void 0 : incompleteSettings.syncGroupsApplyMode,
|
|
24
23
|
};
|
|
25
24
|
}
|
|
@@ -52,7 +51,8 @@ function isDescriptionMatch(l, r) {
|
|
|
52
51
|
* the class that should be used and interacted with.
|
|
53
52
|
*/
|
|
54
53
|
export class _CfgProductInternal {
|
|
55
|
-
constructor(initSuccess, initFail, _productLoaderRaw, lang, catId, partNumber, settings, optional, selected, rootFeatureRefs, allRawFeatures, uuid, _rawUnit, _rawProductData, apiSelection, loadingObservable,
|
|
54
|
+
constructor(initSuccess, initFail, _productLoaderRaw, lang, catId, partNumber, settings, optional, selected, rootFeatureRefs, allRawFeatures, uuid, _rawUnit, _rawProductData, apiSelection, loadingObservable, parent, root, _additionalProductRef, _syncGroupHandler) {
|
|
55
|
+
var _a;
|
|
56
56
|
this._productLoaderRaw = _productLoaderRaw;
|
|
57
57
|
this.lang = lang;
|
|
58
58
|
this.catId = catId;
|
|
@@ -62,11 +62,8 @@ export class _CfgProductInternal {
|
|
|
62
62
|
this._rawUnit = _rawUnit;
|
|
63
63
|
this._rawProductData = _rawProductData;
|
|
64
64
|
this.loadingObservable = loadingObservable;
|
|
65
|
-
this.refKey = refKey;
|
|
66
|
-
this.refDescription = refDescription;
|
|
67
65
|
this.parent = parent;
|
|
68
|
-
this.
|
|
69
|
-
this.anchor = anchor;
|
|
66
|
+
this._additionalProductRef = _additionalProductRef;
|
|
70
67
|
this._syncGroupHandler = _syncGroupHandler;
|
|
71
68
|
this._destroyed = false;
|
|
72
69
|
this.additionalProducts = [];
|
|
@@ -153,8 +150,8 @@ export class _CfgProductInternal {
|
|
|
153
150
|
this.setApiSelection = (s, doValidate, productLoaderForGroupedLoad) => __awaiter(this, void 0, void 0, function* () {
|
|
154
151
|
return this._setApiSelectionWithOtherProduct(s, doValidate, productLoaderForGroupedLoad, undefined);
|
|
155
152
|
});
|
|
156
|
-
this.copyFrom = (
|
|
157
|
-
return yield this._setApiSelectionWithOtherProduct(
|
|
153
|
+
this.copyFrom = (source, doValidate, productLoaderForGroupedLoad) => __awaiter(this, void 0, void 0, function* () {
|
|
154
|
+
return yield this._setApiSelectionWithOtherProduct(source.getApiSelection(), doValidate, productLoaderForGroupedLoad, source);
|
|
158
155
|
});
|
|
159
156
|
this._setApiSelectionWithOtherProduct = (s, doValidate, productLoaderForGroupedLoad, sourceProduct) => __awaiter(this, void 0, void 0, function* () {
|
|
160
157
|
// Wrap with cache will make getProduct for this function call use the same server call
|
|
@@ -364,14 +361,19 @@ export class _CfgProductInternal {
|
|
|
364
361
|
let i = currentAdditionalProducts.length;
|
|
365
362
|
while (i--) {
|
|
366
363
|
const currentAdditionalProduct = currentAdditionalProducts[i];
|
|
367
|
-
const
|
|
368
|
-
|
|
364
|
+
const j = additionalProductRefs.findIndex((p) => {
|
|
365
|
+
const prodRef = p.prodRef;
|
|
366
|
+
return (prodRef.refKey === currentAdditionalProduct.refKey &&
|
|
367
|
+
prodRef.partNumber === currentAdditionalProduct.partNumber &&
|
|
368
|
+
isSameCatalogueParams(prodRef.catId, currentAdditionalProduct.catId));
|
|
369
|
+
});
|
|
369
370
|
if (j === -1) {
|
|
370
371
|
currentAdditionalProduct.destroy();
|
|
371
372
|
currentAdditionalProducts.splice(i, 1);
|
|
372
373
|
change = true;
|
|
373
374
|
}
|
|
374
375
|
else {
|
|
376
|
+
currentAdditionalProduct._internal._updateAdditionalProdRef(additionalProductRefs[j].prodRef);
|
|
375
377
|
additionalProductRefs.splice(j, 1);
|
|
376
378
|
}
|
|
377
379
|
}
|
|
@@ -384,7 +386,7 @@ export class _CfgProductInternal {
|
|
|
384
386
|
const additionalProductRef = p.prodRef;
|
|
385
387
|
return {
|
|
386
388
|
originalIndex: p.originalIndex,
|
|
387
|
-
product: CfgProduct._makeNewRefFrom(yield _CfgProductInternal.make(productLoaderRaw, productLoaderForGroupedLoad, lang, additionalProductRef.catId, additionalProductRef.partNumber, this.settings, additionalProductRef.optional === true, this.loadingObservable,
|
|
389
|
+
product: CfgProduct._makeNewRefFrom(yield _CfgProductInternal.make(productLoaderRaw, productLoaderForGroupedLoad, lang, additionalProductRef.catId, additionalProductRef.partNumber, this.settings, additionalProductRef.optional === true, this.loadingObservable, this, this.root, additionalProductRef)),
|
|
388
390
|
};
|
|
389
391
|
}))()));
|
|
390
392
|
if (this._destroyed) {
|
|
@@ -403,7 +405,7 @@ export class _CfgProductInternal {
|
|
|
403
405
|
}
|
|
404
406
|
});
|
|
405
407
|
this.root = root !== null && root !== void 0 ? root : this;
|
|
406
|
-
this.key = makeProductKey(catId,
|
|
408
|
+
this.key = makeProductKey(catId, (_a = _additionalProductRef === null || _additionalProductRef === void 0 ? void 0 : _additionalProductRef.refKey) !== null && _a !== void 0 ? _a : partNumber);
|
|
407
409
|
this._selected = optional ? selected : undefined;
|
|
408
410
|
this.isAdditionalProduct = parent !== undefined;
|
|
409
411
|
this._configuration = CfgProductConfiguration.make(initSuccess, initFail, rootFeatureRefs, allRawFeatures, apiSelection, this, this.root);
|
|
@@ -417,7 +419,7 @@ export class _CfgProductInternal {
|
|
|
417
419
|
var _a;
|
|
418
420
|
const p = new _CfgProductInternal(() => {
|
|
419
421
|
initSuccess(p);
|
|
420
|
-
}, initFail, this._productLoaderRaw, this.lang, this.catId, this.partNumber, this.settings, this.optional, this.selected, this._configuration.rootFeatureRefs, this._configuration.allRawFeatures, this.uuid, this._rawUnit, this._rawProductData, this.configuration.getApiSelection(), new AggregatedLoadingObservable(),
|
|
422
|
+
}, initFail, this._productLoaderRaw, this.lang, this.catId, this.partNumber, this.settings, this.optional, this.selected, this._configuration.rootFeatureRefs, this._configuration.allRawFeatures, this.uuid, this._rawUnit, this._rawProductData, this.configuration.getApiSelection(), new AggregatedLoadingObservable(), parent, root, this._additionalProductRef, (_a = this._syncGroupHandler) === null || _a === void 0 ? void 0 : _a.clone());
|
|
421
423
|
});
|
|
422
424
|
for (const additionalProduct of this.additionalProducts) {
|
|
423
425
|
product.additionalProducts.push(CfgProduct._makeNewRefFrom(yield additionalProduct._internal.clone(product, root || product)));
|
|
@@ -425,8 +427,15 @@ export class _CfgProductInternal {
|
|
|
425
427
|
return product;
|
|
426
428
|
});
|
|
427
429
|
}
|
|
430
|
+
_updateAdditionalProdRef(p) {
|
|
431
|
+
this._additionalProductRef = p;
|
|
432
|
+
if (p.optional !== this.optional) {
|
|
433
|
+
this._selected = p.optional ? false : undefined;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
428
436
|
get description() {
|
|
429
|
-
|
|
437
|
+
var _a, _b;
|
|
438
|
+
return (_b = (_a = this._additionalProductRef) === null || _a === void 0 ? void 0 : _a.refDescription) !== null && _b !== void 0 ? _b : this._rawProductData.description;
|
|
430
439
|
}
|
|
431
440
|
get rootNodeSources() {
|
|
432
441
|
return this._rawProductData.models;
|
|
@@ -452,6 +461,18 @@ export class _CfgProductInternal {
|
|
|
452
461
|
}
|
|
453
462
|
return this._measureDefinitions;
|
|
454
463
|
}
|
|
464
|
+
get refKey() {
|
|
465
|
+
var _a;
|
|
466
|
+
return (_a = this._additionalProductRef) === null || _a === void 0 ? void 0 : _a.refKey;
|
|
467
|
+
}
|
|
468
|
+
get transform() {
|
|
469
|
+
var _a;
|
|
470
|
+
return (_a = this._additionalProductRef) === null || _a === void 0 ? void 0 : _a.transform;
|
|
471
|
+
}
|
|
472
|
+
get anchor() {
|
|
473
|
+
var _a;
|
|
474
|
+
return (_a = this._additionalProductRef) === null || _a === void 0 ? void 0 : _a.anchor;
|
|
475
|
+
}
|
|
455
476
|
/** @throws an error if the actual unit sent by the server was not a LengthUnit */
|
|
456
477
|
get unit() {
|
|
457
478
|
if (this._unit === undefined) {
|
|
@@ -534,13 +555,15 @@ export class _CfgProductInternal {
|
|
|
534
555
|
}
|
|
535
556
|
}
|
|
536
557
|
_CfgProductInternal.make = (productLoaderRaw, productLoaderForGroupedLoad, // Used when instantiating the current product
|
|
537
|
-
lang, catId, partNumber, settings, optional, loadingObservable,
|
|
558
|
+
lang, catId, partNumber, settings, optional, loadingObservable, parent, root, additionalProductRef) => __awaiter(void 0, void 0, void 0, function* () {
|
|
538
559
|
// Wrap with cache will make getProduct for this function call use the same server call
|
|
539
560
|
// for the same product with the same params. Not retained for future calls, only used
|
|
540
561
|
// at this initial load.
|
|
541
562
|
productLoaderForGroupedLoad =
|
|
542
563
|
productLoaderForGroupedLoad || wrapWithCache(productLoaderRaw);
|
|
543
|
-
const syncGroupHandler = root === undefined
|
|
564
|
+
const syncGroupHandler = root === undefined
|
|
565
|
+
? SyncGroupsHandler.make(settings.syncGroupsApplyMode, loadingObservable)
|
|
566
|
+
: undefined;
|
|
544
567
|
const productResponse = yield productLoaderForGroupedLoad.getProduct(Object.assign(Object.assign({ lang }, correctDefaultsOnCatalogueParams(catId)), { partNumber }));
|
|
545
568
|
const { productData, rootFeatureRefs, features: allRawFeatures, uuid, unit, } = productResponse;
|
|
546
569
|
const product = yield new Promise((initSuccess, initFail) => {
|
|
@@ -549,7 +572,7 @@ lang, catId, partNumber, settings, optional, loadingObservable, refKey, refDescr
|
|
|
549
572
|
// But we can not set the api selection synchronously. And the product configuration needs "this". So we use this callback.
|
|
550
573
|
// Feel free to find a nicer more readable solution :)
|
|
551
574
|
initSuccess(p);
|
|
552
|
-
}, initFail, productLoaderRaw, lang, catId, partNumber, settings, optional, !optional, rootFeatureRefs, allRawFeatures, uuid, unit, productData, productData.partsData.selOptions || [], loadingObservable,
|
|
575
|
+
}, initFail, productLoaderRaw, lang, catId, partNumber, settings, optional, !optional, rootFeatureRefs, allRawFeatures, uuid, unit, productData, productData.partsData.selOptions || [], loadingObservable, parent, root, additionalProductRef, syncGroupHandler);
|
|
553
576
|
});
|
|
554
577
|
yield product._syncAndLoadAdditionalProducts(productLoaderForGroupedLoad);
|
|
555
578
|
// Product is guaranteed to be root
|
|
@@ -604,7 +627,7 @@ export class CfgProduct {
|
|
|
604
627
|
}
|
|
605
628
|
static make(productLoader, lang, catId, partNumber, settings) {
|
|
606
629
|
return __awaiter(this, void 0, void 0, function* () {
|
|
607
|
-
return this._makeNewRefFrom(yield _CfgProductInternal.make(productLoader, undefined, lang, catId, partNumber, completeSettings(settings), false, new AggregatedLoadingObservable(), undefined, undefined, undefined
|
|
630
|
+
return this._makeNewRefFrom(yield _CfgProductInternal.make(productLoader, undefined, lang, catId, partNumber, completeSettings(settings), false, new AggregatedLoadingObservable(), undefined, undefined, undefined));
|
|
608
631
|
});
|
|
609
632
|
}
|
|
610
633
|
/**
|
|
@@ -12,7 +12,6 @@ import { CfgProduct } from "../CfgProduct.js";
|
|
|
12
12
|
import { CfgMtrlApplication } from "../material/CfgMtrlApplication.js";
|
|
13
13
|
import { CfgMtrlApplicationSource } from "../material/CfgMtrlApplicationSource.js";
|
|
14
14
|
import { wrapWithCache } from "../productLoader.js";
|
|
15
|
-
import { SyncGroupsPathHelper } from "../syncGroups/SyncGroupsPathHelper.js";
|
|
16
15
|
import { CfgOption, ProductConfigurationBubbleMode } from "./CfgOption.js";
|
|
17
16
|
import { _CfgProductConfigurationInternal } from "./CfgProductConfiguration.js";
|
|
18
17
|
import { getMtrlPreview } from "./utilitiesProductConfiguration.js";
|
|
@@ -190,18 +189,6 @@ export class _CfgFeatureInternal {
|
|
|
190
189
|
console.warn(wrongCountWarning);
|
|
191
190
|
}
|
|
192
191
|
}
|
|
193
|
-
if (!isGroup) {
|
|
194
|
-
const apiSelectionCount = Object.keys(apiOptionSelectionMap).length;
|
|
195
|
-
if (selectionCount !== apiSelectionCount) {
|
|
196
|
-
const wrongCountWarning = `All provided Options are expected to be selected. Feature key: "${this.key}". Expected: ${apiSelectionCount} Actual: ${selectionCount}`;
|
|
197
|
-
if (this.rootProduct.settings.strictSetApiSelectionMatch) {
|
|
198
|
-
throw new Error(wrongCountWarning);
|
|
199
|
-
}
|
|
200
|
-
else {
|
|
201
|
-
console.warn(wrongCountWarning);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
192
|
}
|
|
206
193
|
if (change) {
|
|
207
194
|
if (isAllOptionsAffectedByAnySelection) {
|
|
@@ -313,7 +300,7 @@ export class _CfgFeatureInternal {
|
|
|
313
300
|
const product = this.rootProduct;
|
|
314
301
|
const syncGroupHandler = product.syncGroupHandler;
|
|
315
302
|
assertDefined(syncGroupHandler, `Sync group handler is required for bubble mode ${ProductConfigurationBubbleMode.ValidateAndBubbleSelectedAndApplySyncGroups}`);
|
|
316
|
-
return yield syncGroupHandler.selectOption(product,
|
|
303
|
+
return yield syncGroupHandler.selectOption(product, optionInternal, on, wrapWithCache(product._productLoaderRaw));
|
|
317
304
|
}
|
|
318
305
|
if (!on) {
|
|
319
306
|
if (this.selectionType === SelectionType.Group) {
|
|
@@ -79,6 +79,7 @@ export declare class _CfgOptionInternal {
|
|
|
79
79
|
get unit(): LengthUnit;
|
|
80
80
|
get description(): string;
|
|
81
81
|
get selected(): boolean;
|
|
82
|
+
get selectedChangeInProgress(): boolean;
|
|
82
83
|
get ancestorsSelected(): boolean;
|
|
83
84
|
get mtrlApplications(): CfgMtrlApplication[];
|
|
84
85
|
get thumbnail(): string | undefined;
|
|
@@ -124,6 +125,14 @@ export declare class CfgOption {
|
|
|
124
125
|
get unit(): LengthUnit;
|
|
125
126
|
get description(): string;
|
|
126
127
|
get selected(): boolean;
|
|
128
|
+
/**
|
|
129
|
+
* Selection state is in progress to be changed. This can be used in GUI
|
|
130
|
+
* to display the state as transitioning, or as already changed.
|
|
131
|
+
* If selectedChangeInProgress and:
|
|
132
|
+
* selected is true, it means that this is about to get unselected
|
|
133
|
+
* selected is false, it means that this is about to get selected
|
|
134
|
+
*/
|
|
135
|
+
get selectedChangeInProgress(): boolean;
|
|
127
136
|
/** Are all ancestors up to the CfgProductConfiguration selected? Includes self. */
|
|
128
137
|
get ancestorsSelected(): boolean;
|
|
129
138
|
/**
|
|
@@ -256,6 +256,20 @@ export class _CfgOptionInternal {
|
|
|
256
256
|
get selected() {
|
|
257
257
|
return this.parent.isSelected(this);
|
|
258
258
|
}
|
|
259
|
+
get selectedChangeInProgress() {
|
|
260
|
+
const syncGroupHandler = this.rootProduct.syncGroupHandler;
|
|
261
|
+
if (syncGroupHandler === undefined) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
const inProgressOption = syncGroupHandler.pending;
|
|
265
|
+
if (inProgressOption === this) {
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
if (!(this.selected && this.parent.selectionType === SelectionType.SelectOne)) {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
return this.parent.options.some((o) => o._internal === inProgressOption);
|
|
272
|
+
}
|
|
259
273
|
get ancestorsSelected() {
|
|
260
274
|
return this.selected && this.parent.ancestorsSelected;
|
|
261
275
|
}
|
|
@@ -383,6 +397,16 @@ export class CfgOption {
|
|
|
383
397
|
get selected() {
|
|
384
398
|
return this._internal.selected;
|
|
385
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* Selection state is in progress to be changed. This can be used in GUI
|
|
402
|
+
* to display the state as transitioning, or as already changed.
|
|
403
|
+
* If selectedChangeInProgress and:
|
|
404
|
+
* selected is true, it means that this is about to get unselected
|
|
405
|
+
* selected is false, it means that this is about to get selected
|
|
406
|
+
*/
|
|
407
|
+
get selectedChangeInProgress() {
|
|
408
|
+
return this._internal.selectedChangeInProgress;
|
|
409
|
+
}
|
|
386
410
|
/** Are all ancestors up to the CfgProductConfiguration selected? Includes self. */
|
|
387
411
|
get ancestorsSelected() {
|
|
388
412
|
return this._internal.ancestorsSelected;
|
|
@@ -12,5 +12,5 @@ export declare function applyProductRefFilters(filters: Filters<ProductRefParams
|
|
|
12
12
|
* @param prdRefsFilter Products not in this array are removed
|
|
13
13
|
* @param showEmpty Shall empty levels be shown?
|
|
14
14
|
*/
|
|
15
|
-
export declare function cloneFilterSortLevels(levels: Level[], prdRefsFilter: ProductRef[], showEmpty: boolean): Level[] | undefined;
|
|
15
|
+
export declare function cloneFilterSortLevels(levels: Level[], prdRefsFilter: ProductRef[], showEmpty: boolean, doSort?: boolean): Level[] | undefined;
|
|
16
16
|
//# sourceMappingURL=filters.d.ts.map
|
|
@@ -34,13 +34,13 @@ export function applyProductRefFilters(filters, productRefs) {
|
|
|
34
34
|
* @param prdRefsFilter Products not in this array are removed
|
|
35
35
|
* @param showEmpty Shall empty levels be shown?
|
|
36
36
|
*/
|
|
37
|
-
export function cloneFilterSortLevels(levels, prdRefsFilter, showEmpty) {
|
|
37
|
+
export function cloneFilterSortLevels(levels, prdRefsFilter, showEmpty, doSort = true) {
|
|
38
38
|
const newLevels = [];
|
|
39
39
|
for (const level of levels) {
|
|
40
40
|
// recursively fetch the next levels
|
|
41
41
|
let nextLevels;
|
|
42
42
|
if (level.lvls !== undefined) {
|
|
43
|
-
nextLevels = cloneFilterSortLevels(level.lvls, prdRefsFilter, showEmpty);
|
|
43
|
+
nextLevels = cloneFilterSortLevels(level.lvls, prdRefsFilter, showEmpty, doSort);
|
|
44
44
|
}
|
|
45
45
|
// filter out products
|
|
46
46
|
let newPrdRefs;
|
|
@@ -63,5 +63,8 @@ export function cloneFilterSortLevels(levels, prdRefsFilter, showEmpty) {
|
|
|
63
63
|
newLevels.push(newLevel);
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
-
|
|
66
|
+
if (doSort) {
|
|
67
|
+
return newLevels.sort((l1, l2) => l1.description.toLocaleLowerCase().localeCompare(l2.description.toLocaleLowerCase()));
|
|
68
|
+
}
|
|
69
|
+
return newLevels;
|
|
67
70
|
}
|
|
@@ -88,7 +88,7 @@ class SyncStateOnto {
|
|
|
88
88
|
// All settled, continue to pullPhase
|
|
89
89
|
return yield OntoSyncState.rootProduct(transaction);
|
|
90
90
|
}
|
|
91
|
-
if (transaction.
|
|
91
|
+
if (transaction.isClosed) {
|
|
92
92
|
// We could exit in more places when the transaction has been aborted,
|
|
93
93
|
// but as revalidate is really the only thing that could be expensive /
|
|
94
94
|
// time consuming we only check here.
|
|
@@ -106,7 +106,7 @@ class SyncStateOnto {
|
|
|
106
106
|
// could accumulate and what is cause and effect be hard to know.
|
|
107
107
|
if (!revalidationResults.every((r) => r.requestDidValidate) ||
|
|
108
108
|
revalidationResults.every((r) => r.wasAborted)) {
|
|
109
|
-
transaction.
|
|
109
|
+
transaction.close();
|
|
110
110
|
return false;
|
|
111
111
|
}
|
|
112
112
|
// Apply over again, to settle deeper down. Our theory is that the front of
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { AggregatedLoadingObservable } from "@configura/web-utilities";
|
|
1
2
|
import { _CfgProductInternal } from "../CfgProduct.js";
|
|
3
|
+
import { _CfgOptionInternal } from "../productConfiguration/CfgOption.js";
|
|
2
4
|
import { ProductLoader } from "../productLoader.js";
|
|
3
5
|
import { SyncGroupsApplyMode } from "./SyncGroupsApplyMode.js";
|
|
4
|
-
import { CfgPath } from "./SyncGroupsPathHelper.js";
|
|
5
6
|
import { SyncGroupsTransaction } from "./SyncGroupsTransaction.js";
|
|
6
7
|
export declare type SyncCode = string;
|
|
7
8
|
export declare type OptionCode = string;
|
|
@@ -12,9 +13,13 @@ export declare type OptionCode = string;
|
|
|
12
13
|
export declare class SyncGroupsHandler {
|
|
13
14
|
private _syncState;
|
|
14
15
|
readonly updateMode: SyncGroupsApplyMode;
|
|
16
|
+
private readonly _loadingObservable;
|
|
15
17
|
private _currentTransaction;
|
|
16
|
-
static make(updateMode?: SyncGroupsApplyMode): SyncGroupsHandler;
|
|
18
|
+
static make(updateMode?: SyncGroupsApplyMode, loadingObservable?: AggregatedLoadingObservable): SyncGroupsHandler;
|
|
17
19
|
private constructor();
|
|
20
|
+
/**
|
|
21
|
+
* Please note that clones will use the same loadingObservable as their source
|
|
22
|
+
*/
|
|
18
23
|
clone(): SyncGroupsHandler;
|
|
19
24
|
/**
|
|
20
25
|
* Used to initially apply the sync state onto a new product so that it is "in sync"
|
|
@@ -24,8 +29,12 @@ export declare class SyncGroupsHandler {
|
|
|
24
29
|
* Used when an Option is selected or deselected to apply all consequences of the sync groups.
|
|
25
30
|
* Can cause multiple extra validation calls to the server.
|
|
26
31
|
*/
|
|
27
|
-
selectOption(product: _CfgProductInternal,
|
|
32
|
+
selectOption(product: _CfgProductInternal, option: _CfgOptionInternal, on: boolean, productLoader: ProductLoader): Promise<boolean>;
|
|
33
|
+
private _pending;
|
|
34
|
+
private setPending;
|
|
35
|
+
get pending(): _CfgOptionInternal | undefined;
|
|
28
36
|
newTransaction(product: _CfgProductInternal, productLoader: ProductLoader, assumeNoStartProductState: boolean): Promise<SyncGroupsTransaction>;
|
|
37
|
+
private closeTransaction;
|
|
29
38
|
applyTransaction(transaction: SyncGroupsTransaction): Promise<void>;
|
|
30
39
|
}
|
|
31
40
|
//# sourceMappingURL=SyncGroupsHandler.d.ts.map
|
|
@@ -7,23 +7,265 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
+
import { SelectionType } from "../productConfiguration/CfgFeature.js";
|
|
11
|
+
import { ProductConfigurationBubbleMode, } from "../productConfiguration/CfgOption.js";
|
|
10
12
|
import { SyncGroupsApplyMode } from "./SyncGroupsApplyMode.js";
|
|
13
|
+
import { SyncGroupsPathHelper } from "./SyncGroupsPathHelper.js";
|
|
11
14
|
import { SyncGroupsState } from "./SyncGroupsState.js";
|
|
12
15
|
import { SyncGroupsTransaction } from "./SyncGroupsTransaction.js";
|
|
16
|
+
/* SyncGroup Concepts
|
|
17
|
+
* ==================
|
|
18
|
+
*
|
|
19
|
+
* SyncGroups are a concept in Catalogues that gives the creator the option to attempt to
|
|
20
|
+
* synchronize selections between otherwise independent Features.
|
|
21
|
+
*
|
|
22
|
+
* Each Feature can optionally specify a "Sync Group Code" in the Catalogue. All features with the
|
|
23
|
+
* same sync group code is said to belong to the same Sync Group.
|
|
24
|
+
*
|
|
25
|
+
* In addition, each Feature that is part of a Sync Group should have a "Sync Mode" set which can
|
|
26
|
+
* be either "Read", "Write" or "Read and Write". The code in the SDK refers to them as "pull",
|
|
27
|
+
* "push" and "twoWay" respectively.
|
|
28
|
+
*
|
|
29
|
+
* The current state of all the SyncGroups is stored in a SyncState. The sync state keeps track of
|
|
30
|
+
* each sync group even if no features are currently visible for that sync group code. In that way,
|
|
31
|
+
* the sync state acts like a kind of short-term memory when a user is configuring a product.
|
|
32
|
+
*
|
|
33
|
+
* The sync state is always discarded when you leave or reload a product and is thus always an
|
|
34
|
+
* empty slate when a product is loaded.
|
|
35
|
+
*
|
|
36
|
+
*
|
|
37
|
+
* Best Effort and Conflicts
|
|
38
|
+
* =========================
|
|
39
|
+
*
|
|
40
|
+
* Catalogues had been along for a long time when the Sync Group functionality was added in 2021,
|
|
41
|
+
* and as a result, it is sort of a layer on top off the normal handling of selecting Options on
|
|
42
|
+
* Features and it is applied in a "best effort"-manner.
|
|
43
|
+
*
|
|
44
|
+
* Two or more Features belonging to the same SyncGroup shows the intent that they should be
|
|
45
|
+
* synchronized, with how being controlled by their respective sync mode.
|
|
46
|
+
*
|
|
47
|
+
* However, the Catalogue format is not strict on how you define the features you want tp keep
|
|
48
|
+
* synchronized. They can for example have only partially overlapping domains (i.e. what Options
|
|
49
|
+
* can be selected) or even no overlap at all which means there is no guarantee that they can
|
|
50
|
+
* always stay in sync.
|
|
51
|
+
*
|
|
52
|
+
* Using different sync modes and bringing features into scope also adds complexity as well as
|
|
53
|
+
* power to create "creative" solutions to design problems.
|
|
54
|
+
*
|
|
55
|
+
* There is also a use case for adding a unique sync group code to single features, which signals
|
|
56
|
+
* the intent that the creator wants those features to keep their previously selected values even
|
|
57
|
+
* when they are not currently visible in the current configuration.
|
|
58
|
+
*
|
|
59
|
+
* In the end, it's up to the Catalogue creator to build the products inside their Catalogues so
|
|
60
|
+
* that his or her intentions are met without unexpected side effects.
|
|
61
|
+
*
|
|
62
|
+
*
|
|
63
|
+
* Technical Details
|
|
64
|
+
* =================
|
|
65
|
+
*
|
|
66
|
+
* Feature types
|
|
67
|
+
* -------------
|
|
68
|
+
*
|
|
69
|
+
* Features can be of three types. (Or that is the model we use in Stage, it seems to hold.)
|
|
70
|
+
*
|
|
71
|
+
* 1. SelectOne Features are like radio buttons, one Option is selected at a time.
|
|
72
|
+
* 2. SelectMany Features ("optional" in Catalogues) are like check boxes, any number of Options
|
|
73
|
+
* are selected at a time.
|
|
74
|
+
* 3. Group Features ("multiple" in Catalogues) act like a grouping mechanism where all the Options
|
|
75
|
+
* are permanently selected. No user interaction allowed.
|
|
76
|
+
*
|
|
77
|
+
* Group Features work like any other Feature without SyncGroups. They are just transparent.
|
|
78
|
+
*
|
|
79
|
+
* SelectOne and SelectMany have fully separated sync states. This means that there is never a sync
|
|
80
|
+
* connection between SelectOne and SelectMany Features even if they have the same Sync Group Code.
|
|
81
|
+
*
|
|
82
|
+
* In other words, SelectOne Features only sync with other SelectOne features and vice versa.
|
|
83
|
+
*
|
|
84
|
+
* On what level sync happens is different between SelectOne and SelectMany Features. For SelectOne
|
|
85
|
+
* we store an option code per SyncGroup. For SelectMany we store "On" or "Off" per option code per
|
|
86
|
+
* SyncGroup.
|
|
87
|
+
*
|
|
88
|
+
* You can say that SelectOne is synced on Feature level, and SelectMany is synced on Option level.
|
|
89
|
+
*
|
|
90
|
+
* Note that the same Feature (as in feature code) can exist in multiple places in a configuration
|
|
91
|
+
* tree. In this context, they are treated as individual Features that just happens to have an
|
|
92
|
+
* identical set of settings in the Catalogue.
|
|
93
|
+
*
|
|
94
|
+
*
|
|
95
|
+
* Transactions
|
|
96
|
+
* ------------
|
|
97
|
+
*
|
|
98
|
+
* Selecting an Option in a Feature can trigger a bunch of changes, each of them cascading into new
|
|
99
|
+
* features coming into scope and in turn changing other SyncGroups. As far as the user goes these
|
|
100
|
+
* changes is under the hood and is part of a single change triggered by the first selection.
|
|
101
|
+
*
|
|
102
|
+
* This is also how it is implemented. All the changes created by the initial selection are
|
|
103
|
+
* validated and propagated inside a single "transaction". The transaction is considered complete
|
|
104
|
+
* when the last round of changes did not trigger any new changes in the sync state.
|
|
105
|
+
*
|
|
106
|
+
* In every transaction, each SyncGroup is allowed to be updated only once for SelectOne features or
|
|
107
|
+
* once per unique option code in the case of SelectMany features. This is done to ensure that the
|
|
108
|
+
* propagated changes is guaranteed to stabilize over time and eliminates the risk of loops.
|
|
109
|
+
*
|
|
110
|
+
* Since a Feature can only have a single sync group code, this also means that a Feature will be
|
|
111
|
+
* changed at most once during a single transaction.
|
|
112
|
+
*
|
|
113
|
+
*
|
|
114
|
+
* Applying SyncGroups => Features/Options
|
|
115
|
+
* ---------------------------------------
|
|
116
|
+
*
|
|
117
|
+
* The state of the SyncGroups (as stored in the SyncState) is applied to a Feature/Option under
|
|
118
|
+
* the following conditions:
|
|
119
|
+
* A) The Feature belongs to a SyncGroup, i.e. has a Sync Group Code.
|
|
120
|
+
* B) The feature's sync mode is set to "pull" or "twoWay".
|
|
121
|
+
*
|
|
122
|
+
* ...for SelectOne:
|
|
123
|
+
* C) The SyncState has an option code for this SyncGroup.
|
|
124
|
+
* D) The option code in the SyncState for the SyncGroup is not the Option selected.
|
|
125
|
+
* E) The Feature has an Option with the right option code.
|
|
126
|
+
* F) The Feature has not previously been affected in this transaction.
|
|
127
|
+
*
|
|
128
|
+
* ...for SelectMany (done for every Option):
|
|
129
|
+
* C) The SyncState has a value (on or off) for this SyncGroup and option code.
|
|
130
|
+
* D) The Option is on when it should be off or the other way around.
|
|
131
|
+
* E) The Option has not previously been affected in this transaction.
|
|
132
|
+
*
|
|
133
|
+
* Applying the sync state typically happen when:
|
|
134
|
+
* - A user action on a Feature has changed the SyncState.
|
|
135
|
+
* - Features "comes into scope"
|
|
136
|
+
*
|
|
137
|
+
* A feature "coming into scope" refers features "appearing" during for example a Product load, a
|
|
138
|
+
* parent feature changing selected options to expose new children or an Additional Product coming
|
|
139
|
+
* into scope.
|
|
140
|
+
*
|
|
141
|
+
*
|
|
142
|
+
* Applying Features/Options => SyncGroups
|
|
143
|
+
* ---------------------------------------
|
|
144
|
+
*
|
|
145
|
+
* The Feature/Option is applied to the SyncState under the following conditions:
|
|
146
|
+
* A) The Feature belongs to a SyncGroup, i.e. has a Sync Group Code.
|
|
147
|
+
* B) The feature's sync mode is set to "push" or "twoWay".
|
|
148
|
+
*
|
|
149
|
+
* ...for SelectOne:
|
|
150
|
+
* C) The Option selected is not the option code currently in the SyncState for the SyncGroup.
|
|
151
|
+
* D) The SyncState has not been affected for this SyncGroup in this transaction.
|
|
152
|
+
* E) Any one of the below:
|
|
153
|
+
* - A user actively selects an Option.
|
|
154
|
+
* - There is no option code in the SyncState for this SyncGroup.
|
|
155
|
+
* - The Feature did just come into scope, and the option code in the SyncState for this
|
|
156
|
+
* SyncGroup is not one of the Options in the Feature. The option code is not in the Feature
|
|
157
|
+
* Domain for this Feature that is. So, a Feature appearing which cannot take it's selection
|
|
158
|
+
* from the SyncState will instead write it back.
|
|
159
|
+
*
|
|
160
|
+
* ...for SelectMany (for each Option):
|
|
161
|
+
* C) The SyncState has not been affected for this SyncGroup and option code in this transaction.
|
|
162
|
+
* D) Any one of the below:
|
|
163
|
+
* - A user actively selects or deselects an Option, and the on or off status differs from what
|
|
164
|
+
* is in the SyncState for the SyncGroup and option code.
|
|
165
|
+
* - There is no entry for SyncGroup and option code in the SyncState and the Option is
|
|
166
|
+
* selected. We only implicitly initialize the SyncState for "on" as there is no way to
|
|
167
|
+
* explicitly chose what is default of in Catalogues.
|
|
168
|
+
*
|
|
169
|
+
* This typically happens when:
|
|
170
|
+
* - A user selects or deselects an option.
|
|
171
|
+
* - Features comes into scope, like at Product load, a parent feature changing selected options to
|
|
172
|
+
* expose new children or an Additional Product coming into scope.
|
|
173
|
+
*
|
|
174
|
+
*
|
|
175
|
+
* Implementation Details
|
|
176
|
+
* ======================
|
|
177
|
+
*
|
|
178
|
+
* There are two entry points into the process. One for when a new Product is loaded, when we want
|
|
179
|
+
* to move it into a synced state. The other is when a user selects or deselects an Option.
|
|
180
|
+
*
|
|
181
|
+
* The process can be thought of as a state machine with three states or stages:
|
|
182
|
+
* A) Select/deselect Option and force to SyncState
|
|
183
|
+
* B) Apply the SyncState onto the Product
|
|
184
|
+
* C) Apply the Product onto the SyncState
|
|
185
|
+
*
|
|
186
|
+
* The entry point for a new Product is C. This makes sense as a new Product has no SyncState at
|
|
187
|
+
* all, so applying the Product onto the SyncState is good start.
|
|
188
|
+
*
|
|
189
|
+
* The entry point for user selection is A.
|
|
190
|
+
*
|
|
191
|
+
* Let's look at how we move between states starting with A.
|
|
192
|
+
*
|
|
193
|
+
* State A will apply the selection onto the Option. It will write to the SyncState. It will then
|
|
194
|
+
* transition to state B.
|
|
195
|
+
*
|
|
196
|
+
* State A is only run once for each user interaction, and not at all for Product Load.
|
|
197
|
+
*
|
|
198
|
+
* State B will apply the SyncState onto Features by recursively going through the configuration
|
|
199
|
+
* tree. Once it finds a Feature/Option which should be changed by the state it:
|
|
200
|
+
* - Checks if it can change it (it can't if the belongs to a so far uninitialized SyncGroup) and
|
|
201
|
+
* if it can then:
|
|
202
|
+
* - Changes the selection on the Feature.
|
|
203
|
+
* - Add the parent Product for the Feature to a list of Products for which a validate call
|
|
204
|
+
* must later be made.
|
|
205
|
+
* - Stop recursing down this branch as the validation call might actually change the structure
|
|
206
|
+
* and children in the branch.
|
|
207
|
+
*
|
|
208
|
+
* Once we have recursed the entire tree the result can be either:
|
|
209
|
+
* - There are Products to validate.
|
|
210
|
+
* Then we do the validations, and once they are done we move back into state B. We run it again
|
|
211
|
+
* as we did not recurse all the way down the branches that changed value. With each run, we will
|
|
212
|
+
* get further out on the branches until finally reaching the end.
|
|
213
|
+
* - There are no Products to validate.
|
|
214
|
+
* This means that there were no Features which we could apply the SyncState onto. We now move
|
|
215
|
+
* into state C as the selection changes may have brought new Features into scope, Features which
|
|
216
|
+
* might write to the SyncState.
|
|
217
|
+
*
|
|
218
|
+
* State C will apply Features onto the SyncState. It will recursively go through the configuration
|
|
219
|
+
* tree. When it's done one of two has happened:
|
|
220
|
+
* - There was a write to the SyncState.
|
|
221
|
+
* We then move back to state B, as this change in the SyncState could open up things in B. For
|
|
222
|
+
* example a Feature that could not pull from the SyncState before might be able to do that now.
|
|
223
|
+
* - There was no write to the SyncState.
|
|
224
|
+
* All is calm. We are done. The settling of the SyncState vs configuration is done. Things are
|
|
225
|
+
* as in sync as they will be. This is the exit.
|
|
226
|
+
*
|
|
227
|
+
*
|
|
228
|
+
* Side Notes
|
|
229
|
+
* ==========
|
|
230
|
+
*
|
|
231
|
+
* _CfgProductInternal, _CfgFeatureInternal and _CfgOptionInternal are used in Sets and Maps in the
|
|
232
|
+
* code. These objects are only created once for the data they represent. That is, even if a Feature
|
|
233
|
+
* goes in or out of scope the object is the same. This is not true for their wrapper classer
|
|
234
|
+
* CfgProduct, CfgFeature and CfgOption. Those are frequently replaced while running.
|
|
235
|
+
*
|
|
236
|
+
*/
|
|
237
|
+
/**
|
|
238
|
+
* Send to root for the passed option and any sibling which is selected, provided the Feature
|
|
239
|
+
* is SelectOne. (As that one (should only be one) can be assumed to be affected).
|
|
240
|
+
*/
|
|
241
|
+
function notifyOptionAndSelectedSiblings(option) {
|
|
242
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
243
|
+
const parentFeature = option.parent;
|
|
244
|
+
if (parentFeature.selectionType === SelectionType.SelectOne) {
|
|
245
|
+
// These only need to be OneLevel, as the final is ToRoot and they share their parent.
|
|
246
|
+
yield Promise.all(option.parent.selectedOptions.map((o) => parentFeature._childHasChanged(o._internal, ProductConfigurationBubbleMode.OneLevel)));
|
|
247
|
+
}
|
|
248
|
+
yield parentFeature._childHasChanged(option, ProductConfigurationBubbleMode.ToRoot);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
13
251
|
/**
|
|
14
252
|
* Is used to apply the SyncGroups functionality on the Configuration and the other
|
|
15
253
|
* way around. It also keeps the SyncState.
|
|
16
254
|
*/
|
|
17
255
|
export class SyncGroupsHandler {
|
|
18
|
-
constructor(_syncState, updateMode) {
|
|
256
|
+
constructor(_syncState, updateMode, _loadingObservable) {
|
|
19
257
|
this._syncState = _syncState;
|
|
20
258
|
this.updateMode = updateMode;
|
|
259
|
+
this._loadingObservable = _loadingObservable;
|
|
21
260
|
}
|
|
22
|
-
static make(updateMode = SyncGroupsApplyMode.Strict) {
|
|
23
|
-
return new SyncGroupsHandler(new SyncGroupsState(), updateMode);
|
|
261
|
+
static make(updateMode = SyncGroupsApplyMode.Strict, loadingObservable) {
|
|
262
|
+
return new SyncGroupsHandler(new SyncGroupsState(), updateMode, loadingObservable);
|
|
24
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* Please note that clones will use the same loadingObservable as their source
|
|
266
|
+
*/
|
|
25
267
|
clone() {
|
|
26
|
-
return new SyncGroupsHandler(this._syncState.clone(), this.updateMode);
|
|
268
|
+
return new SyncGroupsHandler(this._syncState.clone(), this.updateMode, this._loadingObservable);
|
|
27
269
|
}
|
|
28
270
|
/**
|
|
29
271
|
* Used to initially apply the sync state onto a new product so that it is "in sync"
|
|
@@ -31,41 +273,80 @@ export class SyncGroupsHandler {
|
|
|
31
273
|
init(product, productLoader) {
|
|
32
274
|
return __awaiter(this, void 0, void 0, function* () {
|
|
33
275
|
const transaction = yield this.newTransaction(product, productLoader, true);
|
|
34
|
-
|
|
35
|
-
|
|
276
|
+
try {
|
|
277
|
+
yield transaction.init();
|
|
278
|
+
yield this.applyTransaction(transaction);
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
this.closeTransaction(transaction);
|
|
282
|
+
}
|
|
36
283
|
});
|
|
37
284
|
}
|
|
38
285
|
/**
|
|
39
286
|
* Used when an Option is selected or deselected to apply all consequences of the sync groups.
|
|
40
287
|
* Can cause multiple extra validation calls to the server.
|
|
41
288
|
*/
|
|
42
|
-
selectOption(product,
|
|
289
|
+
selectOption(product, option, on, productLoader) {
|
|
43
290
|
return __awaiter(this, void 0, void 0, function* () {
|
|
44
291
|
//todo: should we guarantee that it will use root? Tricky...
|
|
292
|
+
yield this.setPending(option);
|
|
45
293
|
const transaction = yield this.newTransaction(product, productLoader, false);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
294
|
+
try {
|
|
295
|
+
const change = yield transaction.selectOption(SyncGroupsPathHelper.getPath(option), on);
|
|
296
|
+
// We always apply. The change-result above only tells if the configuration
|
|
297
|
+
// has changed. The SyncState may also have changed.
|
|
298
|
+
yield this.applyTransaction(transaction);
|
|
299
|
+
return change;
|
|
300
|
+
}
|
|
301
|
+
finally {
|
|
302
|
+
if (this._pending === option) {
|
|
303
|
+
yield this.setPending(undefined);
|
|
304
|
+
}
|
|
305
|
+
this.closeTransaction(transaction);
|
|
306
|
+
}
|
|
51
307
|
});
|
|
52
308
|
}
|
|
309
|
+
setPending(newPending) {
|
|
310
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
311
|
+
const oldPending = this._pending;
|
|
312
|
+
this._pending = newPending;
|
|
313
|
+
if (oldPending !== undefined) {
|
|
314
|
+
yield notifyOptionAndSelectedSiblings(oldPending);
|
|
315
|
+
}
|
|
316
|
+
if (newPending !== undefined) {
|
|
317
|
+
yield notifyOptionAndSelectedSiblings(newPending);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
get pending() {
|
|
322
|
+
return this._pending;
|
|
323
|
+
}
|
|
53
324
|
newTransaction(product, productLoader, assumeNoStartProductState) {
|
|
325
|
+
var _a;
|
|
54
326
|
return __awaiter(this, void 0, void 0, function* () {
|
|
55
327
|
if (this._currentTransaction !== undefined) {
|
|
56
|
-
this._currentTransaction
|
|
328
|
+
this.closeTransaction(this._currentTransaction);
|
|
57
329
|
}
|
|
58
|
-
|
|
59
|
-
|
|
330
|
+
const transaction = yield SyncGroupsTransaction.make(this._syncState, this.updateMode, product, productLoader, assumeNoStartProductState);
|
|
331
|
+
this._currentTransaction = transaction;
|
|
332
|
+
// The transaction object is used as loading token
|
|
333
|
+
(_a = this._loadingObservable) === null || _a === void 0 ? void 0 : _a.startChildLoading(transaction);
|
|
334
|
+
return transaction;
|
|
60
335
|
});
|
|
61
336
|
}
|
|
337
|
+
closeTransaction(transaction) {
|
|
338
|
+
var _a;
|
|
339
|
+
transaction.close();
|
|
340
|
+
(_a = this._loadingObservable) === null || _a === void 0 ? void 0 : _a.stopChildLoading(transaction);
|
|
341
|
+
}
|
|
62
342
|
applyTransaction(transaction) {
|
|
63
343
|
return __awaiter(this, void 0, void 0, function* () {
|
|
64
|
-
if (transaction.
|
|
344
|
+
if (transaction.isClosed) {
|
|
65
345
|
return;
|
|
66
346
|
}
|
|
67
|
-
this._syncState.
|
|
347
|
+
this._syncState.copyFrom(transaction.syncState);
|
|
68
348
|
yield transaction.original.copyFrom(transaction.target, false, transaction.productLoader);
|
|
349
|
+
this.closeTransaction(transaction);
|
|
69
350
|
});
|
|
70
351
|
}
|
|
71
352
|
}
|
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
import { OptionCode, SyncCode } from "./SyncGroupsHandler.js";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* The SyncState is used to keep track of the current value of the SyncGroups.
|
|
4
|
+
*
|
|
5
|
+
* SelectOne and SelectMany uses fully separate states internally since the two types of features
|
|
6
|
+
* are synced separately. See SyncGroupsHandler for details.
|
|
5
7
|
*/
|
|
6
8
|
export declare class SyncGroupsState {
|
|
7
9
|
readonly _selectOne: Map<SyncCode, OptionCode>;
|
|
8
10
|
readonly _selectMany: Map<SyncCode, Map<OptionCode, boolean>>;
|
|
11
|
+
/**
|
|
12
|
+
* @returns a deep copy of the SyncGroupState.
|
|
13
|
+
*/
|
|
9
14
|
clone(): SyncGroupsState;
|
|
10
15
|
/**
|
|
11
|
-
* Replaces the
|
|
16
|
+
* Replaces the internal state of this SyncGroupState with a deep copy of the one in source.
|
|
17
|
+
* @returns the updated SyncGroupState.
|
|
12
18
|
*/
|
|
13
|
-
|
|
19
|
+
copyFrom(source: SyncGroupsState): this;
|
|
14
20
|
setForSelectOne(syncCode: SyncCode, optionCode: OptionCode): void;
|
|
15
21
|
setForSelectMany(syncCode: SyncCode, optionCode: OptionCode, selected: boolean): void;
|
|
16
22
|
getForSelectOne(syncCode: SyncCode): OptionCode | undefined;
|
|
17
23
|
getForSelectMany(syncCode: SyncCode, optionCode: OptionCode): boolean | undefined;
|
|
18
|
-
|
|
24
|
+
log(syncCode?: SyncCode, optionCode?: OptionCode, selected?: boolean): void;
|
|
19
25
|
}
|
|
20
26
|
//# sourceMappingURL=SyncGroupsState.d.ts.map
|
|
@@ -1,46 +1,51 @@
|
|
|
1
|
+
/** Set to true to get verbose sync state changes logged to the console. */
|
|
2
|
+
const SYNCSTATE_VERBOSE = true; // TODO: Disable before merge
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
+
* The SyncState is used to keep track of the current value of the SyncGroups.
|
|
5
|
+
*
|
|
6
|
+
* SelectOne and SelectMany uses fully separate states internally since the two types of features
|
|
7
|
+
* are synced separately. See SyncGroupsHandler for details.
|
|
4
8
|
*/
|
|
5
9
|
export class SyncGroupsState {
|
|
6
10
|
constructor() {
|
|
7
11
|
this._selectOne = new Map();
|
|
8
12
|
this._selectMany = new Map();
|
|
9
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* @returns a deep copy of the SyncGroupState.
|
|
16
|
+
*/
|
|
10
17
|
clone() {
|
|
11
|
-
|
|
12
|
-
result.setFrom(this);
|
|
13
|
-
return result;
|
|
18
|
+
return new SyncGroupsState().copyFrom(this);
|
|
14
19
|
}
|
|
15
20
|
/**
|
|
16
|
-
* Replaces the
|
|
21
|
+
* Replaces the internal state of this SyncGroupState with a deep copy of the one in source.
|
|
22
|
+
* @returns the updated SyncGroupState.
|
|
17
23
|
*/
|
|
18
|
-
|
|
24
|
+
copyFrom(source) {
|
|
19
25
|
this._selectOne.clear();
|
|
20
26
|
this._selectMany.clear();
|
|
21
|
-
|
|
22
|
-
|
|
27
|
+
let entryOne; // Ensures OptionCode is a primitive
|
|
28
|
+
for (entryOne of source._selectOne) {
|
|
29
|
+
this._selectOne.set(...entryOne);
|
|
23
30
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
targetOptionToSelected.set(sourceOptionCode, sourceIsSelected);
|
|
28
|
-
}
|
|
29
|
-
this._selectMany.set(sourceSyncCode, targetOptionToSelected);
|
|
31
|
+
let entryMany;
|
|
32
|
+
for (entryMany of source._selectMany) {
|
|
33
|
+
this._selectMany.set(entryMany[0], new Map(entryMany[1]));
|
|
30
34
|
}
|
|
35
|
+
return this;
|
|
31
36
|
}
|
|
32
37
|
setForSelectOne(syncCode, optionCode) {
|
|
33
38
|
this._selectOne.set(syncCode, optionCode);
|
|
34
|
-
this.
|
|
39
|
+
this.log(syncCode, optionCode);
|
|
35
40
|
}
|
|
36
41
|
setForSelectMany(syncCode, optionCode, selected) {
|
|
37
|
-
let
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
this._selectMany.set(syncCode,
|
|
42
|
+
let entry = this._selectMany.get(syncCode);
|
|
43
|
+
if (entry === undefined) {
|
|
44
|
+
entry = new Map();
|
|
45
|
+
this._selectMany.set(syncCode, entry);
|
|
41
46
|
}
|
|
42
|
-
|
|
43
|
-
this.
|
|
47
|
+
entry.set(optionCode, selected);
|
|
48
|
+
this.log(syncCode, optionCode, selected);
|
|
44
49
|
}
|
|
45
50
|
getForSelectOne(syncCode) {
|
|
46
51
|
return this._selectOne.get(syncCode);
|
|
@@ -49,7 +54,9 @@ export class SyncGroupsState {
|
|
|
49
54
|
var _a;
|
|
50
55
|
return (_a = this._selectMany.get(syncCode)) === null || _a === void 0 ? void 0 : _a.get(optionCode);
|
|
51
56
|
}
|
|
52
|
-
|
|
57
|
+
log(syncCode, optionCode, selected) {
|
|
58
|
+
if (!SYNCSTATE_VERBOSE)
|
|
59
|
+
return;
|
|
53
60
|
const isMany = selected !== undefined;
|
|
54
61
|
const selectOne = Array.from(this._selectOne.entries());
|
|
55
62
|
const selectMany = Array.from(this._selectMany.entries()).reduce((a, [groupCode, optionCodeToSelected]) => {
|
|
@@ -66,43 +73,39 @@ export class SyncGroupsState {
|
|
|
66
73
|
columnWidth = Math.max(columnWidth, e[0].length);
|
|
67
74
|
});
|
|
68
75
|
const padding = Array(columnWidth).join(" ");
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const styleOff = "color:red";
|
|
76
|
-
const output = [];
|
|
77
|
-
if (group === undefined) {
|
|
78
|
-
output.push("");
|
|
76
|
+
const bold = "font-weight: bold";
|
|
77
|
+
const styles = [];
|
|
78
|
+
let msg = "";
|
|
79
|
+
if (syncCode !== undefined && isMany) {
|
|
80
|
+
msg += `%c${optionCode}%c in %c${syncCode}%c set to %c${selected ? "on" : "off"}`;
|
|
81
|
+
styles.push(bold, "", bold, "", bold);
|
|
79
82
|
}
|
|
80
|
-
else if (isMany) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
styleBoldThis,
|
|
84
|
-
"",
|
|
85
|
-
styleBoldThis,
|
|
86
|
-
"",
|
|
87
|
-
selected ? styleBoldOn : styleBoldOff,
|
|
88
|
-
]);
|
|
83
|
+
else if (syncCode !== undefined && !isMany) {
|
|
84
|
+
msg += `%c${syncCode}%c set to %c${optionCode}`;
|
|
85
|
+
styles.push(bold, "", bold);
|
|
89
86
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
output[0] = output[0] + "%cSync State (single)";
|
|
94
|
-
output.push(styleBold);
|
|
87
|
+
msg += "\n\n%cSync State (single)";
|
|
88
|
+
styles.push(bold);
|
|
95
89
|
selectOne.forEach((e) => {
|
|
96
|
-
|
|
97
|
-
|
|
90
|
+
const match = !isMany && syncCode === e[0];
|
|
91
|
+
msg += `\n%c${match ? "*" : " "} ${(padding + e[0]).slice(-columnWidth)}: ${e[1]}`;
|
|
92
|
+
styles.push(match ? bold : "");
|
|
98
93
|
});
|
|
99
|
-
|
|
100
|
-
|
|
94
|
+
if (selectOne.length === 0) {
|
|
95
|
+
msg += "\n%c <Empty>";
|
|
96
|
+
styles.push("");
|
|
97
|
+
}
|
|
98
|
+
msg += "\n\n%cSync State (multi)";
|
|
99
|
+
styles.push(bold);
|
|
101
100
|
selectMany.forEach((e) => {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
const match = isMany && syncCode === e[0] && optionCode === e[1];
|
|
102
|
+
msg += `\n%c${match ? "*" : " "} ${(padding + e[0]).slice(-columnWidth)}: ${e[2] === true ? "\u2705" : "\u274c"} ${e[1]}`;
|
|
103
|
+
styles.push(match ? bold : "");
|
|
105
104
|
});
|
|
106
|
-
|
|
105
|
+
if (selectMany.length === 0) {
|
|
106
|
+
msg += "\n%c <Empty>";
|
|
107
|
+
styles.push("");
|
|
108
|
+
}
|
|
109
|
+
console.log(msg, ...styles);
|
|
107
110
|
}
|
|
108
111
|
}
|
|
@@ -22,21 +22,21 @@ export declare class SyncGroupsTransaction {
|
|
|
22
22
|
static make(syncState: SyncGroupsState, updateMode: SyncGroupsApplyMode, product: _CfgProductInternal, productLoader: ProductLoader, assumeNoStartState: boolean): Promise<SyncGroupsTransaction>;
|
|
23
23
|
/**
|
|
24
24
|
*
|
|
25
|
-
* @param syncState A clone of the original syncState. Replaces the original syncState if nothing fails and the transaction doesn't get
|
|
25
|
+
* @param syncState A clone of the original syncState. Replaces the original syncState if nothing fails and the transaction doesn't get cancelled
|
|
26
26
|
* @param updateMode
|
|
27
27
|
* @param productLoader
|
|
28
|
-
* @param original The product instance that this transaction will be applied on provided nothing fails and the transaction doesn't get
|
|
28
|
+
* @param original The product instance that this transaction will be applied on provided nothing fails and the transaction doesn't get cancelled
|
|
29
29
|
* @param target A clone of the original product used to apply the configuration changes to
|
|
30
30
|
* @param initial A clone of the original product used to track what the original state was. As a safe measure we do not use originalProduct for this, as it might be changed by someone else
|
|
31
31
|
*/
|
|
32
32
|
private constructor();
|
|
33
|
-
private
|
|
33
|
+
private _closed;
|
|
34
34
|
private affectedSelectOneFeatures;
|
|
35
35
|
private affectedSelectManyOptions;
|
|
36
36
|
private affectedSelectOneSyncGroups;
|
|
37
37
|
private affectedSelectManySyncGroupsAndOptions;
|
|
38
|
-
get
|
|
39
|
-
|
|
38
|
+
get isClosed(): boolean;
|
|
39
|
+
close(): void;
|
|
40
40
|
init(): Promise<boolean>;
|
|
41
41
|
selectOption(optionPath: CfgPath, on: boolean): Promise<boolean>;
|
|
42
42
|
addSelectOneFeatureAffected(feature: _CfgFeatureInternal): void;
|
|
@@ -20,10 +20,10 @@ import { SyncGroupsPathHelper } from "./SyncGroupsPathHelper.js";
|
|
|
20
20
|
export class SyncGroupsTransaction {
|
|
21
21
|
/**
|
|
22
22
|
*
|
|
23
|
-
* @param syncState A clone of the original syncState. Replaces the original syncState if nothing fails and the transaction doesn't get
|
|
23
|
+
* @param syncState A clone of the original syncState. Replaces the original syncState if nothing fails and the transaction doesn't get cancelled
|
|
24
24
|
* @param updateMode
|
|
25
25
|
* @param productLoader
|
|
26
|
-
* @param original The product instance that this transaction will be applied on provided nothing fails and the transaction doesn't get
|
|
26
|
+
* @param original The product instance that this transaction will be applied on provided nothing fails and the transaction doesn't get cancelled
|
|
27
27
|
* @param target A clone of the original product used to apply the configuration changes to
|
|
28
28
|
* @param initial A clone of the original product used to track what the original state was. As a safe measure we do not use originalProduct for this, as it might be changed by someone else
|
|
29
29
|
*/
|
|
@@ -34,7 +34,7 @@ export class SyncGroupsTransaction {
|
|
|
34
34
|
this.original = original;
|
|
35
35
|
this.target = target;
|
|
36
36
|
this.initial = initial;
|
|
37
|
-
this.
|
|
37
|
+
this._closed = false;
|
|
38
38
|
this.affectedSelectOneFeatures = new Set();
|
|
39
39
|
this.affectedSelectManyOptions = new Set();
|
|
40
40
|
this.affectedSelectOneSyncGroups = new Set();
|
|
@@ -46,11 +46,11 @@ export class SyncGroupsTransaction {
|
|
|
46
46
|
return t;
|
|
47
47
|
});
|
|
48
48
|
}
|
|
49
|
-
get
|
|
50
|
-
return this.
|
|
49
|
+
get isClosed() {
|
|
50
|
+
return this._closed;
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
this.
|
|
52
|
+
close() {
|
|
53
|
+
this._closed = true;
|
|
54
54
|
}
|
|
55
55
|
init() {
|
|
56
56
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -2,14 +2,16 @@ import { shallowCompareDictionaries } from "@configura/web-utilities";
|
|
|
2
2
|
export const makeCatalogueKey = (cat) => `${cat.enterprise}-${cat.prdCat}-${cat.prdCatVersion}-${cat.priceList}-${cat.vendor}`;
|
|
3
3
|
export const makeProductKey = (cat, pKey) => `${makeCatalogueKey(cat)}-${pKey}`;
|
|
4
4
|
export const makeSelOptionsKey = (options) => options.reduce((p, option) => {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
var _a;
|
|
6
|
+
const { code, numericValue, next } = option;
|
|
7
|
+
p += `_{${code}`;
|
|
8
|
+
p += numericValue === undefined ? "" : `_${numericValue.value}${(_a = numericValue.unit) !== null && _a !== void 0 ? _a : ""}`;
|
|
7
9
|
if (next === undefined) {
|
|
8
10
|
return p;
|
|
9
11
|
}
|
|
10
12
|
for (const key of Object.keys(next)) {
|
|
11
13
|
const innerOption = next[key];
|
|
12
|
-
p +=
|
|
14
|
+
p += `_{${key}_${makeSelOptionsKey([innerOption])}_}`;
|
|
13
15
|
}
|
|
14
16
|
p += "_}";
|
|
15
17
|
return p;
|
|
@@ -130,10 +132,10 @@ export function isSameProductRef(left, right) {
|
|
|
130
132
|
if (left.refKey !== right.refKey) {
|
|
131
133
|
return false;
|
|
132
134
|
}
|
|
133
|
-
if (
|
|
135
|
+
if (left.partNumber !== right.partNumber) {
|
|
134
136
|
return false;
|
|
135
137
|
}
|
|
136
|
-
if (left.
|
|
138
|
+
if (!isSameCatalogueParams(left.catId, right.catId)) {
|
|
137
139
|
return false;
|
|
138
140
|
}
|
|
139
141
|
if (left.refDescription !== right.refDescription) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@configura/web-api",
|
|
3
|
-
"version": "1.6.1-alpha.
|
|
3
|
+
"version": "1.6.1-alpha.2",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"access": "public"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@configura/web-utilities": "
|
|
26
|
+
"@configura/web-utilities": "1.6.1-alpha.2"
|
|
27
27
|
},
|
|
28
|
-
"gitHead": "
|
|
28
|
+
"gitHead": "e4b98f07293de3363e402a343194a9931d00732a"
|
|
29
29
|
}
|