@configura/web-api 1.3.0-alpha.0 → 1.3.0-alpha.4

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.
Files changed (53) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +1 -1
  3. package/dist/CatalogueAPI.d.ts +448 -448
  4. package/dist/CatalogueAPI.js +206 -206
  5. package/dist/CfgProduct.d.ts +116 -116
  6. package/dist/CfgProduct.js +588 -588
  7. package/dist/index.d.ts +15 -15
  8. package/dist/index.js +15 -15
  9. package/dist/material/CfgMaterialMapping.d.ts +7 -7
  10. package/dist/material/CfgMaterialMapping.js +176 -176
  11. package/dist/material/CfgMtrlApplication.d.ts +18 -18
  12. package/dist/material/CfgMtrlApplication.js +43 -43
  13. package/dist/material/CfgMtrlApplicationSource.d.ts +7 -7
  14. package/dist/material/CfgMtrlApplicationSource.js +8 -8
  15. package/dist/material/CfgMtrlSource.d.ts +19 -19
  16. package/dist/material/CfgMtrlSource.js +40 -40
  17. package/dist/material/CfgMtrlSourceWithMetaData.d.ts +7 -7
  18. package/dist/material/CfgMtrlSourceWithMetaData.js +1 -1
  19. package/dist/productConfiguration/CfgFeature.d.ts +134 -80
  20. package/dist/productConfiguration/CfgFeature.js +483 -451
  21. package/dist/productConfiguration/CfgOption.d.ts +112 -64
  22. package/dist/productConfiguration/CfgOption.js +293 -263
  23. package/dist/productConfiguration/CfgProductConfiguration.d.ts +50 -50
  24. package/dist/productConfiguration/CfgProductConfiguration.js +198 -198
  25. package/dist/productConfiguration/filters.d.ts +7 -7
  26. package/dist/productConfiguration/filters.js +29 -29
  27. package/dist/productConfiguration/productParamsGenerator.d.ts +15 -15
  28. package/dist/productConfiguration/productParamsGenerator.js +51 -51
  29. package/dist/productConfiguration/utilitiesProductConfiguration.d.ts +9 -9
  30. package/dist/productConfiguration/utilitiesProductConfiguration.js +61 -61
  31. package/dist/productLoader.d.ts +11 -11
  32. package/dist/productLoader.js +41 -41
  33. package/dist/tests/testData/collectorForTest.d.ts +73 -73
  34. package/dist/tests/testData/collectorForTest.js +195 -195
  35. package/dist/tests/testData/dummyProductForTest.d.ts +4 -4
  36. package/dist/tests/testData/dummyProductForTest.js +35 -35
  37. package/dist/tests/testData/testDataAdditionalProductInAdditionalProductInProductForTest.d.ts +32 -32
  38. package/dist/tests/testData/testDataAdditionalProductInAdditionalProductInProductForTest.js +368 -368
  39. package/dist/tests/testData/testDataCachedGetProduct.d.ts +5 -5
  40. package/dist/tests/testData/testDataCachedGetProduct.js +199 -199
  41. package/dist/tests/testData/testDataCachedPostValidate.d.ts +7 -7
  42. package/dist/tests/testData/testDataCachedPostValidate.js +189 -189
  43. package/dist/tests/testData/testDataNoAdditionalProductNoPropagateForTest.d.ts +3 -3
  44. package/dist/tests/testData/testDataNoAdditionalProductNoPropagateForTest.js +1117 -1117
  45. package/dist/tests/testData/testDataProductAggregatedPrice.d.ts +28 -28
  46. package/dist/tests/testData/testDataProductAggregatedPrice.js +205 -205
  47. package/dist/tests/testData/testDataUpcharge.d.ts +29 -29
  48. package/dist/tests/testData/testDataUpcharge.js +159 -159
  49. package/dist/utilitiesCatalogueData.d.ts +20 -18
  50. package/dist/utilitiesCatalogueData.js +64 -56
  51. package/dist/utilitiesCataloguePermission.d.ts +39 -39
  52. package/dist/utilitiesCataloguePermission.js +84 -84
  53. package/package.json +3 -3
@@ -1,451 +1,483 @@
1
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
- return new (P || (P = Promise))(function (resolve, reject) {
4
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
- step((generator = generator.apply(thisArg, _arguments || [])).next());
8
- });
9
- };
10
- import { compareArrays, count, Observable, someMatch, } from "@configura/web-utilities";
11
- import { CfgProduct } from "../CfgProduct.js";
12
- import { CfgMtrlApplication } from "../material/CfgMtrlApplication.js";
13
- import { CfgMtrlApplicationSource } from "../material/CfgMtrlApplicationSource.js";
14
- import { CfgOption, ProductConfigurationBubbleMode } from "./CfgOption.js";
15
- import { getMtrlPreview } from "./utilitiesProductConfiguration.js";
16
- export var SelectionType;
17
- (function (SelectionType) {
18
- /// All options are permanently selected. In our ui-component for this we skip over this level,
19
- /// and immediately show the children, but you could do this differently, like for instance
20
- /// showing the group heading.
21
- SelectionType[SelectionType["Group"] = 0] = "Group";
22
- /// One and only one can be selected at a time. This normally corresponds to either a dropdown
23
- /// menu or radio buttons.
24
- SelectionType[SelectionType["SelectOne"] = 1] = "SelectOne";
25
- /// Zero to all can be selected at a time. This normally corresponds to checkboxes.
26
- SelectionType[SelectionType["SelectMany"] = 2] = "SelectMany";
27
- })(SelectionType || (SelectionType = {}));
28
- function getOptionFilter(option1) {
29
- return (option2) => option2._internal === option1;
30
- }
31
- /// Returns a fresh reference for the option at the given index
32
- function doFreshRefOptionAtIndex(options, index, beforeNotify) {
33
- const freshRef = CfgOption._makeNewRefFrom(options[index]._internal);
34
- options[index] = freshRef;
35
- if (beforeNotify) {
36
- beforeNotify(freshRef);
37
- }
38
- freshRef._internal.changeObservable.notifyAll({ freshRef });
39
- return freshRef;
40
- }
41
- /// Returns a fresh reference for the given option.
42
- /// Throws an error if the option is not found in options.
43
- function doFreshRefOption(options, optionInternal, beforeNotify) {
44
- const i = options.findIndex(getOptionFilter(optionInternal));
45
- if (i === -1) {
46
- throw new Error("Did not find the option in options");
47
- }
48
- return doFreshRefOptionAtIndex(options, i, beforeNotify);
49
- }
50
- /// This class is meant to only be used through CfgFeature. It should
51
- /// never be instantiated on its own. Normally the internal state of this class
52
- /// should never be directly modified. CfgFeature is the class that
53
- /// should be used and interacted with.
54
- export class _CfgFeatureInternal {
55
- constructor(rawFeature, allRawFeatures, key, // Unique amongst siblings
56
- parent, parentProduct, rootProduct) {
57
- this.rawFeature = rawFeature;
58
- this.allRawFeatures = allRawFeatures;
59
- this.key = key;
60
- this.parent = parent;
61
- this.parentProduct = parentProduct;
62
- this.rootProduct = rootProduct;
63
- this._selectedOptions = [];
64
- this.changeObservable = new Observable();
65
- this._notifyAllOfChange = (bubbleMode) => __awaiter(this, void 0, void 0, function* () {
66
- if (bubbleMode === ProductConfigurationBubbleMode.Stop) {
67
- return;
68
- }
69
- const parent = this.parent;
70
- const freshRef = CfgFeature._makeNewRefFrom(this);
71
- this.changeObservable.notifyAll({ freshRef });
72
- if (parent !== undefined) {
73
- yield parent._childHasChanged(freshRef, bubbleMode === ProductConfigurationBubbleMode.OneLevel
74
- ? ProductConfigurationBubbleMode.Stop
75
- : bubbleMode);
76
- }
77
- });
78
- /// Called by child to tell its parent that it has changed.
79
- /// Will throw if options have not yet been generated. This should be impossible
80
- /// as nonexisting children can not call their parent.
81
- this._childHasChanged = (childOption, bubbleMode) => __awaiter(this, void 0, void 0, function* () {
82
- const options = this._options;
83
- if (options === undefined) {
84
- throw Error("Child says it changed, but no children has actually been generated");
85
- }
86
- const freshRef = doFreshRefOption(options, childOption);
87
- const selectedOptions = this._selectedOptions;
88
- const i = selectedOptions.findIndex(getOptionFilter(childOption));
89
- if (i !== -1) {
90
- selectedOptions[i] = freshRef;
91
- }
92
- yield this._notifyAllOfChange(bubbleMode);
93
- });
94
- this.getApiSelection = () => {
95
- const result = {};
96
- for (const option of this._selectedOptions) {
97
- result[option.code] = option._internal.getApiSelection();
98
- }
99
- return result;
100
- };
101
- this.setApiSelection = (apiOptionSelectionMap) => __awaiter(this, void 0, void 0, function* () {
102
- const selectionType = this.selectionType;
103
- const isGroup = selectionType === SelectionType.Group;
104
- const isSelectOne = selectionType === SelectionType.SelectOne;
105
- const isAllOptionsAffectedByAnySelection = this.isAllOptionsAffectedByAnySelection;
106
- let options;
107
- if (apiOptionSelectionMap === undefined) {
108
- options = this._options || []; // Already generated children (all or none)
109
- apiOptionSelectionMap = {};
110
- }
111
- else {
112
- options = this.options; // This will generate all children
113
- }
114
- let change = false;
115
- for (let i = 0; i < options.length; i++) {
116
- const option = options[i];
117
- const apiOptionSelection = apiOptionSelectionMap[option.code];
118
- const selectedIndex = this._selectedOptions.findIndex(getOptionFilter(option._internal));
119
- const isOn = isGroup || apiOptionSelection;
120
- if (isOn) {
121
- if (selectedIndex === -1) {
122
- if (isAllOptionsAffectedByAnySelection) {
123
- this._selectedOptions.push(option);
124
- }
125
- else {
126
- doFreshRefOptionAtIndex(options, i, (freshRef) => this._selectedOptions.push(freshRef));
127
- }
128
- change = true;
129
- }
130
- }
131
- else {
132
- if (selectedIndex !== -1) {
133
- this._selectedOptions.splice(selectedIndex, 1);
134
- if (!isAllOptionsAffectedByAnySelection) {
135
- doFreshRefOptionAtIndex(options, i);
136
- }
137
- change = true;
138
- }
139
- }
140
- if (isOn && (yield option._internal.setApiSelection(apiOptionSelection))) {
141
- change = true;
142
- }
143
- }
144
- const selectionCount = this._selectedOptions.length;
145
- if (isSelectOne && selectionCount !== 1) {
146
- const wrongCountWarning = `A select-one Feature (i.e. neither optional nor multiple) should have exactly one Option selected. Feature key: "${this.key}". Actual: ${selectionCount}`;
147
- if (this.rootProduct.settings.strictSelectOneSelectionCount) {
148
- throw new Error(wrongCountWarning);
149
- }
150
- else {
151
- console.warn(wrongCountWarning);
152
- }
153
- }
154
- if (!isGroup) {
155
- const apiSelectionCount = Object.keys(apiOptionSelectionMap).length;
156
- if (selectionCount !== apiSelectionCount) {
157
- const wrongCountWarning = `All provided Options are expected to be selected. Feature key: "${this.key}". Expected: ${apiSelectionCount} Actual: ${selectionCount}`;
158
- if (this.rootProduct.settings.strictSetApiSelectionMatch) {
159
- throw new Error(wrongCountWarning);
160
- }
161
- else {
162
- console.warn(wrongCountWarning);
163
- }
164
- }
165
- }
166
- if (change) {
167
- if (isAllOptionsAffectedByAnySelection) {
168
- this._freshRefAllOptions();
169
- }
170
- // setApiSelection works its way top down and handles notifications on
171
- // each level by looking at change returned from its children. That way
172
- // we do not get multiple notifications for the same ancestral node even
173
- // if multiple descendants are affected by the setApiSelection. Therefore
174
- // we use OneLevel to make this feature notify and the parent update its
175
- // reference.
176
- yield this._notifyAllOfChange(ProductConfigurationBubbleMode.OneLevel);
177
- }
178
- return change;
179
- });
180
- this.structureCompare = (other, strictOrder = true, descriptionMatch = false) => this.keyMatch(other, descriptionMatch) &&
181
- compareArrays(this.options, other.options, (l, r) => l._internal.structureCompare(r._internal, strictOrder, descriptionMatch), strictOrder);
182
- this.tryMatchSelection = (other, descriptionMatch = false) => __awaiter(this, void 0, void 0, function* () {
183
- const selectionType = this.selectionType;
184
- if (selectionType !== other.selectionType) {
185
- // Will not try if the selection type is different
186
- return false;
187
- }
188
- const thisOptions = this.options;
189
- const otherOptions = other.options;
190
- const change = (yield Promise.all(otherOptions.map((otherO) => (() => __awaiter(this, void 0, void 0, function* () {
191
- const otherOSelected = otherO.selected;
192
- if (selectionType === SelectionType.SelectOne && !otherOSelected) {
193
- return false;
194
- }
195
- if (1 <
196
- count(otherOptions, (item) => item._internal.keyMatch(otherO._internal, descriptionMatch))) {
197
- console.warn("tryMatchSelection will ignore options that have the same key");
198
- return false;
199
- }
200
- const toTryChangeOptions = thisOptions.filter((o) => otherO._internal.keyMatch(o._internal, descriptionMatch));
201
- if (1 < toTryChangeOptions.length) {
202
- console.warn("tryMatchSelection will ignore options that have the same key");
203
- return false;
204
- }
205
- if (toTryChangeOptions.length === 0) {
206
- return false;
207
- }
208
- const toTryChangeOption = toTryChangeOptions[0];
209
- let change = false;
210
- if (selectionType === SelectionType.SelectMany ||
211
- selectionType === SelectionType.SelectOne) {
212
- // The setSelected will only affect ourselves,
213
- // so we do not need to bubble the notification.
214
- // Instead we use the change variable to know
215
- // when to notify for this feature.
216
- change = yield toTryChangeOption._internal.parent.selectOption(toTryChangeOption._internal, otherOSelected, ProductConfigurationBubbleMode.Stop);
217
- }
218
- if (otherOSelected &&
219
- (yield toTryChangeOption._internal.tryMatchSelection(otherO, descriptionMatch))) {
220
- change = true;
221
- }
222
- return change;
223
- }))()))).some((b) => b);
224
- if (change) {
225
- // tryMatchSelection works its way top down and handles notifications on
226
- // each level by looking at change returned from its children. That way
227
- // we do not get multiple notifications for the same ancestral node even
228
- // if multiple descendants are affected by the tryMatchSelection. Therefore
229
- // we use OneLevel to make this feature notify and the parent update its
230
- // reference.
231
- yield this._notifyAllOfChange(ProductConfigurationBubbleMode.OneLevel);
232
- }
233
- return change;
234
- });
235
- /// Normally this is used through methods on CfgFeature and CfgOption. Use this
236
- /// internal version if you need to control the bubbleMode.
237
- /// Using a validate bubbleMode will cause validation calls to the server.
238
- this.selectOption = (optionInternal, on, bubbleMode) => __awaiter(this, void 0, void 0, function* () {
239
- if (!on) {
240
- if (this.selectionType === SelectionType.Group) {
241
- throw new Error(`Multiple features are always selected and are not user selectable. Feature key: "${this.key}".`);
242
- }
243
- if (this.selectionType === SelectionType.SelectOne) {
244
- // Select one can never be deselected. However, propagate can
245
- // make calls with deselect ending up here. We simply ignore them.
246
- return false;
247
- }
248
- }
249
- const selectedOptions = this._selectedOptions;
250
- const isAllOptionsAffectedByAnySelection = this.isAllOptionsAffectedByAnySelection;
251
- const index = selectedOptions.findIndex(getOptionFilter(optionInternal));
252
- if ((index !== -1) === on) {
253
- return false;
254
- }
255
- if (on) {
256
- // Calling this.options will populate (generate) all Options if they have not yet
257
- // been generated. As we are selecting an option we need all Options to be generated.
258
- const options = this.options;
259
- if (this.selectionType === SelectionType.SelectOne) {
260
- let removeOption = selectedOptions.shift();
261
- while (removeOption !== undefined) {
262
- if (!isAllOptionsAffectedByAnySelection) {
263
- doFreshRefOption(options, removeOption._internal);
264
- }
265
- removeOption = selectedOptions.shift();
266
- }
267
- }
268
- if (isAllOptionsAffectedByAnySelection) {
269
- selectedOptions.push(CfgOption._makeNewRefFrom(optionInternal));
270
- }
271
- else {
272
- doFreshRefOption(options, optionInternal, (freshRef) => selectedOptions.push(freshRef));
273
- }
274
- }
275
- else {
276
- selectedOptions.splice(index, 1);
277
- if (!isAllOptionsAffectedByAnySelection) {
278
- // Accessing this._options gives us the Options that have been generated
279
- // or undefined if the Options have not yet been generated. As this action
280
- // is deselect and deselected is the default uninitialized state we can
281
- // safely ignore if undefined.
282
- const options = this._options;
283
- if (options !== undefined) {
284
- doFreshRefOption(options, optionInternal);
285
- }
286
- }
287
- }
288
- if (isAllOptionsAffectedByAnySelection) {
289
- this._freshRefAllOptions();
290
- }
291
- // If this was a deselect action we shall not bubble selected, that is, we shall not
292
- // select the ancestors if this action was deselect.
293
- yield this._notifyAllOfChange(!on && bubbleMode === ProductConfigurationBubbleMode.ValidateAndBubbleSelected
294
- ? ProductConfigurationBubbleMode.Validate
295
- : bubbleMode);
296
- return true;
297
- });
298
- this.isSelected = (option) => this.selectionType === SelectionType.Group ||
299
- this._selectedOptions.some(getOptionFilter(option));
300
- this.keyMatch = (other, descriptionMatch = false) => descriptionMatch
301
- ? this.description.toLowerCase() === other.description.toLowerCase()
302
- : this.code === other.code;
303
- /// Only in selected options
304
- this._getFeaturesWithCode = (code) => this._selectedOptions.reduce((agg, selectedOption) => {
305
- agg.push(...selectedOption._internal._getFeaturesWithCode(code));
306
- return agg;
307
- }, code === this.code ? [this] : []);
308
- if (rawFeature.multiple) {
309
- this.selectionType = SelectionType.Group;
310
- }
311
- else if (rawFeature.optional) {
312
- this.selectionType = SelectionType.SelectMany;
313
- }
314
- else {
315
- this.selectionType = SelectionType.SelectOne;
316
- }
317
- this.hasUpcharge = rawFeature.options.some((option) => (option.upcharge !== undefined && option.upcharge !== 0) ||
318
- (option.priceCodes !== undefined && option.priceCodes.length > 0));
319
- }
320
- get code() {
321
- return this.rawFeature.code;
322
- }
323
- get groupCode() {
324
- return this.rawFeature.groupCode;
325
- }
326
- get description() {
327
- return this.rawFeature.description;
328
- }
329
- get mtrlApplications() {
330
- if (this._mtrlApplications === undefined) {
331
- this._mtrlApplications = (this.rawFeature.mtrlApplications || []).map((m) => CfgMtrlApplication.fromMtrlLikeApplication(CfgMtrlApplicationSource.Feature, m));
332
- }
333
- return this._mtrlApplications;
334
- }
335
- get selectedOptions() {
336
- return this._selectedOptions;
337
- }
338
- /// If one option is selected or deselected this will potentially
339
- /// affect all other Options on this Feature
340
- get isAllOptionsAffectedByAnySelection() {
341
- return this.selectionType === SelectionType.SelectOne && this.hasUpcharge !== false;
342
- }
343
- get preview() {
344
- return getMtrlPreview(this._mtrlApplications);
345
- }
346
- get visibleIfAdditionalProduct() {
347
- return this.rawFeature.hideIfAdditionalProduct !== true;
348
- }
349
- get visibleIfMainProduct() {
350
- return this.rawFeature.hideIfMainProduct !== true;
351
- }
352
- get visible() {
353
- return this.parentProduct.isAdditionalProduct
354
- ? this.visibleIfAdditionalProduct
355
- : this.visibleIfMainProduct;
356
- }
357
- get options() {
358
- if (this._options === undefined) {
359
- const hasDuplicateDescription = someMatch(this.rawFeature.options, (l, r) => {
360
- return l.description.toLowerCase() === r.description.toLowerCase();
361
- });
362
- this._options = this.rawFeature.options.map((o) => CfgOption.make(o, this, this.allRawFeatures, hasDuplicateDescription, this.parentProduct, this.rootProduct));
363
- }
364
- return this._options;
365
- }
366
- /// Make fresh references to all options on this feature.
367
- /// Also includes currently selected options.
368
- /// Will throw if options have not yet been generated.
369
- _freshRefAllOptions() {
370
- const options = this._options;
371
- const selectedOptions = this._selectedOptions;
372
- if (options === undefined) {
373
- throw new Error("We expect all options to be generated at freshref");
374
- }
375
- for (let i = 0; i < options.length; i++) {
376
- doFreshRefOptionAtIndex(options, i, (freshRef) => {
377
- const selectedIndex = selectedOptions.findIndex(getOptionFilter(freshRef._internal));
378
- if (selectedIndex !== -1) {
379
- selectedOptions[selectedIndex] = freshRef;
380
- }
381
- });
382
- }
383
- }
384
- _freshRefDescendants() {
385
- const options = this._options || [];
386
- for (let i = 0; i < options.length; i++) {
387
- const optionInternal = options[i]._internal;
388
- optionInternal._freshRefDescendants();
389
- options[i] = CfgOption._makeNewRefFrom(optionInternal);
390
- }
391
- }
392
- }
393
- export class CfgFeature {
394
- /// Private constructor and make-method because make new ref requires the constructor to
395
- /// take an internal and we don't want those who instantiate CfgFeature to have to be aware
396
- /// of the internal.
397
- constructor(_internal) {
398
- this._internal = _internal;
399
- this.isBackedBySame = (other) => this._internal === other._internal;
400
- /// Selects the passed Option.
401
- /// Only Options belonging to Features that are "select many" can be deselected.
402
- /// Calling this will cause a validation call to the server.
403
- this.selectOption = (option, on) => __awaiter(this, void 0, void 0, function* () {
404
- return yield this._internal.selectOption(option._internal, on, ProductConfigurationBubbleMode.ValidateAndBubbleSelected);
405
- });
406
- this.isSelected = (option) => this._internal.isSelected(option._internal);
407
- this.listenForChange = (l) => this._internal.changeObservable.listen(l);
408
- this.stopListenForChange = (l) => this._internal.changeObservable.stopListen(l);
409
- }
410
- get parentProduct() {
411
- return CfgProduct._makeNewRefFrom(this._internal.parentProduct);
412
- }
413
- get rootProduct() {
414
- return CfgProduct._makeNewRefFrom(this._internal.rootProduct);
415
- }
416
- get selectionType() {
417
- return this._internal.selectionType;
418
- }
419
- get key() {
420
- return this._internal.key;
421
- }
422
- get code() {
423
- return this._internal.code;
424
- }
425
- get groupCode() {
426
- return this._internal.groupCode;
427
- }
428
- get description() {
429
- return this._internal.description;
430
- }
431
- get hasUpcharge() {
432
- return this._internal.hasUpcharge;
433
- }
434
- get selectedOptions() {
435
- return this._internal.selectedOptions;
436
- }
437
- get preview() {
438
- return this._internal.preview;
439
- }
440
- get options() {
441
- return this._internal.options;
442
- }
443
- get visible() {
444
- return this._internal.visible;
445
- }
446
- }
447
- CfgFeature.make = (rawFeature, allRawFeatures, key, parent, parentProduct, rootProduct) => new CfgFeature(new _CfgFeatureInternal(rawFeature, allRawFeatures, key, parent, parentProduct, rootProduct));
448
- /// Makes an object wrapping the passed object. This is not a clone method,
449
- /// it is a method to make a new outer reference. Like a shallow copy.
450
- /// We use this to help frameworks that are built around using equals to detect change.
451
- CfgFeature._makeNewRefFrom = (internal) => new CfgFeature(internal);
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { compareArrays, count, Observable, someMatch, } from "@configura/web-utilities";
11
+ import { CfgProduct } from "../CfgProduct.js";
12
+ import { CfgMtrlApplication } from "../material/CfgMtrlApplication.js";
13
+ import { CfgMtrlApplicationSource } from "../material/CfgMtrlApplicationSource.js";
14
+ import { CfgOption, ProductConfigurationBubbleMode } from "./CfgOption.js";
15
+ import { _CfgProductConfigurationInternal } from "./CfgProductConfiguration.js";
16
+ import { getMtrlPreview } from "./utilitiesProductConfiguration.js";
17
+ export var SelectionType;
18
+ (function (SelectionType) {
19
+ /**
20
+ * All options are permanently selected. In our ui-component for this we skip over this level,
21
+ * and immediately show the children, but you could do this differently, like for instance
22
+ * showing the group heading.
23
+ */
24
+ SelectionType[SelectionType["Group"] = 0] = "Group";
25
+ /**
26
+ * One and only one can be selected at a time. This normally corresponds to either a dropdown
27
+ * menu or radio buttons.
28
+ */
29
+ SelectionType[SelectionType["SelectOne"] = 1] = "SelectOne";
30
+ /** Zero to all can be selected at a time. This normally corresponds to checkboxes. */
31
+ SelectionType[SelectionType["SelectMany"] = 2] = "SelectMany";
32
+ })(SelectionType || (SelectionType = {}));
33
+ function getOptionFilter(option1) {
34
+ return (option2) => option2._internal === option1;
35
+ }
36
+ /** Returns a fresh reference for the option at the given index */
37
+ function doFreshRefOptionAtIndex(options, index, beforeNotify) {
38
+ const freshRef = CfgOption._makeNewRefFrom(options[index]._internal);
39
+ options[index] = freshRef;
40
+ if (beforeNotify) {
41
+ beforeNotify(freshRef);
42
+ }
43
+ freshRef._internal.changeObservable.notifyAll({ freshRef });
44
+ return freshRef;
45
+ }
46
+ /**
47
+ * Returns a fresh reference for the given option.
48
+ * @throws Throws an error if the option is not found in options.
49
+ */
50
+ function doFreshRefOption(options, optionInternal, beforeNotify) {
51
+ const i = options.findIndex(getOptionFilter(optionInternal));
52
+ if (i === -1) {
53
+ throw new Error("Did not find the option in options");
54
+ }
55
+ return doFreshRefOptionAtIndex(options, i, beforeNotify);
56
+ }
57
+ /**
58
+ * This class is meant to only be used through CfgFeature. It should
59
+ * never be instantiated on its own. Normally the internal state of this class
60
+ * should never be directly modified. CfgFeature is the class that
61
+ * should be used and interacted with.
62
+ */
63
+ export class _CfgFeatureInternal {
64
+ constructor(rawFeature, allRawFeatures, key, // Unique amongst siblings
65
+ parent, parentProduct, rootProduct) {
66
+ this.rawFeature = rawFeature;
67
+ this.allRawFeatures = allRawFeatures;
68
+ this.key = key;
69
+ this.parent = parent;
70
+ this.parentProduct = parentProduct;
71
+ this.rootProduct = rootProduct;
72
+ this._selectedOptions = [];
73
+ this.changeObservable = new Observable();
74
+ this._notifyAllOfChange = (bubbleMode) => __awaiter(this, void 0, void 0, function* () {
75
+ if (bubbleMode === ProductConfigurationBubbleMode.Stop) {
76
+ return;
77
+ }
78
+ const parent = this.parent;
79
+ const freshRef = CfgFeature._makeNewRefFrom(this);
80
+ this.changeObservable.notifyAll({ freshRef });
81
+ if (parent !== undefined) {
82
+ yield parent._childHasChanged(freshRef, bubbleMode === ProductConfigurationBubbleMode.OneLevel
83
+ ? ProductConfigurationBubbleMode.Stop
84
+ : bubbleMode);
85
+ }
86
+ });
87
+ /**
88
+ * Called by child to tell its parent that it has changed.
89
+ * @throws Will throw if options have not yet been generated. This should be impossible
90
+ * as nonexisting children can not call their parent.
91
+ */
92
+ this._childHasChanged = (childOption, bubbleMode) => __awaiter(this, void 0, void 0, function* () {
93
+ const options = this._options;
94
+ if (options === undefined) {
95
+ throw Error("Child says it changed, but no children has actually been generated");
96
+ }
97
+ const freshRef = doFreshRefOption(options, childOption);
98
+ const selectedOptions = this._selectedOptions;
99
+ const i = selectedOptions.findIndex(getOptionFilter(childOption));
100
+ if (i !== -1) {
101
+ selectedOptions[i] = freshRef;
102
+ }
103
+ yield this._notifyAllOfChange(bubbleMode);
104
+ });
105
+ this.getApiSelection = () => {
106
+ const result = {};
107
+ for (const option of this._selectedOptions) {
108
+ result[option.code] = option._internal.getApiSelection();
109
+ }
110
+ return result;
111
+ };
112
+ this.setApiSelection = (apiOptionSelectionMap) => __awaiter(this, void 0, void 0, function* () {
113
+ const selectionType = this.selectionType;
114
+ const isGroup = selectionType === SelectionType.Group;
115
+ const isSelectOne = selectionType === SelectionType.SelectOne;
116
+ const isAllOptionsAffectedByAnySelection = this.isAllOptionsAffectedByAnySelection;
117
+ let options;
118
+ if (apiOptionSelectionMap === undefined) {
119
+ options = this._options || []; // Already generated children (all or none)
120
+ apiOptionSelectionMap = {};
121
+ }
122
+ else {
123
+ options = this.options; // This will generate all children
124
+ }
125
+ let change = false;
126
+ for (let i = 0; i < options.length; i++) {
127
+ const option = options[i];
128
+ const apiOptionSelection = apiOptionSelectionMap[option.code];
129
+ const selectedIndex = this._selectedOptions.findIndex(getOptionFilter(option._internal));
130
+ const isOn = isGroup || apiOptionSelection;
131
+ if (isOn) {
132
+ if (selectedIndex === -1) {
133
+ if (isAllOptionsAffectedByAnySelection) {
134
+ this._selectedOptions.push(option);
135
+ }
136
+ else {
137
+ doFreshRefOptionAtIndex(options, i, (freshRef) => this._selectedOptions.push(freshRef));
138
+ }
139
+ change = true;
140
+ }
141
+ }
142
+ else {
143
+ if (selectedIndex !== -1) {
144
+ this._selectedOptions.splice(selectedIndex, 1);
145
+ if (!isAllOptionsAffectedByAnySelection) {
146
+ doFreshRefOptionAtIndex(options, i);
147
+ }
148
+ change = true;
149
+ }
150
+ }
151
+ if (yield option._internal.setApiSelection(apiOptionSelection)) {
152
+ change = true;
153
+ }
154
+ }
155
+ if (this.ancestorsSelected) {
156
+ const selectionCount = this._selectedOptions.length;
157
+ if (isSelectOne && selectionCount !== 1) {
158
+ const wrongCountWarning = `A select-one Feature (i.e. neither optional nor multiple) should have exactly one Option selected. Feature key: "${this.key}". Actual: ${selectionCount}`;
159
+ if (this.rootProduct.settings.strictSelectOneSelectionCount) {
160
+ throw new Error(wrongCountWarning);
161
+ }
162
+ else {
163
+ console.warn(wrongCountWarning);
164
+ }
165
+ }
166
+ if (!isGroup) {
167
+ const apiSelectionCount = Object.keys(apiOptionSelectionMap).length;
168
+ if (selectionCount !== apiSelectionCount) {
169
+ const wrongCountWarning = `All provided Options are expected to be selected. Feature key: "${this.key}". Expected: ${apiSelectionCount} Actual: ${selectionCount}`;
170
+ if (this.rootProduct.settings.strictSetApiSelectionMatch) {
171
+ throw new Error(wrongCountWarning);
172
+ }
173
+ else {
174
+ console.warn(wrongCountWarning);
175
+ }
176
+ }
177
+ }
178
+ }
179
+ if (change) {
180
+ if (isAllOptionsAffectedByAnySelection) {
181
+ this._freshRefAllOptions();
182
+ }
183
+ // setApiSelection works its way top down and handles notifications on
184
+ // each level by looking at change returned from its children. That way
185
+ // we do not get multiple notifications for the same ancestral node even
186
+ // if multiple descendants are affected by the setApiSelection. Therefore
187
+ // we use OneLevel to make this feature notify and the parent update its
188
+ // reference.
189
+ yield this._notifyAllOfChange(ProductConfigurationBubbleMode.OneLevel);
190
+ }
191
+ return change;
192
+ });
193
+ this.structureCompare = (other, strictOrder = true, descriptionMatch = false) => this.keyMatch(other, descriptionMatch) &&
194
+ compareArrays(this.options, other.options, (l, r) => l._internal.structureCompare(r._internal, strictOrder, descriptionMatch), strictOrder);
195
+ this.tryMatchSelection = (other, descriptionMatch = false) => __awaiter(this, void 0, void 0, function* () {
196
+ const selectionType = this.selectionType;
197
+ if (selectionType !== other.selectionType) {
198
+ // Will not try if the selection type is different
199
+ return false;
200
+ }
201
+ const thisOptions = this.options;
202
+ const otherOptions = other.options;
203
+ const change = (yield Promise.all(otherOptions.map((otherO) => (() => __awaiter(this, void 0, void 0, function* () {
204
+ const otherOSelected = otherO.selected;
205
+ if (selectionType === SelectionType.SelectOne && !otherOSelected) {
206
+ return false;
207
+ }
208
+ if (1 <
209
+ count(otherOptions, (item) => item._internal.keyMatch(otherO._internal, descriptionMatch))) {
210
+ console.warn("tryMatchSelection will ignore options that have the same key");
211
+ return false;
212
+ }
213
+ const toTryChangeOptions = thisOptions.filter((o) => otherO._internal.keyMatch(o._internal, descriptionMatch));
214
+ if (1 < toTryChangeOptions.length) {
215
+ console.warn("tryMatchSelection will ignore options that have the same key");
216
+ return false;
217
+ }
218
+ if (toTryChangeOptions.length === 0) {
219
+ return false;
220
+ }
221
+ const toTryChangeOption = toTryChangeOptions[0];
222
+ let change = false;
223
+ if (selectionType === SelectionType.SelectMany ||
224
+ selectionType === SelectionType.SelectOne) {
225
+ // The setSelected will only affect ourselves,
226
+ // so we do not need to bubble the notification.
227
+ // Instead we use the change variable to know
228
+ // when to notify for this feature.
229
+ change = yield toTryChangeOption._internal.parent.selectOption(toTryChangeOption._internal, otherOSelected, ProductConfigurationBubbleMode.Stop);
230
+ }
231
+ if (otherOSelected &&
232
+ (yield toTryChangeOption._internal.tryMatchSelection(otherO, descriptionMatch))) {
233
+ change = true;
234
+ }
235
+ return change;
236
+ }))()))).some((b) => b);
237
+ if (change) {
238
+ // tryMatchSelection works its way top down and handles notifications on
239
+ // each level by looking at change returned from its children. That way
240
+ // we do not get multiple notifications for the same ancestral node even
241
+ // if multiple descendants are affected by the tryMatchSelection. Therefore
242
+ // we use OneLevel to make this feature notify and the parent update its
243
+ // reference.
244
+ yield this._notifyAllOfChange(ProductConfigurationBubbleMode.OneLevel);
245
+ }
246
+ return change;
247
+ });
248
+ /**
249
+ * Normally this is used through methods on CfgFeature and CfgOption. Use this
250
+ * internal version if you need to control the bubbleMode.
251
+ * Using a validate bubbleMode will cause validation calls to the server.
252
+ */
253
+ this.selectOption = (optionInternal, on, bubbleMode) => __awaiter(this, void 0, void 0, function* () {
254
+ if (!on) {
255
+ if (this.selectionType === SelectionType.Group) {
256
+ throw new Error(`Multiple features are always selected and are not user selectable. Feature key: "${this.key}".`);
257
+ }
258
+ if (this.selectionType === SelectionType.SelectOne) {
259
+ // Select one can never be deselected. However, propagate can
260
+ // make calls with deselect ending up here. We simply ignore them.
261
+ return false;
262
+ }
263
+ }
264
+ const selectedOptions = this._selectedOptions;
265
+ const isAllOptionsAffectedByAnySelection = this.isAllOptionsAffectedByAnySelection;
266
+ const index = selectedOptions.findIndex(getOptionFilter(optionInternal));
267
+ if ((index !== -1) === on) {
268
+ return false;
269
+ }
270
+ if (on) {
271
+ // Calling this.options will populate (generate) all Options if they have not yet
272
+ // been generated. As we are selecting an option we need all Options to be generated.
273
+ const options = this.options;
274
+ if (this.selectionType === SelectionType.SelectOne) {
275
+ let removeOption = selectedOptions.shift();
276
+ while (removeOption !== undefined) {
277
+ if (!isAllOptionsAffectedByAnySelection) {
278
+ doFreshRefOption(options, removeOption._internal);
279
+ }
280
+ removeOption = selectedOptions.shift();
281
+ }
282
+ }
283
+ if (isAllOptionsAffectedByAnySelection) {
284
+ selectedOptions.push(CfgOption._makeNewRefFrom(optionInternal));
285
+ }
286
+ else {
287
+ doFreshRefOption(options, optionInternal, (freshRef) => selectedOptions.push(freshRef));
288
+ }
289
+ }
290
+ else {
291
+ selectedOptions.splice(index, 1);
292
+ if (!isAllOptionsAffectedByAnySelection) {
293
+ // Accessing this._options gives us the Options that have been generated
294
+ // or undefined if the Options have not yet been generated. As this action
295
+ // is deselect and deselected is the default uninitialized state we can
296
+ // safely ignore if undefined.
297
+ const options = this._options;
298
+ if (options !== undefined) {
299
+ doFreshRefOption(options, optionInternal);
300
+ }
301
+ }
302
+ }
303
+ if (isAllOptionsAffectedByAnySelection) {
304
+ this._freshRefAllOptions();
305
+ }
306
+ // If this was a deselect action we shall not bubble selected, that is, we shall not
307
+ // select the ancestors if this action was deselect.
308
+ yield this._notifyAllOfChange(!on && bubbleMode === ProductConfigurationBubbleMode.ValidateAndBubbleSelected
309
+ ? ProductConfigurationBubbleMode.Validate
310
+ : bubbleMode);
311
+ return true;
312
+ });
313
+ this.isSelected = (option) => this.selectionType === SelectionType.Group ||
314
+ this._selectedOptions.some(getOptionFilter(option));
315
+ this.keyMatch = (other, descriptionMatch = false) => descriptionMatch
316
+ ? this.description.toLowerCase() === other.description.toLowerCase()
317
+ : this.code === other.code;
318
+ /** Only in selected options */
319
+ this._getFeaturesWithCode = (code) => this._selectedOptions.reduce((agg, selectedOption) => {
320
+ agg.push(...selectedOption._internal._getFeaturesWithCode(code));
321
+ return agg;
322
+ }, code === this.code ? [this] : []);
323
+ if (rawFeature.multiple) {
324
+ this.selectionType = SelectionType.Group;
325
+ }
326
+ else if (rawFeature.optional) {
327
+ this.selectionType = SelectionType.SelectMany;
328
+ }
329
+ else {
330
+ this.selectionType = SelectionType.SelectOne;
331
+ }
332
+ this.hasUpcharge = rawFeature.options.some((option) => (option.upcharge !== undefined && option.upcharge !== 0) ||
333
+ (option.priceCodes !== undefined && option.priceCodes.length > 0));
334
+ }
335
+ get code() {
336
+ return this.rawFeature.code;
337
+ }
338
+ get groupCode() {
339
+ return this.rawFeature.groupCode;
340
+ }
341
+ get description() {
342
+ return this.rawFeature.description;
343
+ }
344
+ get mtrlApplications() {
345
+ if (this._mtrlApplications === undefined) {
346
+ this._mtrlApplications = (this.rawFeature.mtrlApplications || []).map((m) => CfgMtrlApplication.fromMtrlLikeApplication(CfgMtrlApplicationSource.Feature, m));
347
+ }
348
+ return this._mtrlApplications;
349
+ }
350
+ get selectedOptions() {
351
+ return this._selectedOptions;
352
+ }
353
+ get ancestorsSelected() {
354
+ return (this.parent instanceof _CfgProductConfigurationInternal || this.parent.ancestorsSelected);
355
+ }
356
+ /**
357
+ * If one option is selected or deselected this will potentially
358
+ * affect all other Options on this Feature
359
+ */
360
+ get isAllOptionsAffectedByAnySelection() {
361
+ return this.selectionType === SelectionType.SelectOne && this.hasUpcharge !== false;
362
+ }
363
+ get preview() {
364
+ return getMtrlPreview(this._mtrlApplications);
365
+ }
366
+ get visibleIfAdditionalProduct() {
367
+ return this.rawFeature.hideIfAdditionalProduct !== true;
368
+ }
369
+ get visibleIfMainProduct() {
370
+ return this.rawFeature.hideIfMainProduct !== true;
371
+ }
372
+ get visible() {
373
+ return this.parentProduct.isAdditionalProduct
374
+ ? this.visibleIfAdditionalProduct
375
+ : this.visibleIfMainProduct;
376
+ }
377
+ get options() {
378
+ if (this._options === undefined) {
379
+ const hasDuplicateDescription = someMatch(this.rawFeature.options, (l, r) => {
380
+ return l.description.toLowerCase() === r.description.toLowerCase();
381
+ });
382
+ this._options = this.rawFeature.options.map((o) => CfgOption.make(o, this, this.allRawFeatures, hasDuplicateDescription, this.parentProduct, this.rootProduct));
383
+ }
384
+ return this._options;
385
+ }
386
+ /**
387
+ * Make fresh references to all options on this feature.
388
+ * Also includes currently selected options.
389
+ * @throws Will throw if options have not yet been generated.
390
+ */
391
+ _freshRefAllOptions() {
392
+ const options = this._options;
393
+ const selectedOptions = this._selectedOptions;
394
+ if (options === undefined) {
395
+ throw new Error("We expect all options to be generated at freshref");
396
+ }
397
+ for (let i = 0; i < options.length; i++) {
398
+ doFreshRefOptionAtIndex(options, i, (freshRef) => {
399
+ const selectedIndex = selectedOptions.findIndex(getOptionFilter(freshRef._internal));
400
+ if (selectedIndex !== -1) {
401
+ selectedOptions[selectedIndex] = freshRef;
402
+ }
403
+ });
404
+ }
405
+ }
406
+ _freshRefDescendants() {
407
+ const options = this._options || [];
408
+ for (let i = 0; i < options.length; i++) {
409
+ const optionInternal = options[i]._internal;
410
+ optionInternal._freshRefDescendants();
411
+ options[i] = CfgOption._makeNewRefFrom(optionInternal);
412
+ }
413
+ }
414
+ }
415
+ export class CfgFeature {
416
+ /**
417
+ * Private constructor and make-method because make new ref requires the constructor to
418
+ * take an internal and we don't want those who instantiate CfgFeature to have to be aware
419
+ * of the internal.
420
+ */
421
+ constructor(_internal) {
422
+ this._internal = _internal;
423
+ this.isBackedBySame = (other) => this._internal === other._internal;
424
+ /**
425
+ * Selects the passed Option.
426
+ * Only Options belonging to Features that are "select many" can be deselected.
427
+ * Calling this will cause a validation call to the server.
428
+ */
429
+ this.selectOption = (option, on) => __awaiter(this, void 0, void 0, function* () {
430
+ return yield this._internal.selectOption(option._internal, on, ProductConfigurationBubbleMode.ValidateAndBubbleSelected);
431
+ });
432
+ this.isSelected = (option) => this._internal.isSelected(option._internal);
433
+ this.listenForChange = (l) => this._internal.changeObservable.listen(l);
434
+ this.stopListenForChange = (l) => this._internal.changeObservable.stopListen(l);
435
+ }
436
+ get parentProduct() {
437
+ return CfgProduct._makeNewRefFrom(this._internal.parentProduct);
438
+ }
439
+ get rootProduct() {
440
+ return CfgProduct._makeNewRefFrom(this._internal.rootProduct);
441
+ }
442
+ get selectionType() {
443
+ return this._internal.selectionType;
444
+ }
445
+ get key() {
446
+ return this._internal.key;
447
+ }
448
+ get code() {
449
+ return this._internal.code;
450
+ }
451
+ get groupCode() {
452
+ return this._internal.groupCode;
453
+ }
454
+ get description() {
455
+ return this._internal.description;
456
+ }
457
+ get hasUpcharge() {
458
+ return this._internal.hasUpcharge;
459
+ }
460
+ get selectedOptions() {
461
+ return this._internal.selectedOptions;
462
+ }
463
+ /** Are all ancestors up to the CfgProductConfiguration selected? */
464
+ get ancestorsSelected() {
465
+ return this._internal.ancestorsSelected;
466
+ }
467
+ get preview() {
468
+ return this._internal.preview;
469
+ }
470
+ get options() {
471
+ return this._internal.options;
472
+ }
473
+ get visible() {
474
+ return this._internal.visible;
475
+ }
476
+ }
477
+ CfgFeature.make = (rawFeature, allRawFeatures, key, parent, parentProduct, rootProduct) => new CfgFeature(new _CfgFeatureInternal(rawFeature, allRawFeatures, key, parent, parentProduct, rootProduct));
478
+ /**
479
+ * Makes an object wrapping the passed object. This is not a clone method,
480
+ * it is a method to make a new outer reference. Like a shallow copy.
481
+ * We use this to help frameworks that are built around using equals to detect change.
482
+ */
483
+ CfgFeature._makeNewRefFrom = (internal) => new CfgFeature(internal);