@configura/web-api 1.6.1-alpha.1 → 1.6.1-alpha.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.
@@ -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
  *
@@ -64,10 +57,6 @@ export declare type CfgPrice = {
64
57
  currency: string;
65
58
  fractionDigits: number;
66
59
  };
67
- export declare type RevalidateResult = {
68
- wasAborted: boolean;
69
- requestDidValidate: boolean;
70
- };
71
60
  /**
72
61
  * This class is meant to only be used through CfgProduct. It should never be instantiated on its
73
62
  * own. Normally the internal state of this class should never be directly modified. CfgProduct is
@@ -83,13 +72,10 @@ export declare class _CfgProductInternal {
83
72
  private readonly _rawUnit;
84
73
  private _rawProductData;
85
74
  readonly loadingObservable: AggregatedLoadingObservable;
86
- readonly refKey: string | undefined;
87
- readonly refDescription: string | undefined;
88
75
  readonly parent: _CfgProductInternal | undefined;
89
- readonly transform: Transform | undefined;
90
- anchor: MeasureParam | undefined;
76
+ private _additionalProductRef;
91
77
  private readonly _syncGroupHandler;
92
- static make: (productLoaderRaw: ProductLoader, productLoaderForGroupedLoad: ProductLoader | undefined, lang: string, catId: CatalogueParams, partNumber: string, settings: CfgProductSettings, optional: boolean, loadingObservable: AggregatedLoadingObservable, refKey: string | undefined, refDescription: string | undefined, parent: _CfgProductInternal | undefined, root: _CfgProductInternal | undefined, transform: Transform | undefined, anchor: MeasureParam | undefined) => Promise<_CfgProductInternal>;
78
+ 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
79
  private constructor();
94
80
  readonly root: _CfgProductInternal;
95
81
  private _destroyed;
@@ -102,6 +88,7 @@ export declare class _CfgProductInternal {
102
88
  readonly isAdditionalProduct: boolean;
103
89
  clone(parent?: _CfgProductInternal, root?: _CfgProductInternal): Promise<_CfgProductInternal>;
104
90
  destroy: () => void;
91
+ _updateAdditionalProdRef(p: AdditionalProductRef): void;
105
92
  get description(): string | undefined;
106
93
  get rootNodeSources(): RootNodeSource[] | undefined;
107
94
  get mtrlApplications(): MtrlApplication[] | undefined;
@@ -111,6 +98,9 @@ export declare class _CfgProductInternal {
111
98
  private _measureDefinitions;
112
99
  get measureDefinitions(): CfgMeasureDefinition[];
113
100
  private _unit;
101
+ get refKey(): string | undefined;
102
+ get transform(): Transform | undefined;
103
+ get anchor(): MeasureParam | undefined;
114
104
  /** @throws an error if the actual unit sent by the server was not a LengthUnit */
115
105
  get unit(): LengthUnit;
116
106
  get aggregatedPrice(): CfgPrice;
@@ -142,7 +132,7 @@ export declare class _CfgProductInternal {
142
132
  _configurationHasChanged: (freshRef: CfgProductConfiguration, bubbleMode: ProductConfigurationBubbleMode) => Promise<void>;
143
133
  getApiSelection: () => AdditionalProductConfiguration;
144
134
  setApiSelection: (s: AdditionalProductConfiguration, doValidate: boolean, productLoaderForGroupedLoad?: ProductLoader | undefined) => Promise<boolean>;
145
- copyFrom: (otherProduct: _CfgProductInternal, doValidate: boolean, productLoaderForGroupedLoad?: ProductLoader | undefined) => Promise<boolean>;
135
+ copyFrom: (source: _CfgProductInternal, doValidate: boolean, productLoaderForGroupedLoad?: ProductLoader | undefined) => Promise<boolean>;
146
136
  private _setApiSelectionWithOtherProduct;
147
137
  get syncGroupHandler(): SyncGroupsHandler | undefined;
148
138
  structureCompare: (other: _CfgProductInternal, strictOrder?: boolean, descriptionMatch?: boolean) => boolean;
@@ -155,7 +145,7 @@ export declare class _CfgProductInternal {
155
145
  * product in isolation. The validation result is applied on the configuration. Then additional
156
146
  * products are synced (unloaded, loaded etc.) Finally the changes bubble up the tree.
157
147
  */
158
- _revalidate: (bubbleMode: CfgProductBubbleMode, productLoader: ProductLoader) => Promise<RevalidateResult>;
148
+ _revalidate: (bubbleMode: CfgProductBubbleMode, productLoader: ProductLoader) => Promise<boolean>;
159
149
  /**
160
150
  * Based on this configuration find what additional products should be shown and not, unload
161
151
  * (i.e. destroy) those that should no longer be shown, load the new ones.
@@ -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, _b;
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, refKey, refDescription, parent, root, transform, anchor, _syncGroupHandler) {
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.transform = transform;
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 = (otherProduct, doValidate, productLoaderForGroupedLoad) => __awaiter(this, void 0, void 0, function* () {
157
- return yield this._setApiSelectionWithOtherProduct(otherProduct.getApiSelection(), doValidate, productLoaderForGroupedLoad, otherProduct);
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
@@ -296,7 +293,6 @@ export class _CfgProductInternal {
296
293
  this._revalidateInProgressToken = token;
297
294
  try {
298
295
  const response = yield productLoader.postValidate(Object.assign(Object.assign({ lang: this.lang }, correctDefaultsOnCatalogueParams(this.catId)), { partNumber: this.partNumber }), { selOptions: configuration.getApiSelection() });
299
- const requestDidValidate = response.validated;
300
296
  // The revalidateInProgressToken is used to know if some other revalidate
301
297
  // call has happened after this call, thereby making this call obsolete.
302
298
  // This is a bit crude in that it does not actually cancel previous validate
@@ -305,7 +301,7 @@ export class _CfgProductInternal {
305
301
  // of all, the heavy work happens on the server, and that work will not be
306
302
  // cancelled even if we would cancel the call.
307
303
  if (this._revalidateInProgressToken !== token) {
308
- return { wasAborted: true, requestDidValidate };
304
+ return false;
309
305
  }
310
306
  // After a successful validate-call we will always assume there
311
307
  // is a change. It would be possible to compare relevant parts
@@ -313,7 +309,7 @@ export class _CfgProductInternal {
313
309
  // syndAndLoad, however the code comparing productData would be fragile
314
310
  // and likely to break if new data-fields were added.
315
311
  if (this._destroyed) {
316
- return { wasAborted: true, requestDidValidate };
312
+ return false;
317
313
  }
318
314
  const { productData, rootFeatureRefs } = response;
319
315
  const pricesUpdated = !comparePricesObjects(this.prices, productData.partsData.prices);
@@ -328,10 +324,10 @@ export class _CfgProductInternal {
328
324
  yield configuration._internal.setApiSelection(productData.partsData.selOptions || [], false);
329
325
  yield this._syncAndLoadAdditionalProducts(productLoader);
330
326
  if (this._destroyed) {
331
- return { wasAborted: true, requestDidValidate };
327
+ return false;
332
328
  }
333
329
  yield this._notifyAllOfChange(bubbleMode);
334
- return { wasAborted: false, requestDidValidate };
330
+ return true;
335
331
  }
336
332
  catch (e) {
337
333
  throw e;
@@ -364,14 +360,19 @@ export class _CfgProductInternal {
364
360
  let i = currentAdditionalProducts.length;
365
361
  while (i--) {
366
362
  const currentAdditionalProduct = currentAdditionalProducts[i];
367
- const refKey = currentAdditionalProduct.refKey;
368
- const j = additionalProductRefs.findIndex((p) => p.prodRef.refKey === refKey);
363
+ const j = additionalProductRefs.findIndex((p) => {
364
+ const prodRef = p.prodRef;
365
+ return (prodRef.refKey === currentAdditionalProduct.refKey &&
366
+ prodRef.partNumber === currentAdditionalProduct.partNumber &&
367
+ isSameCatalogueParams(prodRef.catId, currentAdditionalProduct.catId));
368
+ });
369
369
  if (j === -1) {
370
370
  currentAdditionalProduct.destroy();
371
371
  currentAdditionalProducts.splice(i, 1);
372
372
  change = true;
373
373
  }
374
374
  else {
375
+ currentAdditionalProduct._internal._updateAdditionalProdRef(additionalProductRefs[j].prodRef);
375
376
  additionalProductRefs.splice(j, 1);
376
377
  }
377
378
  }
@@ -384,7 +385,7 @@ export class _CfgProductInternal {
384
385
  const additionalProductRef = p.prodRef;
385
386
  return {
386
387
  originalIndex: p.originalIndex,
387
- product: CfgProduct._makeNewRefFrom(yield _CfgProductInternal.make(productLoaderRaw, productLoaderForGroupedLoad, lang, additionalProductRef.catId, additionalProductRef.partNumber, this.settings, additionalProductRef.optional === true, this.loadingObservable, additionalProductRef.refKey, additionalProductRef.refDescription, this, this.root, additionalProductRef.transform, additionalProductRef.anchor)),
388
+ 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
389
  };
389
390
  }))()));
390
391
  if (this._destroyed) {
@@ -403,7 +404,7 @@ export class _CfgProductInternal {
403
404
  }
404
405
  });
405
406
  this.root = root !== null && root !== void 0 ? root : this;
406
- this.key = makeProductKey(catId, refKey || partNumber);
407
+ this.key = makeProductKey(catId, (_a = _additionalProductRef === null || _additionalProductRef === void 0 ? void 0 : _additionalProductRef.refKey) !== null && _a !== void 0 ? _a : partNumber);
407
408
  this._selected = optional ? selected : undefined;
408
409
  this.isAdditionalProduct = parent !== undefined;
409
410
  this._configuration = CfgProductConfiguration.make(initSuccess, initFail, rootFeatureRefs, allRawFeatures, apiSelection, this, this.root);
@@ -417,7 +418,7 @@ export class _CfgProductInternal {
417
418
  var _a;
418
419
  const p = new _CfgProductInternal(() => {
419
420
  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(), this.refKey, this.refDescription, parent, root, this.transform, this.anchor, (_a = this._syncGroupHandler) === null || _a === void 0 ? void 0 : _a.clone());
421
+ }, 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
422
  });
422
423
  for (const additionalProduct of this.additionalProducts) {
423
424
  product.additionalProducts.push(CfgProduct._makeNewRefFrom(yield additionalProduct._internal.clone(product, root || product)));
@@ -425,8 +426,15 @@ export class _CfgProductInternal {
425
426
  return product;
426
427
  });
427
428
  }
429
+ _updateAdditionalProdRef(p) {
430
+ this._additionalProductRef = p;
431
+ if (p.optional !== this.optional) {
432
+ this._selected = p.optional ? false : undefined;
433
+ }
434
+ }
428
435
  get description() {
429
- return this.refDescription || this._rawProductData.description;
436
+ var _a, _b;
437
+ return (_b = (_a = this._additionalProductRef) === null || _a === void 0 ? void 0 : _a.refDescription) !== null && _b !== void 0 ? _b : this._rawProductData.description;
430
438
  }
431
439
  get rootNodeSources() {
432
440
  return this._rawProductData.models;
@@ -452,6 +460,18 @@ export class _CfgProductInternal {
452
460
  }
453
461
  return this._measureDefinitions;
454
462
  }
463
+ get refKey() {
464
+ var _a;
465
+ return (_a = this._additionalProductRef) === null || _a === void 0 ? void 0 : _a.refKey;
466
+ }
467
+ get transform() {
468
+ var _a;
469
+ return (_a = this._additionalProductRef) === null || _a === void 0 ? void 0 : _a.transform;
470
+ }
471
+ get anchor() {
472
+ var _a;
473
+ return (_a = this._additionalProductRef) === null || _a === void 0 ? void 0 : _a.anchor;
474
+ }
455
475
  /** @throws an error if the actual unit sent by the server was not a LengthUnit */
456
476
  get unit() {
457
477
  if (this._unit === undefined) {
@@ -534,13 +554,15 @@ export class _CfgProductInternal {
534
554
  }
535
555
  }
536
556
  _CfgProductInternal.make = (productLoaderRaw, productLoaderForGroupedLoad, // Used when instantiating the current product
537
- lang, catId, partNumber, settings, optional, loadingObservable, refKey, refDescription, parent, root, transform, anchor) => __awaiter(void 0, void 0, void 0, function* () {
557
+ lang, catId, partNumber, settings, optional, loadingObservable, parent, root, additionalProductRef) => __awaiter(void 0, void 0, void 0, function* () {
538
558
  // Wrap with cache will make getProduct for this function call use the same server call
539
559
  // for the same product with the same params. Not retained for future calls, only used
540
560
  // at this initial load.
541
561
  productLoaderForGroupedLoad =
542
562
  productLoaderForGroupedLoad || wrapWithCache(productLoaderRaw);
543
- const syncGroupHandler = root === undefined ? SyncGroupsHandler.make(settings.syncGroupsApplyMode) : undefined;
563
+ const syncGroupHandler = root === undefined
564
+ ? SyncGroupsHandler.make(settings.syncGroupsApplyMode, loadingObservable)
565
+ : undefined;
544
566
  const productResponse = yield productLoaderForGroupedLoad.getProduct(Object.assign(Object.assign({ lang }, correctDefaultsOnCatalogueParams(catId)), { partNumber }));
545
567
  const { productData, rootFeatureRefs, features: allRawFeatures, uuid, unit, } = productResponse;
546
568
  const product = yield new Promise((initSuccess, initFail) => {
@@ -549,7 +571,7 @@ lang, catId, partNumber, settings, optional, loadingObservable, refKey, refDescr
549
571
  // But we can not set the api selection synchronously. And the product configuration needs "this". So we use this callback.
550
572
  // Feel free to find a nicer more readable solution :)
551
573
  initSuccess(p);
552
- }, initFail, productLoaderRaw, lang, catId, partNumber, settings, optional, !optional, rootFeatureRefs, allRawFeatures, uuid, unit, productData, productData.partsData.selOptions || [], loadingObservable, refKey, refDescription, parent, root, transform, anchor, syncGroupHandler);
574
+ }, initFail, productLoaderRaw, lang, catId, partNumber, settings, optional, !optional, rootFeatureRefs, allRawFeatures, uuid, unit, productData, productData.partsData.selOptions || [], loadingObservable, parent, root, additionalProductRef, syncGroupHandler);
553
575
  });
554
576
  yield product._syncAndLoadAdditionalProducts(productLoaderForGroupedLoad);
555
577
  // Product is guaranteed to be root
@@ -604,7 +626,7 @@ export class CfgProduct {
604
626
  }
605
627
  static make(productLoader, lang, catId, partNumber, settings) {
606
628
  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, undefined, undefined, undefined));
629
+ return this._makeNewRefFrom(yield _CfgProductInternal.make(productLoader, undefined, lang, catId, partNumber, completeSettings(settings), false, new AggregatedLoadingObservable(), undefined, undefined, undefined));
608
630
  });
609
631
  }
610
632
  /**
@@ -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, SyncGroupsPathHelper.getPath(optionInternal), on, wrapWithCache(product._productLoaderRaw));
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
- return newLevels.sort((l1, l2) => l1.description.toLocaleLowerCase().localeCompare(l2.description.toLocaleLowerCase()));
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.isAborted) {
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.
@@ -99,14 +99,8 @@ class SyncStateOnto {
99
99
  promises.push(product._revalidate(CfgProductBubbleMode.ToRoot, transaction.productLoader));
100
100
  }
101
101
  const revalidationResults = yield Promise.all(promises);
102
- // When using SyncGroups we require each and every validate call to validate on the
103
- // server. Without SyncGroups we let these pass through, because any diversion from
104
- // what you did chose should be immediately visible. With SyncGroups the end result
105
- // is the combination of potentially many different validate calls, and so errors
106
- // could accumulate and what is cause and effect be hard to know.
107
- if (!revalidationResults.every((r) => r.requestDidValidate) ||
108
- revalidationResults.every((r) => r.wasAborted)) {
109
- transaction.abort();
102
+ if (revalidationResults.every((r) => !r)) {
103
+ transaction.close();
110
104
  return false;
111
105
  }
112
106
  // 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, optionPath: CfgPath, on: boolean, productLoader: ProductLoader): Promise<boolean>;
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
- yield transaction.init();
35
- yield this.applyTransaction(transaction);
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, optionPath, on, productLoader) {
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
- const change = yield transaction.selectOption(optionPath, on);
47
- // We always apply. The change-result above only tells if the configuration
48
- // has changed. The SyncState may also have changed.
49
- yield this.applyTransaction(transaction);
50
- return change;
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.abort();
328
+ this.closeTransaction(this._currentTransaction);
57
329
  }
58
- this._currentTransaction = yield SyncGroupsTransaction.make(this._syncState, this.updateMode, product, productLoader, assumeNoStartProductState);
59
- return this._currentTransaction;
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.isAborted) {
344
+ if (transaction.isClosed) {
65
345
  return;
66
346
  }
67
- this._syncState.setFrom(transaction.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
- * Is used to keep track of the current value of the SyncGroups. Is fully separated between
4
- * SelectOne and SelectMany as Features of the two types are synced separately.
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 current state
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
- setFrom(other: SyncGroupsState): void;
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
- logDebug(group?: string, code?: string, selected?: boolean): void;
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
- * Is used to keep track of the current value of the SyncGroups. Is fully separated between
3
- * SelectOne and SelectMany as Features of the two types are synced separately.
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
- const result = new SyncGroupsState();
12
- result.setFrom(this);
13
- return result;
18
+ return new SyncGroupsState().copyFrom(this);
14
19
  }
15
20
  /**
16
- * Replaces the current state
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
- setFrom(other) {
24
+ copyFrom(source) {
19
25
  this._selectOne.clear();
20
26
  this._selectMany.clear();
21
- for (const [k, v] of other._selectOne) {
22
- this._selectOne.set(k, v);
27
+ let entryOne; // Ensures OptionCode is a primitive
28
+ for (entryOne of source._selectOne) {
29
+ this._selectOne.set(...entryOne);
23
30
  }
24
- for (const [sourceSyncCode, sourceOptionToSelected] of other._selectMany) {
25
- const targetOptionToSelected = new Map();
26
- for (const [sourceOptionCode, sourceIsSelected] of sourceOptionToSelected) {
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.logDebug(syncCode, optionCode);
39
+ this.log(syncCode, optionCode);
35
40
  }
36
41
  setForSelectMany(syncCode, optionCode, selected) {
37
- let forSyncCode = this._selectMany.get(syncCode);
38
- if (forSyncCode === undefined) {
39
- forSyncCode = new Map();
40
- this._selectMany.set(syncCode, forSyncCode);
42
+ let entry = this._selectMany.get(syncCode);
43
+ if (entry === undefined) {
44
+ entry = new Map();
45
+ this._selectMany.set(syncCode, entry);
41
46
  }
42
- forSyncCode.set(optionCode, selected);
43
- this.logDebug(syncCode, optionCode, selected);
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
- logDebug(group, code, selected) {
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 styleBold = "font-weight: bold";
70
- const styleBoldThis = "color:blue; font-weight: bold";
71
- const styleBoldOn = "color:green; font-weight: bold";
72
- const styleBoldOff = "color:red; font-weight: bold";
73
- const styleThis = "color:blue";
74
- const styleOn = "color:green";
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
- output.push(...[
82
- `%c${code} %cin %c${group} %cset to %c${selected ? "on" : "off"}\n`,
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
- else {
91
- output.push(...[`%c${group}%c set to %c${code}\n`, styleBoldThis, "", styleBoldThis]);
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
- output[0] = output[0] + `\n %c${(padding + e[0]).slice(-columnWidth)}: ${e[1]}`;
97
- output.push(!isMany && group === e[0] ? styleThis : "");
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
- output[0] = output[0] + "\n%cSync State (multi)";
100
- output.push(styleBold);
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
- output[0] = output[0] + `\n %c${(padding + e[0]).slice(-columnWidth)}: %c${e[1]}`;
103
- output.push(isMany && group === e[0] ? styleThis : "");
104
- output.push(e[2] === true ? styleOn : styleOff);
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
- console.log(...output);
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 aborted
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 aborted
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 _aborted;
33
+ private _closed;
34
34
  private affectedSelectOneFeatures;
35
35
  private affectedSelectManyOptions;
36
36
  private affectedSelectOneSyncGroups;
37
37
  private affectedSelectManySyncGroupsAndOptions;
38
- get isAborted(): boolean;
39
- abort(): void;
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 aborted
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 aborted
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._aborted = false;
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 isAborted() {
50
- return this._aborted;
49
+ get isClosed() {
50
+ return this._closed;
51
51
  }
52
- abort() {
53
- this._aborted = true;
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
- const { code, next } = option;
6
- p += "_{" + code;
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 += "_{" + key + "_" + makeSelOptionsKey([innerOption]) + "_}";
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 (!isSameCatalogueParams(left.catId, right.catId)) {
135
+ if (left.partNumber !== right.partNumber) {
134
136
  return false;
135
137
  }
136
- if (left.partNumber !== right.partNumber) {
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.1",
3
+ "version": "1.6.1-alpha.5",
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": "^1.6.1-alpha.1"
26
+ "@configura/web-utilities": "1.6.1-alpha.5"
27
27
  },
28
- "gitHead": "afaf1e342f0648246a1dcb6f7bc636d4d1979bec"
28
+ "gitHead": "6fba3fe5dd83d77fb9d6af90389a6c46b7bbf161"
29
29
  }