@masterteam/client-components 0.0.89 → 0.0.90
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.
|
@@ -8,7 +8,7 @@ import { Button } from '@masterteam/components/button';
|
|
|
8
8
|
import { RuntimeActionRunner, resolveActionLabel } from '@masterteam/components/runtime-action';
|
|
9
9
|
import { HttpClient } from '@angular/common/http';
|
|
10
10
|
import { switchMap, map, of, Observable, share, take } from 'rxjs';
|
|
11
|
-
import { resolveLocalizedLabel, serializeEntityRawValue, isSectionKey, EntitiesPreview } from '@masterteam/components/entities';
|
|
11
|
+
import { resolveLocalizedLabel, serializeEntityRawValue, isSectionKey, buildDisplayEntities, EntitiesPreview } from '@masterteam/components/entities';
|
|
12
12
|
import { Table, TableExportService, TableGroupingController, TableValueResolver, TableCaption } from '@masterteam/components/table';
|
|
13
13
|
import * as i2 from 'primeng/skeleton';
|
|
14
14
|
import { SkeletonModule } from 'primeng/skeleton';
|
|
@@ -24,6 +24,13 @@ import { PopoverModule } from 'primeng/popover';
|
|
|
24
24
|
import { DashboardViewer } from '@masterteam/dashboard-builder';
|
|
25
25
|
|
|
26
26
|
const CLIENT_LIST_RECORD_STATE_KEY = '__clientListRecordState';
|
|
27
|
+
/**
|
|
28
|
+
* Row key carrying the owning record of a hierarchy row. Sits beside the row's
|
|
29
|
+
* cells rather than in a column so it travels with the row through
|
|
30
|
+
* `transformResult`, row actions and click handlers without ever rendering.
|
|
31
|
+
* Read it with {@link readClientListRowSource}.
|
|
32
|
+
*/
|
|
33
|
+
const CLIENT_LIST_RECORD_SOURCE_KEY = '__clientListRecordSource';
|
|
27
34
|
/** Record state every list request asks for. */
|
|
28
35
|
const CLIENT_LIST_DEFAULT_INCLUDE_STATE = ['escalation'];
|
|
29
36
|
/**
|
|
@@ -43,7 +50,7 @@ class ClientListApiService {
|
|
|
43
50
|
* the request carried, because a write that wants its record projected the
|
|
44
51
|
* same way has to repeat them and cannot recover them from the response.
|
|
45
52
|
*/
|
|
46
|
-
getRows(contextKey, query, filters = []) {
|
|
53
|
+
getRows(contextKey, query, filters = [], hierarchy) {
|
|
47
54
|
return this.resolveTablePropertyKeys(contextKey, query).pipe(switchMap((resolvedKeys) => {
|
|
48
55
|
const propertyKeys = resolvedKeys?.length ? resolvedKeys : undefined;
|
|
49
56
|
return this.queryRuntime({
|
|
@@ -55,6 +62,7 @@ class ClientListApiService {
|
|
|
55
62
|
filters,
|
|
56
63
|
page: this.toPage(query.skip, query.take),
|
|
57
64
|
pageSize: query.take,
|
|
65
|
+
...(hierarchy ? { hierarchy: this.toHierarchyScope(hierarchy) } : {}),
|
|
58
66
|
}).pipe(map((response) => ({ response, propertyKeys })));
|
|
59
67
|
}));
|
|
60
68
|
}
|
|
@@ -63,6 +71,12 @@ class ClientListApiService {
|
|
|
63
71
|
* taken when a write reports `recordProjectionStatus: 'Unavailable'`. The
|
|
64
72
|
* write itself already succeeded; this only reads, and must never be served
|
|
65
73
|
* by re-submitting.
|
|
74
|
+
*
|
|
75
|
+
* `contextKey` is the context that *owns* the record, which in a hierarchy
|
|
76
|
+
* table is the row's `source.contextKey` rather than the list's own. The
|
|
77
|
+
* read deliberately carries no `hierarchy` scope: the row is filtered by its
|
|
78
|
+
* own id in its own context, which is both cheaper and immune to the record
|
|
79
|
+
* having moved out of the root's subtree since it was written.
|
|
66
80
|
*/
|
|
67
81
|
getRecord(contextKey, request, filters) {
|
|
68
82
|
return this.queryRuntime({
|
|
@@ -123,6 +137,21 @@ class ClientListApiService {
|
|
|
123
137
|
}
|
|
124
138
|
return Math.floor(skip / take) + 1;
|
|
125
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Sends the scope with both optional fields resolved rather than omitted.
|
|
142
|
+
* The backend's defaults match these, but `depth` is range-checked against
|
|
143
|
+
* `>= -1` and anything else fails the whole request — so a host that means
|
|
144
|
+
* "everything" gets `-1` written down instead of relying on an absent field
|
|
145
|
+
* surviving serialization.
|
|
146
|
+
*/
|
|
147
|
+
toHierarchyScope(scope) {
|
|
148
|
+
const depth = Math.trunc(scope.depth ?? -1);
|
|
149
|
+
return {
|
|
150
|
+
rootId: scope.rootId,
|
|
151
|
+
depth: depth < -1 ? -1 : depth,
|
|
152
|
+
includeRoot: scope.includeRoot ?? false,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
126
155
|
toEffectiveColumnKeys(columns) {
|
|
127
156
|
const keys = (columns ?? [])
|
|
128
157
|
.filter((column) => typeof column === 'string' ? true : column.visible !== false)
|
|
@@ -251,6 +280,10 @@ class ClientListStateService {
|
|
|
251
280
|
const row = {
|
|
252
281
|
Id: record.id,
|
|
253
282
|
[CLIENT_LIST_RECORD_STATE_KEY]: record.state ?? null,
|
|
283
|
+
// Only present on a hierarchy response, where rows come from several
|
|
284
|
+
// owners at once and the list's own context no longer identifies the
|
|
285
|
+
// record a row belongs to.
|
|
286
|
+
[CLIENT_LIST_RECORD_SOURCE_KEY]: record.source ?? null,
|
|
254
287
|
};
|
|
255
288
|
columns.forEach((column) => {
|
|
256
289
|
const property = this.resolvePropertyMeta(column.key, propertyMaps);
|
|
@@ -277,6 +310,12 @@ class ClientListStateService {
|
|
|
277
310
|
columns: shaped.columns,
|
|
278
311
|
rows: shaped.rows,
|
|
279
312
|
cards: [],
|
|
313
|
+
// Pre-pagination aggregates, kept verbatim off the response. They
|
|
314
|
+
// describe every matching row, so they must never be recomputed from
|
|
315
|
+
// the page — and they are absent on an ordinary list, which is a
|
|
316
|
+
// different thing from being zero.
|
|
317
|
+
totals: response.projectionMeta?.totals,
|
|
318
|
+
sourceGroups: response.projectionMeta?.sourceGroups,
|
|
280
319
|
skip,
|
|
281
320
|
take,
|
|
282
321
|
totalCount,
|
|
@@ -370,14 +409,22 @@ class ClientListStateService {
|
|
|
370
409
|
const raw = target.rawData;
|
|
371
410
|
if (!raw)
|
|
372
411
|
return false;
|
|
412
|
+
// A hierarchy table cannot be patched in place. Its totals and per-owner
|
|
413
|
+
// subtotals are summed by the backend over every matching row across all
|
|
414
|
+
// pages, so a written row changes numbers this client cannot recompute —
|
|
415
|
+
// splicing the row would leave the table showing a total that no longer
|
|
416
|
+
// matches its own rows. Refuse, and let the caller reload.
|
|
417
|
+
if (config.hierarchy)
|
|
418
|
+
return false;
|
|
373
419
|
return target.areaType === 'cards'
|
|
374
420
|
? this.upsertCardRecord(key, target, raw, record)
|
|
375
421
|
: this.upsertRowRecord(key, target, raw, record, config);
|
|
376
422
|
}
|
|
377
423
|
/**
|
|
378
424
|
* Drops a record the backend has deleted, without re-fetching. Returns
|
|
379
|
-
* `false` when the record is not in the loaded set
|
|
380
|
-
*
|
|
425
|
+
* `false` when the record is not in the loaded set — or when the list is a
|
|
426
|
+
* hierarchy table, whose server-summed totals a local removal would
|
|
427
|
+
* invalidate — so the caller can decide whether a reload is warranted.
|
|
381
428
|
*/
|
|
382
429
|
removeRecord(key, recordId, config) {
|
|
383
430
|
const target = this.itemsByKey()[key];
|
|
@@ -386,6 +433,8 @@ class ClientListStateService {
|
|
|
386
433
|
const raw = target.rawData;
|
|
387
434
|
if (!raw)
|
|
388
435
|
return false;
|
|
436
|
+
if (config.hierarchy)
|
|
437
|
+
return false;
|
|
389
438
|
const records = (raw.records ?? []).filter((item) => item.id !== recordId);
|
|
390
439
|
if (target.areaType === 'cards') {
|
|
391
440
|
const cards = target.cards.filter((card) => card.id !== recordId);
|
|
@@ -1495,17 +1544,25 @@ class ClientListCardsView {
|
|
|
1495
1544
|
badgeLegend = computed(() => {
|
|
1496
1545
|
const entries = new Map();
|
|
1497
1546
|
for (const card of this.visibleCards()) {
|
|
1498
|
-
|
|
1547
|
+
// A card's entities are unexpanded: a LeafDetails one still carries the
|
|
1548
|
+
// fetch payload (`childSchemas`), not the per-level value the badges are
|
|
1549
|
+
// painted from. Expanding it here — the same split `mt-entities-preview`
|
|
1550
|
+
// does to render the card — is what makes the legend list exactly the
|
|
1551
|
+
// badges on screen, statuses and phases alike.
|
|
1552
|
+
for (const entity of buildDisplayEntities(card.entities ?? [])) {
|
|
1499
1553
|
if (entity.viewType !== 'LeafDetails')
|
|
1500
1554
|
continue;
|
|
1501
1555
|
const value = entity.value;
|
|
1502
|
-
|
|
1503
|
-
|
|
1556
|
+
const badges = value?.kind === 'phase'
|
|
1557
|
+
? (value.phases ?? [])
|
|
1558
|
+
: (value?.statuses ?? []);
|
|
1559
|
+
for (const badge of badges) {
|
|
1560
|
+
if (!badge?.key || entries.has(badge.key))
|
|
1504
1561
|
continue;
|
|
1505
|
-
const color =
|
|
1506
|
-
entries.set(
|
|
1507
|
-
key:
|
|
1508
|
-
label:
|
|
1562
|
+
const color = badge.color ?? DEFAULT_BADGE_COLOR;
|
|
1563
|
+
entries.set(badge.key, {
|
|
1564
|
+
key: badge.key,
|
|
1565
|
+
label: badge.display || badge.key,
|
|
1509
1566
|
color,
|
|
1510
1567
|
textColor: readableTextOn(color),
|
|
1511
1568
|
});
|
|
@@ -1978,6 +2035,54 @@ function serializeFilters(filters) {
|
|
|
1978
2035
|
return JSON.stringify(filters ?? []);
|
|
1979
2036
|
}
|
|
1980
2037
|
|
|
2038
|
+
/**
|
|
2039
|
+
* Reads the owning record off a hierarchy row. Returns null for a row from an
|
|
2040
|
+
* ordinary list, which has no owner other than the list's own context.
|
|
2041
|
+
*
|
|
2042
|
+
* Use this before addressing anything at a row — an edit, a navigation, a
|
|
2043
|
+
* permission check. The list's configured `contextKey` is the aggregation
|
|
2044
|
+
* scope in a hierarchy table, not the row's home.
|
|
2045
|
+
*/
|
|
2046
|
+
function readClientListRowSource(row) {
|
|
2047
|
+
const source = row?.[CLIENT_LIST_RECORD_SOURCE_KEY];
|
|
2048
|
+
return isRecordSource(source) ? source : null;
|
|
2049
|
+
}
|
|
2050
|
+
/**
|
|
2051
|
+
* Looks a total up by property key.
|
|
2052
|
+
*
|
|
2053
|
+
* Always go through this rather than indexing `totals`. The backend fills the
|
|
2054
|
+
* array from the module's configured summary keys — one `amount` entry on the
|
|
2055
|
+
* financial module, nothing at all on every other module — so the array being
|
|
2056
|
+
* present does not mean the key you want is in it, and position means nothing.
|
|
2057
|
+
*/
|
|
2058
|
+
function findClientListTotal(totals, propertyKey) {
|
|
2059
|
+
const wanted = propertyKey.trim().toLowerCase();
|
|
2060
|
+
return ((totals ?? []).find((total) => (total.propertyKey ?? '').trim().toLowerCase() === wanted) ?? null);
|
|
2061
|
+
}
|
|
2062
|
+
/**
|
|
2063
|
+
* Whether a total left rows out — some row's value was missing or not a
|
|
2064
|
+
* number, so the sum is over fewer rows than the table reports.
|
|
2065
|
+
*
|
|
2066
|
+
* A total in this state must be presented as incomplete. Rendering it as a
|
|
2067
|
+
* plain figure states a portfolio total the data does not support.
|
|
2068
|
+
*/
|
|
2069
|
+
function isClientListTotalIncomplete(total) {
|
|
2070
|
+
return (total?.missingValueCount ?? 0) > 0;
|
|
2071
|
+
}
|
|
2072
|
+
/**
|
|
2073
|
+
* The subtotal group for one owning record, by its level-data id.
|
|
2074
|
+
*/
|
|
2075
|
+
function findClientListSourceGroup(groups, levelDataId) {
|
|
2076
|
+
return ((groups ?? []).find((group) => group.source?.levelDataId === levelDataId) ??
|
|
2077
|
+
null);
|
|
2078
|
+
}
|
|
2079
|
+
function isRecordSource(value) {
|
|
2080
|
+
return (!!value &&
|
|
2081
|
+
typeof value === 'object' &&
|
|
2082
|
+
typeof value.levelDataId === 'number' &&
|
|
2083
|
+
typeof value.contextKey === 'string');
|
|
2084
|
+
}
|
|
2085
|
+
|
|
1981
2086
|
const DEFAULT_AREA_TYPE = 'table';
|
|
1982
2087
|
const DEFAULT_MODE = 'auto';
|
|
1983
2088
|
const DEFAULT_TYPE = 'form';
|
|
@@ -2138,6 +2243,15 @@ class ClientList {
|
|
|
2138
2243
|
return;
|
|
2139
2244
|
if (this.isPendingApprovalWrite(result))
|
|
2140
2245
|
return;
|
|
2246
|
+
// A hierarchy table has no in-place path: its totals and per-owner
|
|
2247
|
+
// subtotals are summed server-side over every matching row, and the
|
|
2248
|
+
// written record lives in a descendant context this list's own id filter
|
|
2249
|
+
// would not even match. Reload outright rather than spend a read on a
|
|
2250
|
+
// record that cannot be spliced anyway.
|
|
2251
|
+
if (item.config.hierarchy) {
|
|
2252
|
+
this.reloadByKey(itemKey);
|
|
2253
|
+
return;
|
|
2254
|
+
}
|
|
2141
2255
|
const recordId = this.toFiniteNumber(result?.recordId);
|
|
2142
2256
|
const record = this.toWriteRecord(result, recordId);
|
|
2143
2257
|
if (record) {
|
|
@@ -2510,10 +2624,17 @@ class ClientList {
|
|
|
2510
2624
|
const recordId = this.resolveRecordId(config, row);
|
|
2511
2625
|
if (recordId == null)
|
|
2512
2626
|
return null;
|
|
2627
|
+
// A hierarchy row belongs to a descendant, not to the list's own context.
|
|
2628
|
+
// Its actions, its permissions and the instance they apply to are all the
|
|
2629
|
+
// owner's — asking the aggregation context for them would read the wrong
|
|
2630
|
+
// record's actions, or none at all.
|
|
2631
|
+
const source = readClientListRowSource(row);
|
|
2513
2632
|
return {
|
|
2514
2633
|
listKey: item.key,
|
|
2515
|
-
contextKey: item.config.contextKey ?? null,
|
|
2516
|
-
|
|
2634
|
+
contextKey: source?.contextKey ?? item.config.contextKey ?? null,
|
|
2635
|
+
listContextKey: item.config.contextKey ?? null,
|
|
2636
|
+
source,
|
|
2637
|
+
instanceId: source?.levelDataId ?? item.config.instanceId ?? null,
|
|
2517
2638
|
moduleId: item.config.moduleId ?? 0,
|
|
2518
2639
|
row,
|
|
2519
2640
|
recordId,
|
|
@@ -2689,7 +2810,9 @@ class ClientList {
|
|
|
2689
2810
|
this.inFlightRequestSignatures.set(key, requestSignature);
|
|
2690
2811
|
this.state.setLoading(key, true);
|
|
2691
2812
|
this.state.setError(key, null);
|
|
2692
|
-
const sub = this.api
|
|
2813
|
+
const sub = this.api
|
|
2814
|
+
.getRows(config.contextKey, query, filters, config.hierarchy)
|
|
2815
|
+
.subscribe({
|
|
2693
2816
|
next: ({ response, propertyKeys }) => {
|
|
2694
2817
|
let hasLoadedData = false;
|
|
2695
2818
|
if (response.data) {
|
|
@@ -2840,6 +2963,20 @@ class ClientList {
|
|
|
2840
2963
|
return DEFAULT_MODE;
|
|
2841
2964
|
return (columnKeys ?? []).length > 0 ? 'override' : DEFAULT_MODE;
|
|
2842
2965
|
}
|
|
2966
|
+
/**
|
|
2967
|
+
* Descendant scope only survives where the backend accepts it: a table, with
|
|
2968
|
+
* a usable root id. Anywhere else it is dropped rather than sent, because
|
|
2969
|
+
* the backend answers an unsupported combination with a 400 that takes the
|
|
2970
|
+
* whole list down — a cards area that happens to share a config object would
|
|
2971
|
+
* otherwise render nothing at all.
|
|
2972
|
+
*/
|
|
2973
|
+
resolveHierarchyScope(config, areaType) {
|
|
2974
|
+
const scope = config?.hierarchy;
|
|
2975
|
+
if (!scope || areaType !== 'table')
|
|
2976
|
+
return null;
|
|
2977
|
+
const rootId = this.resolvePositiveInteger(scope.rootId);
|
|
2978
|
+
return rootId == null ? null : { ...scope, rootId };
|
|
2979
|
+
}
|
|
2843
2980
|
toNormalizedConfig(config, type, areaType, mode, isPaginated, take, showHeader, collapseEnabled, layout) {
|
|
2844
2981
|
const informativeConfig = this.asInformativeConfig(config);
|
|
2845
2982
|
const formConfig = this.asFormConfig(config);
|
|
@@ -2847,6 +2984,7 @@ class ClientList {
|
|
|
2847
2984
|
contextKey: formConfig?.contextKey ?? null,
|
|
2848
2985
|
instanceId: formConfig?.instanceId ?? informativeConfig?.instanceId ?? null,
|
|
2849
2986
|
filters: formConfig?.filters ?? informativeConfig?.filters ?? [],
|
|
2987
|
+
hierarchy: this.resolveHierarchyScope(formConfig, areaType),
|
|
2850
2988
|
levelId: informativeConfig?.levelId ?? null,
|
|
2851
2989
|
moduleId: informativeConfig?.moduleId ?? 0,
|
|
2852
2990
|
type,
|
|
@@ -3023,6 +3161,10 @@ class ClientList {
|
|
|
3023
3161
|
config.areaType,
|
|
3024
3162
|
config.contextKey ?? 'x',
|
|
3025
3163
|
this.serializeFilters(filters),
|
|
3164
|
+
// Two lists on the same context and filters are still different fetches
|
|
3165
|
+
// when they are rooted at different records — without this the second
|
|
3166
|
+
// one would be served the first one's rows from the in-flight cache.
|
|
3167
|
+
this.serializeHierarchy(config.hierarchy),
|
|
3026
3168
|
query.mode,
|
|
3027
3169
|
query.skip,
|
|
3028
3170
|
query.take,
|
|
@@ -3077,6 +3219,15 @@ class ClientList {
|
|
|
3077
3219
|
serializeFilters(filters) {
|
|
3078
3220
|
return JSON.stringify(filters ?? []);
|
|
3079
3221
|
}
|
|
3222
|
+
serializeHierarchy(hierarchy) {
|
|
3223
|
+
if (!hierarchy)
|
|
3224
|
+
return 'x';
|
|
3225
|
+
return [
|
|
3226
|
+
hierarchy.rootId,
|
|
3227
|
+
hierarchy.depth ?? -1,
|
|
3228
|
+
hierarchy.includeRoot ?? false,
|
|
3229
|
+
].join(',');
|
|
3230
|
+
}
|
|
3080
3231
|
resolveFetchFilters(key, config) {
|
|
3081
3232
|
return mergeRuntimeFilters(config.filters, this.runtimeFilters.get(key), this.state.itemsByKey()[key]?.columns);
|
|
3082
3233
|
}
|
|
@@ -3106,5 +3257,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
3106
3257
|
* Generated bundle index. Do not edit.
|
|
3107
3258
|
*/
|
|
3108
3259
|
|
|
3109
|
-
export { CLIENT_LIST_DEFAULT_INCLUDE_STATE, CLIENT_LIST_RECORD_ID_FILTER_KEY, CLIENT_LIST_RECORD_STATE_KEY, ClientList, ClientListApiService, ClientListRuntimeActionsService, ClientListStateService, ClientListToolbarService, defaultResolveRecordId };
|
|
3260
|
+
export { CLIENT_LIST_DEFAULT_INCLUDE_STATE, CLIENT_LIST_RECORD_ID_FILTER_KEY, CLIENT_LIST_RECORD_SOURCE_KEY, CLIENT_LIST_RECORD_STATE_KEY, ClientList, ClientListApiService, ClientListRuntimeActionsService, ClientListStateService, ClientListToolbarService, defaultResolveRecordId, findClientListSourceGroup, findClientListTotal, isClientListTotalIncomplete, readClientListRowSource };
|
|
3110
3261
|
//# sourceMappingURL=masterteam-client-components-client-list.mjs.map
|