@configura/web-api 3.0.0-alpha.1 → 3.0.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CatalogueAPI.d.ts +68 -1
- package/dist/CatalogueAPI.js +184 -219
- package/dist/CfgMeasure.js +1 -2
- package/dist/CfgProduct.d.ts +22 -3
- package/dist/CfgProduct.js +172 -162
- package/dist/io/CfgIOManager.js +2 -11
- package/dist/io/CfgIOProdConfConnector.js +14 -25
- package/dist/io/CfgObservableStateManager.js +1 -1
- package/dist/productConfiguration/CfgFeature.d.ts +5 -0
- package/dist/productConfiguration/CfgFeature.js +68 -53
- package/dist/productConfiguration/CfgOption.d.ts +8 -0
- package/dist/productConfiguration/CfgOption.js +68 -73
- package/dist/productConfiguration/CfgProductConfiguration.js +22 -34
- package/dist/productConfiguration/productParamsGenerator.js +43 -57
- package/dist/productLoader.js +2 -13
- package/dist/syncGroups/SyncGroupsHandler.js +60 -83
- package/dist/syncGroups/SyncGroupsPathHelper.js +5 -5
- package/dist/syncGroups/SyncGroupsState.js +4 -5
- package/dist/syncGroups/SyncGroupsTransaction.d.ts +19 -1
- package/dist/syncGroups/SyncGroupsTransaction.js +352 -303
- package/dist/tasks/TaskHandler.js +53 -64
- package/dist/tests/testData/dummyProductForTest.js +4 -1
- package/dist/tests/testData/testDataAdditionalProductInAdditionalProductInProductForTest.js +18 -21
- package/dist/tests/testData/testDataCachedGetProduct.js +20 -20
- package/dist/tests/testData/testDataCachedPostValidate.js +6 -15
- package/dist/tests/testData/testDataProductAggregatedPrice.js +21 -21
- package/dist/tests/testData/testDataUpcharge.js +31 -22
- package/dist/utilitiesCatalogueData.js +21 -9
- package/dist/utilitiesCataloguePermission.js +5 -2
- package/dist/utilitiesConfiguration.js +21 -23
- package/package.json +3 -3
|
@@ -1,12 +1,3 @@
|
|
|
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
1
|
import { assert } from "@configura/web-utilities";
|
|
11
2
|
import { CfgProductBubbleMode } from "../CfgProduct.js";
|
|
12
3
|
import { SelectionType, } from "../productConfiguration/CfgFeature.js";
|
|
@@ -57,11 +48,9 @@ export class SyncGroupsTransaction {
|
|
|
57
48
|
this.target = target;
|
|
58
49
|
this.initial = initial;
|
|
59
50
|
}
|
|
60
|
-
static make(syncState, updateMode, product, productLoader, assumeNoStartState) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return t;
|
|
64
|
-
});
|
|
51
|
+
static async make(syncState, updateMode, product, productLoader, assumeNoStartState) {
|
|
52
|
+
const t = new this(syncState, updateMode, productLoader, product, await product.clone(), assumeNoStartState ? undefined : await product.clone());
|
|
53
|
+
return t;
|
|
65
54
|
}
|
|
66
55
|
/************************************************************************
|
|
67
56
|
* Public API (intentionally limited)
|
|
@@ -79,59 +68,55 @@ export class SyncGroupsTransaction {
|
|
|
79
68
|
* but as this method will hand over to applyProduct if it updates the SyncState it can cause
|
|
80
69
|
* the Features to change. And we need to pass that information back to know when to stop.
|
|
81
70
|
*/
|
|
82
|
-
applyRootProduct() {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return yield this.updateRootProduct(undefined);
|
|
90
|
-
});
|
|
71
|
+
async applyRootProduct() {
|
|
72
|
+
const productWithInitial = { target: this.target, initial: this.initial };
|
|
73
|
+
if (!(await this.applyProduct(productWithInitial))) {
|
|
74
|
+
// All done!
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return await this.updateRootProduct(undefined);
|
|
91
78
|
}
|
|
92
79
|
/**
|
|
93
80
|
* This is the entry point for an active (often user) selection of an option.
|
|
94
81
|
*/
|
|
95
|
-
selectOption(optionPath, on) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
change = true;
|
|
111
|
-
switch (feature.selectionType) {
|
|
112
|
-
case SelectionType.SelectOne:
|
|
113
|
-
this.affectedSelectOneFeatures.set(feature, AffectedLevel.Set);
|
|
114
|
-
break;
|
|
115
|
-
case SelectionType.SelectMany:
|
|
116
|
-
this.affectedSelectManyOptions.add(option);
|
|
117
|
-
break;
|
|
118
|
-
default:
|
|
119
|
-
throw new Error("Should not happen");
|
|
120
|
-
}
|
|
121
|
-
}
|
|
82
|
+
async selectOption(optionPath, on) {
|
|
83
|
+
const option = SyncGroupsPathHelper.getOptionFromPath(optionPath, this.target);
|
|
84
|
+
const feature = option.parent;
|
|
85
|
+
let change = false;
|
|
86
|
+
let productToValidate = undefined;
|
|
87
|
+
// At this point in handling sync groups we go back to normal selecting.
|
|
88
|
+
//
|
|
89
|
+
// This is because a normal selection can bring features into scope which should push to or
|
|
90
|
+
// (if the SyncGroup is defined) pull from the Sync State.
|
|
91
|
+
//
|
|
92
|
+
// We use BubbleSelected (without validation) as we can delay the validation until after we
|
|
93
|
+
// have applied the SyncGroup (if any).
|
|
94
|
+
if (await feature.selectOption(option, on, ProductConfigurationBubbleMode.BubbleSelected)) {
|
|
95
|
+
productToValidate = feature.parentProduct;
|
|
96
|
+
change = true;
|
|
122
97
|
switch (feature.selectionType) {
|
|
123
98
|
case SelectionType.SelectOne:
|
|
124
|
-
this.
|
|
99
|
+
this.affectedSelectOneFeatures.set(feature, AffectedLevel.Set);
|
|
125
100
|
break;
|
|
126
101
|
case SelectionType.SelectMany:
|
|
127
|
-
this.
|
|
102
|
+
this.affectedSelectManyOptions.add(option);
|
|
128
103
|
break;
|
|
104
|
+
default:
|
|
105
|
+
throw new Error("Should not happen");
|
|
129
106
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
107
|
+
}
|
|
108
|
+
switch (feature.selectionType) {
|
|
109
|
+
case SelectionType.SelectOne:
|
|
110
|
+
this.applySelectOneFeature(feature, true, false);
|
|
111
|
+
break;
|
|
112
|
+
case SelectionType.SelectMany:
|
|
113
|
+
this.applySelectManyOption(option, true);
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
if (await this.updateRootProduct(productToValidate)) {
|
|
117
|
+
change = true;
|
|
118
|
+
}
|
|
119
|
+
return change;
|
|
135
120
|
}
|
|
136
121
|
/**
|
|
137
122
|
* Overwrites the original Product and SyncGroupState (supplied when creating the Transaction)
|
|
@@ -139,12 +124,10 @@ export class SyncGroupsTransaction {
|
|
|
139
124
|
*
|
|
140
125
|
* @throws error if the transaction has already been closed.
|
|
141
126
|
*/
|
|
142
|
-
commit() {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
yield this.originalProduct.copyFrom(this.target, false, this.productLoader);
|
|
147
|
-
});
|
|
127
|
+
async commit() {
|
|
128
|
+
assert(this.isClosed === false, "Trying to commit a closed Transaction");
|
|
129
|
+
this.originalSyncState.copyFrom(this.syncState);
|
|
130
|
+
await this.originalProduct.copyFrom(this.target, false, this.productLoader);
|
|
148
131
|
}
|
|
149
132
|
/************************************************************************
|
|
150
133
|
* Updating things with the Transaction's SyncState
|
|
@@ -153,81 +136,76 @@ export class SyncGroupsTransaction {
|
|
|
153
136
|
* Apply current sync groups on those who wants to listen until there is no more to settle.
|
|
154
137
|
* @returns true if at least one Feature changed selected Option
|
|
155
138
|
*/
|
|
156
|
-
updateRootProduct(productToValidate) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
139
|
+
async updateRootProduct(productToValidate) {
|
|
140
|
+
const productsToValidate = new Set();
|
|
141
|
+
if (productToValidate !== undefined) {
|
|
142
|
+
productsToValidate.add(productToValidate);
|
|
143
|
+
}
|
|
144
|
+
const productWithInitial = { target: this.target, initial: this.initial };
|
|
145
|
+
await this.updateProduct(productWithInitial, productsToValidate);
|
|
146
|
+
if (productsToValidate.size === 0) {
|
|
147
|
+
// All have been settled, continue to pullPhase
|
|
148
|
+
return await this.applyRootProduct();
|
|
149
|
+
}
|
|
150
|
+
if (this.isClosed) {
|
|
151
|
+
// We could exit in more places when the transaction has been aborted, but as
|
|
152
|
+
// revalidate is really the only thing that could be expensive time consuming we only
|
|
153
|
+
// check here.
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
let anyChanges = false;
|
|
157
|
+
for (const product of productsToValidate) {
|
|
158
|
+
anyChanges =
|
|
159
|
+
(await product._revalidate(CfgProductBubbleMode.ToRoot, this.productLoader, true)) || anyChanges;
|
|
160
|
+
const diff = product.takeDiff();
|
|
161
|
+
if (diff)
|
|
162
|
+
this.applyFromDiff(diff);
|
|
163
|
+
}
|
|
164
|
+
if (!anyChanges) {
|
|
165
|
+
this.close();
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
// Apply over again, to settle deeper down. Our theory is that the front of "settled" will
|
|
169
|
+
// move deeper and deeper into the tree.
|
|
170
|
+
await this.updateRootProduct(undefined);
|
|
171
|
+
// We had productsToValidate, and so there must have been a change.
|
|
172
|
+
return true;
|
|
189
173
|
}
|
|
190
174
|
/**
|
|
191
175
|
* Applies the SyncState to the Product and it's AdditionalProducts (sub-products).
|
|
192
176
|
* @param productsToValidate To this all products that will need validation are added.
|
|
193
177
|
*/
|
|
194
|
-
updateProduct(product, productsToValidate) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
yield Promise.all(promises);
|
|
202
|
-
});
|
|
178
|
+
async updateProduct(product, productsToValidate) {
|
|
179
|
+
const promises = [];
|
|
180
|
+
promises.push(this.updateFeatures(getFeaturesFromProduct(product), productsToValidate));
|
|
181
|
+
for (const additionalProduct of getAdditionalProducts(product)) {
|
|
182
|
+
promises.push(this.updateProduct(additionalProduct, productsToValidate));
|
|
183
|
+
}
|
|
184
|
+
await Promise.all(promises);
|
|
203
185
|
}
|
|
204
186
|
/**
|
|
205
187
|
* Applies the SyncState to an array of Features.
|
|
206
188
|
* @param productsToValidate To this all products that will need validation are added.
|
|
207
189
|
*/
|
|
208
|
-
updateFeatures(features, productsToValidate) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
})));
|
|
219
|
-
});
|
|
190
|
+
async updateFeatures(features, productsToValidate) {
|
|
191
|
+
await Promise.all(features.map(async (feature) => {
|
|
192
|
+
switch (await this.updateFeature(feature, productsToValidate)) {
|
|
193
|
+
case "stop":
|
|
194
|
+
return;
|
|
195
|
+
case "recurseDown":
|
|
196
|
+
await this.updateOptions(getSelectedOptions(feature), productsToValidate);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
}));
|
|
220
200
|
}
|
|
221
201
|
/**
|
|
222
202
|
* Applies the SyncState to an array of Options.
|
|
223
203
|
* @param productsToValidate To this all products that will need validation are added.
|
|
224
204
|
*/
|
|
225
|
-
updateOptions(options, productsToValidate) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}));
|
|
230
|
-
});
|
|
205
|
+
async updateOptions(options, productsToValidate) {
|
|
206
|
+
await Promise.all(options.map((option) => {
|
|
207
|
+
return this.updateFeatures(getFeaturesFromOption(option), productsToValidate);
|
|
208
|
+
}));
|
|
231
209
|
}
|
|
232
210
|
/**
|
|
233
211
|
* Applies the SyncState to a Feature
|
|
@@ -235,30 +213,28 @@ export class SyncGroupsTransaction {
|
|
|
235
213
|
* @returns Whether we shall stop recursing down (because we are in a state which we
|
|
236
214
|
* expect to be resolved later), we shall continue recursing down.
|
|
237
215
|
*/
|
|
238
|
-
updateFeature(featureWithInitial, productsToValidate) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
216
|
+
async updateFeature(featureWithInitial, productsToValidate) {
|
|
217
|
+
const feature = featureWithInitial.target;
|
|
218
|
+
const syncCode = feature.getSyncCode("pull");
|
|
219
|
+
if (syncCode === undefined) {
|
|
220
|
+
// Just continue down, features with no sync groups are always settled
|
|
221
|
+
return "recurseDown";
|
|
222
|
+
}
|
|
223
|
+
if (syncCode === false) {
|
|
224
|
+
// Here we only handle pull. Initializing those missing in SyncMap
|
|
225
|
+
// we do later. We wait until we have settled as much as we can with what
|
|
226
|
+
// we have now, to increase chances of the values being written to the
|
|
227
|
+
// SyncState being the right ones.
|
|
228
|
+
return "stop";
|
|
229
|
+
}
|
|
230
|
+
switch (feature.selectionType) {
|
|
231
|
+
case SelectionType.Group:
|
|
244
232
|
return "recurseDown";
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
// SyncState being the right ones.
|
|
251
|
-
return "stop";
|
|
252
|
-
}
|
|
253
|
-
switch (feature.selectionType) {
|
|
254
|
-
case SelectionType.Group:
|
|
255
|
-
return "recurseDown";
|
|
256
|
-
case SelectionType.SelectOne:
|
|
257
|
-
return yield this.updateSelectOneFeature(syncCode, featureWithInitial, productsToValidate);
|
|
258
|
-
case SelectionType.SelectMany:
|
|
259
|
-
return yield this.updateSelectManyFeature(syncCode, featureWithInitial, productsToValidate);
|
|
260
|
-
}
|
|
261
|
-
});
|
|
233
|
+
case SelectionType.SelectOne:
|
|
234
|
+
return await this.updateSelectOneFeature(syncCode, featureWithInitial, productsToValidate);
|
|
235
|
+
case SelectionType.SelectMany:
|
|
236
|
+
return await this.updateSelectManyFeature(syncCode, featureWithInitial, productsToValidate);
|
|
237
|
+
}
|
|
262
238
|
}
|
|
263
239
|
/**
|
|
264
240
|
* Decides if the SyncState can be applied to the SelectOne Feature, and then changes
|
|
@@ -268,48 +244,46 @@ export class SyncGroupsTransaction {
|
|
|
268
244
|
* @returns Whether we shall stop recursing down (because we are in a state which we
|
|
269
245
|
* expect to be resolved later), we shall continue recursing down.
|
|
270
246
|
*/
|
|
271
|
-
updateSelectOneFeature(syncCode, featureWithInitial, productsToValidate) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
// This branch will have to be settled later. Don't go further down here.
|
|
293
|
-
return "stop";
|
|
294
|
-
}
|
|
295
|
-
const selectedOption = feature.selectedOptions[0];
|
|
296
|
-
if (selectedOption !== undefined && selectedOption.code === currentSyncGroupValue) {
|
|
297
|
-
// Settled, continue
|
|
298
|
-
return "recurseDown";
|
|
299
|
-
}
|
|
300
|
-
const optionToSelect = feature.options.find((o) => o.code === currentSyncGroupValue);
|
|
301
|
-
if (optionToSelect === undefined) {
|
|
302
|
-
// No option which can be selected (keep current value and recurse down)
|
|
303
|
-
return "recurseDown";
|
|
304
|
-
}
|
|
305
|
-
// Update the value. Validations are collected so that we do not send more than necessary.
|
|
306
|
-
// Do not recurse further as we will change the state and so what is selected now won't be
|
|
307
|
-
// selected then.
|
|
308
|
-
yield feature.selectOption(optionToSelect._internal, true, ProductConfigurationBubbleMode.ToRoot);
|
|
309
|
-
this.affectedSelectOneFeatures.set(feature, syncGroupHasBeenUpdated ? AffectedLevel.Set : AffectedLevel.Initialized);
|
|
310
|
-
productsToValidate.add(feature.parentProduct);
|
|
247
|
+
async updateSelectOneFeature(syncCode, featureWithInitial, productsToValidate) {
|
|
248
|
+
const feature = featureWithInitial.target;
|
|
249
|
+
const featureDidJustComeIntoScope = featureWithInitial.initial === undefined;
|
|
250
|
+
const hasBeenAffectedLevel = this.affectedSelectOneFeatures.get(feature);
|
|
251
|
+
const syncGroupHasBeenUpdated = this.affectedSelectOneSyncGroups.has(syncCode);
|
|
252
|
+
if (hasBeenAffectedLevel === AffectedLevel.Set ||
|
|
253
|
+
(!syncGroupHasBeenUpdated && hasBeenAffectedLevel === AffectedLevel.Initialized)) {
|
|
254
|
+
// A feature can change value for two reasons:
|
|
255
|
+
// 1. The feature did just come into scope and loads from the sync group to set its "defaults"
|
|
256
|
+
// 2. The sync group has updated with a new value
|
|
257
|
+
// The rule is that in one sync group transaction the feature value is allowed to be updated
|
|
258
|
+
// once or twice. Once for initialization and once for new sync group value. But they have to
|
|
259
|
+
// happen in the right order.
|
|
260
|
+
return "recurseDown";
|
|
261
|
+
}
|
|
262
|
+
if (!featureDidJustComeIntoScope && !syncGroupHasBeenUpdated) {
|
|
263
|
+
return "recurseDown";
|
|
264
|
+
}
|
|
265
|
+
const currentSyncGroupValue = this.syncState.getForSelectOne(syncCode);
|
|
266
|
+
if (currentSyncGroupValue === undefined) {
|
|
267
|
+
// This branch will have to be settled later. Don't go further down here.
|
|
311
268
|
return "stop";
|
|
312
|
-
}
|
|
269
|
+
}
|
|
270
|
+
const selectedOption = feature.selectedOptions[0];
|
|
271
|
+
if (selectedOption !== undefined && selectedOption.code === currentSyncGroupValue) {
|
|
272
|
+
// Settled, continue
|
|
273
|
+
return "recurseDown";
|
|
274
|
+
}
|
|
275
|
+
const optionToSelect = feature.options.find((o) => o.code === currentSyncGroupValue);
|
|
276
|
+
if (optionToSelect === undefined) {
|
|
277
|
+
// No option which can be selected (keep current value and recurse down)
|
|
278
|
+
return "recurseDown";
|
|
279
|
+
}
|
|
280
|
+
// Update the value. Validations are collected so that we do not send more than necessary.
|
|
281
|
+
// Do not recurse further as we will change the state and so what is selected now won't be
|
|
282
|
+
// selected then.
|
|
283
|
+
await feature.selectOption(optionToSelect._internal, true, ProductConfigurationBubbleMode.ToRoot);
|
|
284
|
+
this.affectedSelectOneFeatures.set(feature, syncGroupHasBeenUpdated ? AffectedLevel.Set : AffectedLevel.Initialized);
|
|
285
|
+
productsToValidate.add(feature.parentProduct);
|
|
286
|
+
return "stop";
|
|
313
287
|
}
|
|
314
288
|
/**
|
|
315
289
|
* Decides if the SyncState can be applied to Options in the SelectMany Feature, and
|
|
@@ -318,22 +292,20 @@ export class SyncGroupsTransaction {
|
|
|
318
292
|
* @param productsToValidate To this all products that will need validation are added
|
|
319
293
|
* @returns Always "stop" as recursion is handled internally. Return for consistency.
|
|
320
294
|
*/
|
|
321
|
-
updateSelectManyFeature(syncCode, feature, productsToValidate) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
continue;
|
|
331
|
-
}
|
|
295
|
+
async updateSelectManyFeature(syncCode, feature, productsToValidate) {
|
|
296
|
+
const optionsToContinueDown = [];
|
|
297
|
+
for (const option of getOptions(feature)) {
|
|
298
|
+
switch (await this.updateSelectManyOption(syncCode, option, productsToValidate)) {
|
|
299
|
+
case "stop":
|
|
300
|
+
continue;
|
|
301
|
+
case "recurseDown":
|
|
302
|
+
optionsToContinueDown.push(option);
|
|
303
|
+
continue;
|
|
332
304
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
305
|
+
}
|
|
306
|
+
await this.updateOptions(optionsToContinueDown, productsToValidate);
|
|
307
|
+
// stop as the method above handles the recursion down
|
|
308
|
+
return "stop";
|
|
337
309
|
}
|
|
338
310
|
/**
|
|
339
311
|
* Decides if the SyncState can be applied to the SelectMany Option, and then changes
|
|
@@ -343,108 +315,100 @@ export class SyncGroupsTransaction {
|
|
|
343
315
|
* @returns Whether we shall stop recursing down (because we are in a state which we
|
|
344
316
|
* expect to be resolved later), we shall continue recursing down.
|
|
345
317
|
*/
|
|
346
|
-
updateSelectManyOption(syncCode, optionWithInitial, productsToValidate) {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
return "stop";
|
|
380
|
-
});
|
|
318
|
+
async updateSelectManyOption(syncCode, optionWithInitial, productsToValidate) {
|
|
319
|
+
const option = optionWithInitial.target;
|
|
320
|
+
const featureDidJustComeIntoScope = optionWithInitial.initial === undefined;
|
|
321
|
+
const optionSelected = option.selected;
|
|
322
|
+
const recurseOrStopIfNoChange = optionSelected ? "recurseDown" : "stop";
|
|
323
|
+
if (this.affectedSelectManyOptions.has(option)) {
|
|
324
|
+
// This option has already changed selection once for this transaction. We expect
|
|
325
|
+
// this to happen very rarely, as the algorithm should settle the selection tree
|
|
326
|
+
// further and further out. Nevertheless, this safeguard is needed to avoid infinite
|
|
327
|
+
// looping if for example the server would return the same data over and over.
|
|
328
|
+
return recurseOrStopIfNoChange;
|
|
329
|
+
}
|
|
330
|
+
if (!featureDidJustComeIntoScope &&
|
|
331
|
+
!this.hasSyncGroupAffectedForSelectMany(syncCode, option)) {
|
|
332
|
+
return "recurseDown";
|
|
333
|
+
}
|
|
334
|
+
const syncGroupValueForOption = this.syncState.getForSelectMany(syncCode, option.code);
|
|
335
|
+
if (syncGroupValueForOption === undefined) {
|
|
336
|
+
// The sync group has no opinion on this. If it is selected, just continue down.
|
|
337
|
+
return recurseOrStopIfNoChange;
|
|
338
|
+
}
|
|
339
|
+
if (syncGroupValueForOption === optionSelected) {
|
|
340
|
+
// We are in sync for this option
|
|
341
|
+
return recurseOrStopIfNoChange;
|
|
342
|
+
}
|
|
343
|
+
const feature = option.parent;
|
|
344
|
+
// Update the value. Validations are collected so that we do not
|
|
345
|
+
// send more than necessary. Do not recurse further as we will change
|
|
346
|
+
// the state and so what is selected now won't be selected then.
|
|
347
|
+
await feature.selectOption(option, syncGroupValueForOption, ProductConfigurationBubbleMode.ToRoot);
|
|
348
|
+
this.affectedSelectManyOptions.add(option);
|
|
349
|
+
productsToValidate.add(feature.parentProduct);
|
|
350
|
+
return "stop";
|
|
381
351
|
}
|
|
382
352
|
/************************************************************************
|
|
383
353
|
* Applying things to the Transaction's SyncState
|
|
384
354
|
************************************************************************/
|
|
385
|
-
applyProduct(product) {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
if (
|
|
355
|
+
async applyProduct(product) {
|
|
356
|
+
let change = false;
|
|
357
|
+
if (await this.applyFeatures(getFeaturesFromProduct(product))) {
|
|
358
|
+
if (this.updateMode === SyncGroupsApplyMode.Strict) {
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
change = true;
|
|
362
|
+
}
|
|
363
|
+
for (const additionalProduct of getAdditionalProducts(product)) {
|
|
364
|
+
if (await this.applyProduct(additionalProduct)) {
|
|
389
365
|
if (this.updateMode === SyncGroupsApplyMode.Strict) {
|
|
390
366
|
return true;
|
|
391
367
|
}
|
|
392
368
|
change = true;
|
|
393
369
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
370
|
+
}
|
|
371
|
+
return change;
|
|
372
|
+
}
|
|
373
|
+
async applyFeatures(featureWithInitials) {
|
|
374
|
+
let change = false;
|
|
375
|
+
for (const featureWithInitial of featureWithInitials) {
|
|
376
|
+
const feature = featureWithInitial.target;
|
|
377
|
+
switch (feature.selectionType) {
|
|
378
|
+
case SelectionType.SelectOne:
|
|
379
|
+
if (this.applySelectOneFeature(feature, false, featureWithInitial.initial === undefined)) {
|
|
380
|
+
change = true;
|
|
398
381
|
}
|
|
399
|
-
|
|
400
|
-
|
|
382
|
+
break;
|
|
383
|
+
case SelectionType.SelectMany:
|
|
384
|
+
if (this.applySelectManyFeature(featureWithInitial)) {
|
|
385
|
+
change = true;
|
|
386
|
+
}
|
|
387
|
+
break;
|
|
401
388
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
let change = false;
|
|
408
|
-
for (const featureWithInitial of featureWithInitials) {
|
|
409
|
-
const feature = featureWithInitial.target;
|
|
410
|
-
switch (feature.selectionType) {
|
|
411
|
-
case SelectionType.SelectOne:
|
|
412
|
-
if (this.applySelectOneFeature(feature, false, featureWithInitial.initial === undefined)) {
|
|
413
|
-
change = true;
|
|
414
|
-
}
|
|
415
|
-
break;
|
|
416
|
-
case SelectionType.SelectMany:
|
|
417
|
-
if (this.applySelectManyFeature(featureWithInitial)) {
|
|
418
|
-
change = true;
|
|
419
|
-
}
|
|
420
|
-
break;
|
|
421
|
-
}
|
|
422
|
-
if (change && this.updateMode === SyncGroupsApplyMode.Strict) {
|
|
389
|
+
if (change && this.updateMode === SyncGroupsApplyMode.Strict) {
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
if (await this.applyOptions(getSelectedOptions(featureWithInitial))) {
|
|
393
|
+
if (this.updateMode === SyncGroupsApplyMode.Strict) {
|
|
423
394
|
return true;
|
|
424
395
|
}
|
|
425
|
-
|
|
426
|
-
if (this.updateMode === SyncGroupsApplyMode.Strict) {
|
|
427
|
-
return true;
|
|
428
|
-
}
|
|
429
|
-
change = true;
|
|
430
|
-
}
|
|
396
|
+
change = true;
|
|
431
397
|
}
|
|
432
|
-
|
|
433
|
-
|
|
398
|
+
}
|
|
399
|
+
return change;
|
|
434
400
|
}
|
|
435
|
-
applyOptions(options) {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
if (
|
|
440
|
-
|
|
441
|
-
return true;
|
|
442
|
-
}
|
|
443
|
-
change = true;
|
|
401
|
+
async applyOptions(options) {
|
|
402
|
+
let change = false;
|
|
403
|
+
for (const option of options) {
|
|
404
|
+
if (await this.applyFeatures(getFeaturesFromOption(option))) {
|
|
405
|
+
if (this.updateMode === SyncGroupsApplyMode.Strict) {
|
|
406
|
+
return true;
|
|
444
407
|
}
|
|
408
|
+
change = true;
|
|
445
409
|
}
|
|
446
|
-
|
|
447
|
-
|
|
410
|
+
}
|
|
411
|
+
return change;
|
|
448
412
|
}
|
|
449
413
|
applySelectOneFeature(feature, userInitiated, featureDidJustComeIntoScope) {
|
|
450
414
|
const selectionType = feature.selectionType;
|
|
@@ -525,6 +489,44 @@ export class SyncGroupsTransaction {
|
|
|
525
489
|
this.syncState.setForSelectMany(syncCode, option.code, optionSelected);
|
|
526
490
|
return true;
|
|
527
491
|
}
|
|
492
|
+
/**
|
|
493
|
+
* Applies the given diff to the transaction's sync state.
|
|
494
|
+
*/
|
|
495
|
+
applyFromDiff(diffs) {
|
|
496
|
+
for (const diff of diffs) {
|
|
497
|
+
this.applyNewStateFor(diff);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Applies the new state for the given option to the transaction's sync state.
|
|
502
|
+
*/
|
|
503
|
+
applyNewStateFor(option) {
|
|
504
|
+
const feature = option.parent;
|
|
505
|
+
const syncCode = feature.getSyncCode("push");
|
|
506
|
+
if (syncCode === undefined || syncCode === false)
|
|
507
|
+
return false;
|
|
508
|
+
switch (feature.selectionType) {
|
|
509
|
+
case SelectionType.SelectOne: {
|
|
510
|
+
if (this.affectedSelectOneSyncGroups.has(syncCode)) {
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
this.affectedSelectOneSyncGroups.add(syncCode);
|
|
514
|
+
this.syncState.setForSelectOne(syncCode, option.code);
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
case SelectionType.SelectMany: {
|
|
518
|
+
if (this.hasSyncGroupAffectedForSelectMany(syncCode, option)) {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
this.addSyncGroupAffectedForSelectMany(syncCode, option);
|
|
522
|
+
this.syncState.setForSelectMany(syncCode, option.code, option.selected);
|
|
523
|
+
return true;
|
|
524
|
+
}
|
|
525
|
+
case SelectionType.Group:
|
|
526
|
+
console.warn("No state to update for group: ", option.code);
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
528
530
|
addSyncGroupAffectedForSelectMany(syncCode, option) {
|
|
529
531
|
let forSyncCode = this.affectedSelectManySyncGroupsAndOptions.get(syncCode);
|
|
530
532
|
if (forSyncCode === undefined) {
|
|
@@ -534,9 +536,60 @@ export class SyncGroupsTransaction {
|
|
|
534
536
|
forSyncCode.add(option.code);
|
|
535
537
|
}
|
|
536
538
|
hasSyncGroupAffectedForSelectMany(syncCode, option) {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
+
return this.affectedSelectManySyncGroupsAndOptions.get(syncCode)?.has(option.code) === true;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
/************************************************************************
|
|
543
|
+
* Product diffing
|
|
544
|
+
************************************************************************/
|
|
545
|
+
/**
|
|
546
|
+
* Finds all options that have changed in the new product compared to the original product.
|
|
547
|
+
*
|
|
548
|
+
* This is used to determine which options need to be updated in the sync state.
|
|
549
|
+
*/
|
|
550
|
+
export function generateSelectionDiff(originalProduct, newProduct) {
|
|
551
|
+
const diff = generateFeaturesDiff(getFeaturesFromProduct({ initial: originalProduct, target: newProduct }));
|
|
552
|
+
return diff.length ? diff : null;
|
|
553
|
+
}
|
|
554
|
+
function generateFeaturesDiff(features) {
|
|
555
|
+
const changed = [];
|
|
556
|
+
for (const feature of features) {
|
|
557
|
+
switch (feature.target.selectionType) {
|
|
558
|
+
case SelectionType.SelectOne: {
|
|
559
|
+
const option = generateDiffForSelectOne(feature);
|
|
560
|
+
if (option)
|
|
561
|
+
changed.push(option);
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
case SelectionType.SelectMany:
|
|
565
|
+
changed.push(...generateDiffForSelectMany(feature));
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
for (const option of getSelectedOptions(feature)) {
|
|
569
|
+
changed.push(...generateFeaturesDiff(getFeaturesFromOption(option)));
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return changed;
|
|
573
|
+
}
|
|
574
|
+
function generateDiffForSelectOne({ initial, target, }) {
|
|
575
|
+
if (!target.getSyncCode("push"))
|
|
576
|
+
return null;
|
|
577
|
+
const selected = target.selectedOptions[0];
|
|
578
|
+
if (selected && initial?.selectedOptions?.[0]?.code !== selected.code) {
|
|
579
|
+
return selected._internal;
|
|
580
|
+
}
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
function generateDiffForSelectMany(feature) {
|
|
584
|
+
const changed = [];
|
|
585
|
+
if (!feature.target.getSyncCode("push"))
|
|
586
|
+
return changed;
|
|
587
|
+
for (const { initial, target } of getOptions(feature)) {
|
|
588
|
+
if (initial?.selected !== target.selected) {
|
|
589
|
+
changed.push(target);
|
|
590
|
+
}
|
|
539
591
|
}
|
|
592
|
+
return changed;
|
|
540
593
|
}
|
|
541
594
|
function getAdditionalProducts(product) {
|
|
542
595
|
const initial = product.initial;
|
|
@@ -544,46 +597,42 @@ function getAdditionalProducts(product) {
|
|
|
544
597
|
.filter((p) => p.selected)
|
|
545
598
|
.map((childTarget) => {
|
|
546
599
|
const refKey = childTarget.refKey;
|
|
547
|
-
const childInitial = initial
|
|
600
|
+
const childInitial = initial?.additionalProducts.find((p) => refKey === p.refKey);
|
|
548
601
|
return {
|
|
549
602
|
target: childTarget._internal,
|
|
550
|
-
initial: childInitial
|
|
603
|
+
initial: childInitial?._internal,
|
|
551
604
|
};
|
|
552
605
|
});
|
|
553
606
|
}
|
|
554
607
|
function pairOptions(targets, initials) {
|
|
555
608
|
return targets.map((childTarget) => {
|
|
556
609
|
const key = childTarget.key;
|
|
557
|
-
const childInitial = initials
|
|
610
|
+
const childInitial = initials?.find((o) => key === o.key);
|
|
558
611
|
return {
|
|
559
612
|
target: childTarget._internal,
|
|
560
|
-
initial: childInitial
|
|
613
|
+
initial: childInitial?._internal,
|
|
561
614
|
};
|
|
562
615
|
});
|
|
563
616
|
}
|
|
564
617
|
function getSelectedOptions(feature) {
|
|
565
|
-
|
|
566
|
-
return pairOptions(feature.target.selectedOptions, (_a = feature.initial) === null || _a === void 0 ? void 0 : _a.selectedOptions);
|
|
618
|
+
return pairOptions(feature.target.selectedOptions, feature.initial?.selectedOptions);
|
|
567
619
|
}
|
|
568
620
|
function getOptions(feature) {
|
|
569
|
-
|
|
570
|
-
return pairOptions(feature.target.options, (_a = feature.initial) === null || _a === void 0 ? void 0 : _a.options);
|
|
621
|
+
return pairOptions(feature.target.options, feature.initial?.options);
|
|
571
622
|
}
|
|
572
623
|
function pairFeatures(targets, initials) {
|
|
573
624
|
return targets.map((childTarget) => {
|
|
574
625
|
const code = childTarget.code;
|
|
575
|
-
const childInitial = initials
|
|
626
|
+
const childInitial = initials?.find((f) => code === f.code);
|
|
576
627
|
return {
|
|
577
628
|
target: childTarget._internal,
|
|
578
|
-
initial: childInitial
|
|
629
|
+
initial: childInitial?._internal,
|
|
579
630
|
};
|
|
580
631
|
});
|
|
581
632
|
}
|
|
582
633
|
function getFeaturesFromProduct(product) {
|
|
583
|
-
|
|
584
|
-
return pairFeatures(product.target.configuration.features, (_a = product.initial) === null || _a === void 0 ? void 0 : _a.configuration.features);
|
|
634
|
+
return pairFeatures(product.target.configuration.features, product.initial?.configuration.features);
|
|
585
635
|
}
|
|
586
636
|
function getFeaturesFromOption(option) {
|
|
587
|
-
|
|
588
|
-
return pairFeatures(option.target.features, (_a = option.initial) === null || _a === void 0 ? void 0 : _a.getFeaturesOutsideSync());
|
|
637
|
+
return pairFeatures(option.target.features, option.initial?.getFeaturesOutsideSync());
|
|
589
638
|
}
|