@jskit-ai/users-web 0.1.69 → 0.1.71
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/package.descriptor.mjs +7 -7
- package/package.json +11 -8
- package/src/client/bootstrap/user-bootstrap-handler.js +53 -0
- package/src/client/composables/crud/crudSchemaFormHelpers.js +1 -22
- package/src/client/composables/internal/crudListParentTitleSupport.js +14 -15
- package/src/client/composables/records/useAddEdit.js +23 -4
- package/src/client/composables/records/useCrudAddEdit.js +5 -20
- package/src/client/composables/records/useList.js +22 -22
- package/src/client/composables/records/useView.js +13 -27
- package/src/client/composables/runtime/operationValidationHelpers.js +18 -19
- package/src/client/composables/runtime/useAddEditCore.js +6 -10
- package/src/client/composables/runtime/useCommandCore.js +3 -10
- package/src/client/composables/runtime/useEndpointResource.js +75 -14
- package/src/client/composables/runtime/useListCore.js +45 -17
- package/src/client/composables/support/requestQueryRuntimeSupport.js +100 -0
- package/src/client/composables/useAccountSettingsRuntime.js +26 -19
- package/src/client/composables/useCommand.js +4 -2
- package/src/client/composables/useCrudListFilters.js +58 -255
- package/src/client/providers/UsersWebClientProvider.js +4 -2
- package/test/bootstrap.test.js +130 -0
- package/test/operationValidationHelpers.test.js +64 -0
- package/test/requestTransportOptions.test.js +107 -0
- package/test/useAddEditCore.test.js +124 -0
- package/test/useAddEditRequestQueryParams.test.js +162 -0
- package/test/useCrudAddEdit.test.js +1 -51
- package/test/useCrudListFilters.test.js +33 -4
- package/test/useCrudListParentTitle.test.js +40 -27
- package/test/useViewRequestQueryParams.test.js +10 -10
- package/src/client/composables/support/requestQueryPathSupport.js +0 -31
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { computed, reactive
|
|
2
|
-
import { normalizeText
|
|
1
|
+
import { computed, reactive } from "vue";
|
|
2
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
3
3
|
import {
|
|
4
4
|
defineCrudListFilters,
|
|
5
|
+
createCrudListFilterInitialValue,
|
|
6
|
+
isCrudListFilterMultiValue,
|
|
7
|
+
isCrudListFilterStructuredValue,
|
|
8
|
+
normalizeCrudListFilterUiValue,
|
|
9
|
+
areCrudListFilterUiValuesEqual,
|
|
10
|
+
listCrudListFilterChipValues,
|
|
11
|
+
formatCrudListFilterDefaultChipLabel,
|
|
12
|
+
formatCrudListFilterQueryValue,
|
|
5
13
|
resolveCrudListFilterOptionLabel,
|
|
6
|
-
CRUD_LIST_FILTER_TYPE_FLAG
|
|
7
|
-
CRUD_LIST_FILTER_TYPE_ENUM,
|
|
8
|
-
CRUD_LIST_FILTER_TYPE_ENUM_MANY,
|
|
9
|
-
CRUD_LIST_FILTER_TYPE_RECORD_ID,
|
|
10
|
-
CRUD_LIST_FILTER_TYPE_RECORD_ID_MANY,
|
|
11
|
-
CRUD_LIST_FILTER_TYPE_DATE,
|
|
12
|
-
CRUD_LIST_FILTER_TYPE_DATE_RANGE,
|
|
13
|
-
CRUD_LIST_FILTER_TYPE_NUMBER_RANGE,
|
|
14
|
-
CRUD_LIST_FILTER_TYPE_PRESENCE
|
|
14
|
+
CRUD_LIST_FILTER_TYPE_FLAG
|
|
15
15
|
} from "@jskit-ai/kernel/shared/support/crudListFilters";
|
|
16
16
|
|
|
17
17
|
function normalizeFunctionMap(value = {}) {
|
|
@@ -32,29 +32,6 @@ function normalizeFunctionMap(value = {}) {
|
|
|
32
32
|
return Object.freeze(normalized);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
function createInitialFilterValue(filter = {}) {
|
|
36
|
-
if (filter.type === CRUD_LIST_FILTER_TYPE_FLAG) {
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
if (filter.type === CRUD_LIST_FILTER_TYPE_ENUM_MANY || filter.type === CRUD_LIST_FILTER_TYPE_RECORD_ID_MANY) {
|
|
40
|
-
return [];
|
|
41
|
-
}
|
|
42
|
-
if (filter.type === CRUD_LIST_FILTER_TYPE_DATE_RANGE) {
|
|
43
|
-
return reactive({
|
|
44
|
-
from: "",
|
|
45
|
-
to: ""
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
if (filter.type === CRUD_LIST_FILTER_TYPE_NUMBER_RANGE) {
|
|
49
|
-
return reactive({
|
|
50
|
-
min: "",
|
|
51
|
-
max: ""
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return "";
|
|
56
|
-
}
|
|
57
|
-
|
|
58
35
|
function normalizePresetEntries(presets = []) {
|
|
59
36
|
const source = Array.isArray(presets) ? presets : [];
|
|
60
37
|
const normalized = [];
|
|
@@ -105,150 +82,52 @@ function resolvePresetValues(preset = {}, { values = {}, filters = {} } = {}) {
|
|
|
105
82
|
: {};
|
|
106
83
|
}
|
|
107
84
|
|
|
108
|
-
function
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (filter.type === CRUD_LIST_FILTER_TYPE_ENUM_MANY || filter.type === CRUD_LIST_FILTER_TYPE_RECORD_ID_MANY) {
|
|
114
|
-
const allowedValues = filter.type === CRUD_LIST_FILTER_TYPE_ENUM_MANY
|
|
115
|
-
? new Set((filter.options || []).map((entry) => entry.value))
|
|
116
|
-
: null;
|
|
117
|
-
const normalizedList = normalizeUniqueTextList(rawValue, {
|
|
118
|
-
acceptSingle: true
|
|
119
|
-
});
|
|
120
|
-
if (!allowedValues) {
|
|
121
|
-
return normalizedList;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return normalizedList.filter((entry) => allowedValues.has(entry));
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (filter.type === CRUD_LIST_FILTER_TYPE_DATE_RANGE) {
|
|
128
|
-
const source = rawValue && typeof rawValue === "object" && !Array.isArray(rawValue)
|
|
129
|
-
? rawValue
|
|
130
|
-
: {};
|
|
131
|
-
return {
|
|
132
|
-
from: normalizeText(source.from),
|
|
133
|
-
to: normalizeText(source.to)
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (filter.type === CRUD_LIST_FILTER_TYPE_NUMBER_RANGE) {
|
|
138
|
-
const source = rawValue && typeof rawValue === "object" && !Array.isArray(rawValue)
|
|
139
|
-
? rawValue
|
|
140
|
-
: {};
|
|
141
|
-
return {
|
|
142
|
-
min: normalizeText(source.min),
|
|
143
|
-
max: normalizeText(source.max)
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const normalized = normalizeText(rawValue);
|
|
148
|
-
if (filter.type === CRUD_LIST_FILTER_TYPE_ENUM || filter.type === CRUD_LIST_FILTER_TYPE_PRESENCE) {
|
|
149
|
-
const allowedValues = new Set((filter.options || []).map((entry) => entry.value));
|
|
150
|
-
return allowedValues.has(normalized) ? normalized : "";
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return normalized;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function normalizeCurrentManyFilterValues(value) {
|
|
157
|
-
const source = Array.isArray(value) ? value : [value];
|
|
158
|
-
return source
|
|
159
|
-
.map((entry) => normalizeText(entry))
|
|
160
|
-
.filter(Boolean);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function matchArrayValues(currentValue = [], expectedValue = []) {
|
|
164
|
-
const currentList = Array.isArray(currentValue) ? [...currentValue].sort() : [];
|
|
165
|
-
const expectedList = Array.isArray(expectedValue) ? [...expectedValue].sort() : [];
|
|
166
|
-
if (currentList.length !== expectedList.length) {
|
|
167
|
-
return false;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return currentList.every((entry, index) => entry === expectedList[index]);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function matchesPresetFilterValue(filter = {}, currentValue, rawExpectedValue) {
|
|
174
|
-
const expectedValue = normalizePresetFilterValue(filter, rawExpectedValue);
|
|
175
|
-
|
|
176
|
-
if (filter.type === CRUD_LIST_FILTER_TYPE_ENUM_MANY || filter.type === CRUD_LIST_FILTER_TYPE_RECORD_ID_MANY) {
|
|
177
|
-
return matchArrayValues(normalizeCurrentManyFilterValues(currentValue), expectedValue);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const normalizedCurrentValue = normalizePresetFilterValue(filter, currentValue);
|
|
181
|
-
|
|
182
|
-
if (filter.type === CRUD_LIST_FILTER_TYPE_DATE_RANGE) {
|
|
183
|
-
return (
|
|
184
|
-
normalizeText(normalizedCurrentValue?.from) === normalizeText(expectedValue?.from) &&
|
|
185
|
-
normalizeText(normalizedCurrentValue?.to) === normalizeText(expectedValue?.to)
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (filter.type === CRUD_LIST_FILTER_TYPE_NUMBER_RANGE) {
|
|
190
|
-
return (
|
|
191
|
-
normalizeText(normalizedCurrentValue?.min) === normalizeText(expectedValue?.min) &&
|
|
192
|
-
normalizeText(normalizedCurrentValue?.max) === normalizeText(expectedValue?.max)
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return normalizedCurrentValue === expectedValue;
|
|
85
|
+
function createRuntimeFilterValue(filter = {}) {
|
|
86
|
+
const initialValue = createCrudListFilterInitialValue(filter);
|
|
87
|
+
return isCrudListFilterStructuredValue(filter)
|
|
88
|
+
? reactive(initialValue)
|
|
89
|
+
: initialValue;
|
|
197
90
|
}
|
|
198
91
|
|
|
199
|
-
function
|
|
200
|
-
|
|
201
|
-
values[filter.key] = false;
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
92
|
+
function assignFilterValue(values, filter = {}, rawValue) {
|
|
93
|
+
const normalizedValue = normalizeCrudListFilterUiValue(filter, rawValue);
|
|
204
94
|
|
|
205
|
-
if (filter
|
|
206
|
-
values[filter.key] =
|
|
95
|
+
if (!isCrudListFilterStructuredValue(filter)) {
|
|
96
|
+
values[filter.key] = normalizedValue;
|
|
207
97
|
return;
|
|
208
98
|
}
|
|
209
99
|
|
|
210
|
-
if (filter.
|
|
211
|
-
values[filter.key]
|
|
212
|
-
values[filter.key].to = "";
|
|
100
|
+
if (values[filter.key] && typeof values[filter.key] === "object" && !Array.isArray(values[filter.key])) {
|
|
101
|
+
Object.assign(values[filter.key], normalizedValue);
|
|
213
102
|
return;
|
|
214
103
|
}
|
|
215
104
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
values[filter.key].max = "";
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
105
|
+
values[filter.key] = reactive(normalizedValue);
|
|
106
|
+
}
|
|
221
107
|
|
|
222
|
-
|
|
108
|
+
function resetFilterValue(values, filter = {}) {
|
|
109
|
+
assignFilterValue(values, filter, createCrudListFilterInitialValue(filter));
|
|
223
110
|
}
|
|
224
111
|
|
|
225
112
|
function applyPresetFilterValue(values, filter = {}, rawValue) {
|
|
226
|
-
|
|
227
|
-
const nextValue = normalizePresetFilterValue(filter, rawValue);
|
|
228
|
-
Object.assign(values[filter.key], nextValue);
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
values[filter.key] = normalizePresetFilterValue(filter, rawValue);
|
|
113
|
+
assignFilterValue(values, filter, rawValue);
|
|
233
114
|
}
|
|
234
115
|
|
|
235
116
|
function createQueryParams(values, filterEntries = []) {
|
|
236
117
|
const queryParams = {};
|
|
237
118
|
|
|
238
119
|
for (const filter of filterEntries) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
queryParams[filter.queryKey] = toRef(values, filter.key);
|
|
120
|
+
queryParams[filter.queryKey] = computed({
|
|
121
|
+
get() {
|
|
122
|
+
return formatCrudListFilterQueryValue(
|
|
123
|
+
filter,
|
|
124
|
+
normalizeCrudListFilterUiValue(filter, values[filter.key])
|
|
125
|
+
);
|
|
126
|
+
},
|
|
127
|
+
set(nextValue) {
|
|
128
|
+
assignFilterValue(values, filter, nextValue);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
252
131
|
}
|
|
253
132
|
|
|
254
133
|
return Object.freeze(queryParams);
|
|
@@ -268,46 +147,6 @@ function resolveAtomicValueLabel(filter = {}, value = "", labelResolvers = {}) {
|
|
|
268
147
|
});
|
|
269
148
|
}
|
|
270
149
|
|
|
271
|
-
function defaultChipLabel(filter = {}, value, labelResolvers = {}) {
|
|
272
|
-
if (filter.type === CRUD_LIST_FILTER_TYPE_FLAG) {
|
|
273
|
-
return filter.label;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
if (
|
|
277
|
-
filter.type === CRUD_LIST_FILTER_TYPE_ENUM ||
|
|
278
|
-
filter.type === CRUD_LIST_FILTER_TYPE_PRESENCE ||
|
|
279
|
-
filter.type === CRUD_LIST_FILTER_TYPE_RECORD_ID
|
|
280
|
-
) {
|
|
281
|
-
return `${filter.label}: ${resolveAtomicValueLabel(filter, value, labelResolvers)}`;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
if (filter.type === CRUD_LIST_FILTER_TYPE_DATE) {
|
|
285
|
-
return `${filter.label}: ${value}`;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (filter.type === CRUD_LIST_FILTER_TYPE_DATE_RANGE) {
|
|
289
|
-
if (value?.from && value?.to) {
|
|
290
|
-
return `${filter.label}: ${value.from} to ${value.to}`;
|
|
291
|
-
}
|
|
292
|
-
if (value?.from) {
|
|
293
|
-
return `${filter.label}: from ${value.from}`;
|
|
294
|
-
}
|
|
295
|
-
return `${filter.label}: to ${value?.to || ""}`;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (filter.type === CRUD_LIST_FILTER_TYPE_NUMBER_RANGE) {
|
|
299
|
-
if (value?.min && value?.max) {
|
|
300
|
-
return `${filter.label}: ${value.min} to ${value.max}`;
|
|
301
|
-
}
|
|
302
|
-
if (value?.min) {
|
|
303
|
-
return `${filter.label}: min ${value.min}`;
|
|
304
|
-
}
|
|
305
|
-
return `${filter.label}: max ${value?.max || ""}`;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
return filter.label;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
150
|
function useCrudListFilters(definitions = {}, { labelResolvers = {}, chipLabels = {}, presets = [] } = {}) {
|
|
312
151
|
const filters = defineCrudListFilters(definitions);
|
|
313
152
|
const filterEntries = Object.values(filters);
|
|
@@ -318,7 +157,7 @@ function useCrudListFilters(definitions = {}, { labelResolvers = {}, chipLabels
|
|
|
318
157
|
const options = {};
|
|
319
158
|
|
|
320
159
|
for (const filter of filterEntries) {
|
|
321
|
-
values[filter.key] =
|
|
160
|
+
values[filter.key] = createRuntimeFilterValue(filter);
|
|
322
161
|
if (Array.isArray(filter.options) && filter.options.length > 0) {
|
|
323
162
|
options[filter.key] = filter.options;
|
|
324
163
|
}
|
|
@@ -331,62 +170,22 @@ function useCrudListFilters(definitions = {}, { labelResolvers = {}, chipLabels
|
|
|
331
170
|
|
|
332
171
|
for (const filter of filterEntries) {
|
|
333
172
|
const customChipLabel = normalizedChipLabels[filter.key];
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
if (filter.type === CRUD_LIST_FILTER_TYPE_FLAG) {
|
|
337
|
-
if (rawValue === true) {
|
|
338
|
-
chips.push({
|
|
339
|
-
id: filter.key,
|
|
340
|
-
filterKey: filter.key,
|
|
341
|
-
label: normalizeText(customChipLabel?.(rawValue, filter, values))
|
|
342
|
-
|| normalizeText(filter.chipLabel?.(rawValue, filter, values))
|
|
343
|
-
|| defaultChipLabel(filter, rawValue, normalizedLabelResolvers)
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
continue;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (filter.type === CRUD_LIST_FILTER_TYPE_ENUM_MANY || filter.type === CRUD_LIST_FILTER_TYPE_RECORD_ID_MANY) {
|
|
350
|
-
for (const value of Array.isArray(rawValue) ? rawValue : []) {
|
|
351
|
-
chips.push({
|
|
352
|
-
id: `${filter.key}:${value}`,
|
|
353
|
-
filterKey: filter.key,
|
|
354
|
-
value,
|
|
355
|
-
label: normalizeText(customChipLabel?.(value, filter, values))
|
|
356
|
-
|| normalizeText(filter.chipLabel?.(value, filter, values))
|
|
357
|
-
|| `${filter.label}: ${resolveAtomicValueLabel(filter, value, normalizedLabelResolvers)}`
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
continue;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
if (filter.type === CRUD_LIST_FILTER_TYPE_DATE_RANGE || filter.type === CRUD_LIST_FILTER_TYPE_NUMBER_RANGE) {
|
|
364
|
-
const hasValue = Boolean(rawValue?.from || rawValue?.to || rawValue?.min || rawValue?.max);
|
|
365
|
-
if (!hasValue) {
|
|
366
|
-
continue;
|
|
367
|
-
}
|
|
173
|
+
const chipValues = listCrudListFilterChipValues(filter, values[filter.key]);
|
|
368
174
|
|
|
175
|
+
for (const chipValue of chipValues) {
|
|
369
176
|
chips.push({
|
|
370
|
-
id: filter.key,
|
|
177
|
+
id: isCrudListFilterMultiValue(filter) ? `${filter.key}:${chipValue}` : filter.key,
|
|
371
178
|
filterKey: filter.key,
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
||
|
|
179
|
+
...(isCrudListFilterMultiValue(filter) ? { value: chipValue } : {}),
|
|
180
|
+
label: normalizeText(customChipLabel?.(chipValue, filter, values))
|
|
181
|
+
|| normalizeText(filter.chipLabel?.(chipValue, filter, values))
|
|
182
|
+
|| formatCrudListFilterDefaultChipLabel(filter, chipValue, {
|
|
183
|
+
resolveAtomicValue(value) {
|
|
184
|
+
return resolveAtomicValueLabel(filter, value, normalizedLabelResolvers);
|
|
185
|
+
}
|
|
186
|
+
})
|
|
375
187
|
});
|
|
376
|
-
continue;
|
|
377
188
|
}
|
|
378
|
-
|
|
379
|
-
if (!normalizeText(rawValue)) {
|
|
380
|
-
continue;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
chips.push({
|
|
384
|
-
id: filter.key,
|
|
385
|
-
filterKey: filter.key,
|
|
386
|
-
label: normalizeText(customChipLabel?.(rawValue, filter, values))
|
|
387
|
-
|| normalizeText(filter.chipLabel?.(rawValue, filter, values))
|
|
388
|
-
|| defaultChipLabel(filter, rawValue, normalizedLabelResolvers)
|
|
389
|
-
});
|
|
390
189
|
}
|
|
391
190
|
|
|
392
191
|
return chips;
|
|
@@ -415,9 +214,13 @@ function useCrudListFilters(definitions = {}, { labelResolvers = {}, chipLabels
|
|
|
415
214
|
return;
|
|
416
215
|
}
|
|
417
216
|
|
|
418
|
-
if (filter
|
|
419
|
-
|
|
420
|
-
|
|
217
|
+
if (isCrudListFilterMultiValue(filter)) {
|
|
218
|
+
assignFilterValue(
|
|
219
|
+
values,
|
|
220
|
+
filter,
|
|
221
|
+
(Array.isArray(values[filter.key]) ? values[filter.key] : [])
|
|
222
|
+
.filter((entry) => entry !== chip.value)
|
|
223
|
+
);
|
|
421
224
|
return;
|
|
422
225
|
}
|
|
423
226
|
|
|
@@ -475,7 +278,7 @@ function useCrudListFilters(definitions = {}, { labelResolvers = {}, chipLabels
|
|
|
475
278
|
}
|
|
476
279
|
|
|
477
280
|
matchedFilter = true;
|
|
478
|
-
if (!
|
|
281
|
+
if (!areCrudListFilterUiValuesEqual(filter, values[filter.key], presetValues[filter.key])) {
|
|
479
282
|
return false;
|
|
480
283
|
}
|
|
481
284
|
}
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import UsersHomeToolsWidget from "../components/UsersHomeToolsWidget.vue";
|
|
2
2
|
import ProfileClientElement from "../components/ProfileClientElement.vue";
|
|
3
|
+
import { registerUsersBootstrapPayloadHandlers } from "../bootstrap/user-bootstrap-handler.js";
|
|
3
4
|
|
|
4
5
|
class UsersWebClientProvider {
|
|
5
6
|
static id = "users.web.client";
|
|
6
7
|
static dependsOn = ["shell.web.client"];
|
|
7
8
|
|
|
8
9
|
register(app) {
|
|
9
|
-
if (!app || typeof app.singleton !== "function") {
|
|
10
|
-
throw new Error("UsersWebClientProvider requires application singleton().");
|
|
10
|
+
if (!app || typeof app.singleton !== "function" || typeof app.tag !== "function") {
|
|
11
|
+
throw new Error("UsersWebClientProvider requires application singleton()/tag().");
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
app.singleton("users.web.home.tools.widget", () => UsersHomeToolsWidget);
|
|
14
15
|
app.singleton("users.web.profile.element", () => ProfileClientElement);
|
|
16
|
+
registerUsersBootstrapPayloadHandlers(app);
|
|
15
17
|
}
|
|
16
18
|
}
|
|
17
19
|
|
package/test/bootstrap.test.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
+
import { resolveBootstrapPayloadHandlers } from "@jskit-ai/shell-web/client/bootstrap";
|
|
3
4
|
import { resolvePlacementUserFromBootstrapPayload } from "../src/client/lib/bootstrap.js";
|
|
5
|
+
import {
|
|
6
|
+
createUsersBootstrapUserHandler,
|
|
7
|
+
registerUsersBootstrapPayloadHandlers
|
|
8
|
+
} from "../src/client/bootstrap/user-bootstrap-handler.js";
|
|
4
9
|
|
|
5
10
|
test("resolvePlacementUserFromBootstrapPayload returns null for anonymous sessions", () => {
|
|
6
11
|
assert.equal(
|
|
@@ -36,3 +41,128 @@ test("resolvePlacementUserFromBootstrapPayload maps profile fields used by place
|
|
|
36
41
|
avatarUrl: "https://cdn.example.com/ada.png"
|
|
37
42
|
});
|
|
38
43
|
});
|
|
44
|
+
|
|
45
|
+
function createPlacementRuntimeDouble() {
|
|
46
|
+
return {
|
|
47
|
+
context: Object.freeze({}),
|
|
48
|
+
getContext() {
|
|
49
|
+
return this.context;
|
|
50
|
+
},
|
|
51
|
+
setContext(patch = {}, { replace = false } = {}) {
|
|
52
|
+
this.context = Object.freeze(
|
|
53
|
+
replace
|
|
54
|
+
? { ...patch }
|
|
55
|
+
: {
|
|
56
|
+
...this.context,
|
|
57
|
+
...patch
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
return this.context;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function createBootstrapRegistryAppDouble() {
|
|
66
|
+
const singletons = new Map();
|
|
67
|
+
const instances = new Map();
|
|
68
|
+
const tags = new Map();
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
singleton(token, factory) {
|
|
72
|
+
singletons.set(token, factory);
|
|
73
|
+
},
|
|
74
|
+
tag(token, tagName) {
|
|
75
|
+
const current = tags.get(tagName) || [];
|
|
76
|
+
current.push(token);
|
|
77
|
+
tags.set(tagName, current);
|
|
78
|
+
},
|
|
79
|
+
resolveTag(tagName) {
|
|
80
|
+
return (tags.get(tagName) || []).map((token) => this.make(token));
|
|
81
|
+
},
|
|
82
|
+
make(token) {
|
|
83
|
+
if (instances.has(token)) {
|
|
84
|
+
return instances.get(token);
|
|
85
|
+
}
|
|
86
|
+
const factory = singletons.get(token);
|
|
87
|
+
if (typeof factory !== "function") {
|
|
88
|
+
throw new Error(`Unknown token ${String(token)}`);
|
|
89
|
+
}
|
|
90
|
+
const instance = factory(this);
|
|
91
|
+
instances.set(token, instance);
|
|
92
|
+
return instance;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
test("users web bootstrap user handler writes placement user", async () => {
|
|
98
|
+
const handler = createUsersBootstrapUserHandler();
|
|
99
|
+
const placementRuntime = createPlacementRuntimeDouble();
|
|
100
|
+
await handler.applyBootstrapPayload({
|
|
101
|
+
payload: {
|
|
102
|
+
session: {
|
|
103
|
+
authenticated: true,
|
|
104
|
+
userId: "42"
|
|
105
|
+
},
|
|
106
|
+
profile: {
|
|
107
|
+
displayName: "Ada Lovelace",
|
|
108
|
+
email: "ADA@EXAMPLE.COM",
|
|
109
|
+
avatar: {
|
|
110
|
+
effectiveUrl: "https://cdn.example.com/ada.png"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
placementRuntime,
|
|
115
|
+
source: "test"
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
assert.deepEqual(placementRuntime.getContext().user, {
|
|
119
|
+
id: "42",
|
|
120
|
+
displayName: "Ada Lovelace",
|
|
121
|
+
name: "Ada Lovelace",
|
|
122
|
+
email: "ada@example.com",
|
|
123
|
+
avatarUrl: "https://cdn.example.com/ada.png"
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("users web bootstrap user handler clears placement user on bootstrap 401 only", async () => {
|
|
128
|
+
const handler = createUsersBootstrapUserHandler();
|
|
129
|
+
const placementRuntime = createPlacementRuntimeDouble();
|
|
130
|
+
placementRuntime.setContext({
|
|
131
|
+
user: {
|
|
132
|
+
id: "42",
|
|
133
|
+
displayName: "Ada Lovelace"
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await handler.handleBootstrapError({
|
|
138
|
+
error: {
|
|
139
|
+
statusCode: 404
|
|
140
|
+
},
|
|
141
|
+
placementRuntime,
|
|
142
|
+
source: "test"
|
|
143
|
+
});
|
|
144
|
+
assert.deepEqual(placementRuntime.getContext().user, {
|
|
145
|
+
id: "42",
|
|
146
|
+
displayName: "Ada Lovelace"
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await handler.handleBootstrapError({
|
|
150
|
+
error: {
|
|
151
|
+
statusCode: 401
|
|
152
|
+
},
|
|
153
|
+
placementRuntime,
|
|
154
|
+
source: "test"
|
|
155
|
+
});
|
|
156
|
+
assert.equal(placementRuntime.getContext().user, null);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("registerUsersBootstrapPayloadHandlers registers the users bootstrap user handler", () => {
|
|
160
|
+
const app = createBootstrapRegistryAppDouble();
|
|
161
|
+
registerUsersBootstrapPayloadHandlers(app);
|
|
162
|
+
|
|
163
|
+
const handlers = resolveBootstrapPayloadHandlers(app);
|
|
164
|
+
assert.equal(handlers.length, 1);
|
|
165
|
+
assert.equal(handlers[0]?.handlerId, "users.web.bootstrap.user");
|
|
166
|
+
assert.equal(typeof handlers[0]?.applyBootstrapPayload, "function");
|
|
167
|
+
assert.equal(typeof handlers[0]?.handleBootstrapError, "function");
|
|
168
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createSchema } from "json-rest-schema";
|
|
4
|
+
import { validateOperationInput } from "../src/client/composables/runtime/operationValidationHelpers.js";
|
|
5
|
+
|
|
6
|
+
test("validateOperationInput validates a schema definition and returns normalized input", () => {
|
|
7
|
+
const rawPayload = {
|
|
8
|
+
name: " Acme "
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const result = validateOperationInput({
|
|
12
|
+
input: {
|
|
13
|
+
schema: createSchema({
|
|
14
|
+
name: { type: "string", required: true, minLength: 1 }
|
|
15
|
+
}),
|
|
16
|
+
mode: "patch"
|
|
17
|
+
},
|
|
18
|
+
rawPayload
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
assert.equal(result.ok, true);
|
|
22
|
+
assert.deepEqual(result.parsedInput, {
|
|
23
|
+
name: "Acme"
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("validateOperationInput returns validation failures for invalid schema input", () => {
|
|
28
|
+
const result = validateOperationInput({
|
|
29
|
+
input: {
|
|
30
|
+
schema: createSchema({
|
|
31
|
+
name: {
|
|
32
|
+
type: "string",
|
|
33
|
+
required: true,
|
|
34
|
+
minLength: 1,
|
|
35
|
+
messages: {
|
|
36
|
+
minLength: "Name is required."
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}),
|
|
40
|
+
mode: "patch"
|
|
41
|
+
},
|
|
42
|
+
rawPayload: {
|
|
43
|
+
name: ""
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
assert.equal(result.ok, false);
|
|
48
|
+
assert.equal(result.failure.code, "validation_failed");
|
|
49
|
+
assert.equal(result.failure.fieldErrors.name, "Name is required.");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("validateOperationInput rethrows malformed input contracts", () => {
|
|
53
|
+
assert.throws(
|
|
54
|
+
() =>
|
|
55
|
+
validateOperationInput({
|
|
56
|
+
input: {
|
|
57
|
+
schema: null,
|
|
58
|
+
mode: "patch"
|
|
59
|
+
},
|
|
60
|
+
rawPayload: {}
|
|
61
|
+
}),
|
|
62
|
+
/must be a json-rest-schema schema instance/
|
|
63
|
+
);
|
|
64
|
+
});
|