@masterteam/client-components 0.0.85 → 0.0.87
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.
|
@@ -7,7 +7,7 @@ import { TranslocoService, TranslocoModule, TranslocoPipe } from '@jsverse/trans
|
|
|
7
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
|
-
import { switchMap,
|
|
10
|
+
import { switchMap, map, of, Observable, share, take } from 'rxjs';
|
|
11
11
|
import { resolveLocalizedLabel, serializeEntityRawValue, isSectionKey, 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';
|
|
@@ -23,22 +23,65 @@ import * as i3 from 'primeng/popover';
|
|
|
23
23
|
import { PopoverModule } from 'primeng/popover';
|
|
24
24
|
import { DashboardViewer } from '@masterteam/dashboard-builder';
|
|
25
25
|
|
|
26
|
-
const
|
|
26
|
+
const CLIENT_LIST_RECORD_STATE_KEY = '__clientListRecordState';
|
|
27
|
+
/** Record state every list request asks for. */
|
|
28
|
+
const CLIENT_LIST_DEFAULT_INCLUDE_STATE = ['escalation'];
|
|
29
|
+
/**
|
|
30
|
+
* `fetch/query` filter key that selects a single record by its own id. Reading
|
|
31
|
+
* one record back after a write filters on this, never on the list's scope
|
|
32
|
+
* filters alone — those match a whole page, not the row that just changed.
|
|
33
|
+
*/
|
|
34
|
+
const CLIENT_LIST_RECORD_ID_FILTER_KEY = 'id';
|
|
35
|
+
|
|
36
|
+
const DEFAULT_INCLUDE_STATE = CLIENT_LIST_DEFAULT_INCLUDE_STATE;
|
|
27
37
|
class ClientListApiService {
|
|
28
38
|
http = inject(HttpClient);
|
|
29
39
|
runtimeFetchBaseUrl = 'fetch/query';
|
|
30
40
|
informativeBaseUrl = 'LevelData';
|
|
41
|
+
/**
|
|
42
|
+
* Fetches a table page. Resolves to the response **and** the `propertyKeys`
|
|
43
|
+
* the request carried, because a write that wants its record projected the
|
|
44
|
+
* same way has to repeat them and cannot recover them from the response.
|
|
45
|
+
*/
|
|
31
46
|
getRows(contextKey, query, filters = []) {
|
|
32
|
-
return this.resolveTablePropertyKeys(contextKey, query).pipe(switchMap((
|
|
47
|
+
return this.resolveTablePropertyKeys(contextKey, query).pipe(switchMap((resolvedKeys) => {
|
|
48
|
+
const propertyKeys = resolvedKeys?.length ? resolvedKeys : undefined;
|
|
49
|
+
return this.queryRuntime({
|
|
50
|
+
contextKey,
|
|
51
|
+
projection: 'Table',
|
|
52
|
+
surfaceKey: 'table',
|
|
53
|
+
includeState: [...DEFAULT_INCLUDE_STATE],
|
|
54
|
+
propertyKeys,
|
|
55
|
+
filters,
|
|
56
|
+
page: this.toPage(query.skip, query.take),
|
|
57
|
+
pageSize: query.take,
|
|
58
|
+
}).pipe(map((response) => ({ response, propertyKeys })));
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Reads a single record back with the list's own query settings — the path
|
|
63
|
+
* taken when a write reports `recordProjectionStatus: 'Unavailable'`. The
|
|
64
|
+
* write itself already succeeded; this only reads, and must never be served
|
|
65
|
+
* by re-submitting.
|
|
66
|
+
*/
|
|
67
|
+
getRecord(contextKey, request, filters) {
|
|
68
|
+
return this.queryRuntime({
|
|
33
69
|
contextKey,
|
|
34
|
-
projection:
|
|
35
|
-
surfaceKey:
|
|
36
|
-
includeState:
|
|
37
|
-
|
|
70
|
+
projection: request.projection,
|
|
71
|
+
surfaceKey: request.surfaceKey,
|
|
72
|
+
includeState: request.includeState
|
|
73
|
+
? [...request.includeState]
|
|
74
|
+
: [...DEFAULT_INCLUDE_STATE],
|
|
75
|
+
propertyKeys: request.propertyKeys?.length
|
|
76
|
+
? request.propertyKeys
|
|
77
|
+
: undefined,
|
|
38
78
|
filters,
|
|
39
|
-
page:
|
|
40
|
-
pageSize:
|
|
41
|
-
|
|
79
|
+
page: 1,
|
|
80
|
+
pageSize: 1,
|
|
81
|
+
...(request.projection === 'Card'
|
|
82
|
+
? { display: { areas: ['card'] } }
|
|
83
|
+
: {}),
|
|
84
|
+
});
|
|
42
85
|
}
|
|
43
86
|
getCards(contextKey, filters = [], skip, take) {
|
|
44
87
|
// `page` and `pageSize` must be sent together or not at all (fetch/query
|
|
@@ -108,8 +151,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
108
151
|
args: [{ providedIn: 'root' }]
|
|
109
152
|
}] });
|
|
110
153
|
|
|
111
|
-
const CLIENT_LIST_RECORD_STATE_KEY = '__clientListRecordState';
|
|
112
|
-
|
|
113
154
|
const UNSORTABLE_ENTITY_VIEW_TYPES = new Set([
|
|
114
155
|
'Attachment',
|
|
115
156
|
'Checkbox',
|
|
@@ -306,6 +347,146 @@ class ClientListStateService {
|
|
|
306
347
|
},
|
|
307
348
|
});
|
|
308
349
|
}
|
|
350
|
+
/**
|
|
351
|
+
* Splices a canonical record into the data the list already holds, so a
|
|
352
|
+
* create or update lands without re-fetching the page.
|
|
353
|
+
*
|
|
354
|
+
* Tables replay the whole page through {@link setRowsResult}: for a table
|
|
355
|
+
* the rendered page *is* the raw response, so replaying it keeps the catalog
|
|
356
|
+
* lookup, the column build and the host `transformResult` hook on exactly
|
|
357
|
+
* one code path — a second mapper for single rows would drift from the one
|
|
358
|
+
* that built the rest of the table. Cards cannot replay: with infinite
|
|
359
|
+
* scroll `rawData` holds only the last page while `cards` holds every page,
|
|
360
|
+
* so a single card is built and merged into the accumulated set instead.
|
|
361
|
+
*
|
|
362
|
+
* Returns `false` when there is nothing to splice into — an informative
|
|
363
|
+
* dashboard, or a list that never completed a load. The caller then falls
|
|
364
|
+
* back to a real fetch rather than inventing state.
|
|
365
|
+
*/
|
|
366
|
+
upsertRecord(key, record, config) {
|
|
367
|
+
const target = this.itemsByKey()[key];
|
|
368
|
+
if (!target || target.type !== 'form')
|
|
369
|
+
return false;
|
|
370
|
+
const raw = target.rawData;
|
|
371
|
+
if (!raw)
|
|
372
|
+
return false;
|
|
373
|
+
return target.areaType === 'cards'
|
|
374
|
+
? this.upsertCardRecord(key, target, raw, record)
|
|
375
|
+
: this.upsertRowRecord(key, target, raw, record, config);
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Drops a record the backend has deleted, without re-fetching. Returns
|
|
379
|
+
* `false` when the record is not in the loaded set, so the caller can decide
|
|
380
|
+
* whether a reload is warranted.
|
|
381
|
+
*/
|
|
382
|
+
removeRecord(key, recordId, config) {
|
|
383
|
+
const target = this.itemsByKey()[key];
|
|
384
|
+
if (!target || target.type !== 'form')
|
|
385
|
+
return false;
|
|
386
|
+
const raw = target.rawData;
|
|
387
|
+
if (!raw)
|
|
388
|
+
return false;
|
|
389
|
+
const records = (raw.records ?? []).filter((item) => item.id !== recordId);
|
|
390
|
+
if (target.areaType === 'cards') {
|
|
391
|
+
const cards = target.cards.filter((card) => card.id !== recordId);
|
|
392
|
+
if (cards.length === target.cards.length)
|
|
393
|
+
return false;
|
|
394
|
+
this.patchItem(key, {
|
|
395
|
+
...target,
|
|
396
|
+
cards,
|
|
397
|
+
totalCount: Math.max(0, target.totalCount - 1),
|
|
398
|
+
error: null,
|
|
399
|
+
rawData: this.withRecords(raw, records, -1),
|
|
400
|
+
});
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
if (records.length === (raw.records ?? []).length)
|
|
404
|
+
return false;
|
|
405
|
+
this.setRowsResult(key, this.withRecords(raw, records, -1), config, target.skip, target.take);
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
upsertRowRecord(key, target, raw, record, config) {
|
|
409
|
+
// No columns means no completed table load to splice into.
|
|
410
|
+
if (!target.columns.length)
|
|
411
|
+
return false;
|
|
412
|
+
const records = [...(raw.records ?? [])];
|
|
413
|
+
const index = records.findIndex((item) => item.id === record.id);
|
|
414
|
+
if (index >= 0) {
|
|
415
|
+
records[index] = this.mergeRecord(records[index], record);
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
// The backend owns the real ordering and does not say where the new
|
|
419
|
+
// record landed, so it goes to the top of the page in view: the user
|
|
420
|
+
// has to see what they just created. The next real load puts it in its
|
|
421
|
+
// true place.
|
|
422
|
+
records.unshift(record);
|
|
423
|
+
// Keep the page the size the paginator promises — the row pushed off
|
|
424
|
+
// the end now lives on the next page.
|
|
425
|
+
if (config.isPaginated &&
|
|
426
|
+
target.take > 0 &&
|
|
427
|
+
records.length > target.take) {
|
|
428
|
+
records.length = target.take;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
this.setRowsResult(key, this.withRecords(raw, records, index >= 0 ? 0 : 1), config, target.skip, target.take);
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
upsertCardRecord(key, target, raw, record) {
|
|
435
|
+
if (!raw.projectionMeta)
|
|
436
|
+
return false;
|
|
437
|
+
const previous = (raw.records ?? []).find((item) => item.id === record.id);
|
|
438
|
+
const merged = previous ? this.mergeRecord(previous, record) : record;
|
|
439
|
+
const [card] = this.toCards({ ...raw, records: [merged] });
|
|
440
|
+
if (!card)
|
|
441
|
+
return false;
|
|
442
|
+
const index = target.cards.findIndex((item) => item.id === card.id);
|
|
443
|
+
const isInsert = index < 0;
|
|
444
|
+
const cards = isInsert
|
|
445
|
+
? [card, ...target.cards]
|
|
446
|
+
: target.cards.map((item, position) => position === index ? card : item);
|
|
447
|
+
const records = [...(raw.records ?? [])];
|
|
448
|
+
const recordIndex = records.findIndex((item) => item.id === merged.id);
|
|
449
|
+
if (recordIndex >= 0)
|
|
450
|
+
records[recordIndex] = merged;
|
|
451
|
+
else
|
|
452
|
+
records.unshift(merged);
|
|
453
|
+
this.patchItem(key, {
|
|
454
|
+
...target,
|
|
455
|
+
cards,
|
|
456
|
+
totalCount: isInsert ? target.totalCount + 1 : target.totalCount,
|
|
457
|
+
error: null,
|
|
458
|
+
rawData: this.withRecords(raw, records, isInsert ? 1 : 0),
|
|
459
|
+
});
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Overlays the written record on the one being replaced, key by key. A
|
|
464
|
+
* cleared property comes back explicitly (`{ raw: null, value: null }`) and
|
|
465
|
+
* overwrites; only a key the backend left out entirely keeps its previous
|
|
466
|
+
* cell, which beats blanking a value that did not actually change.
|
|
467
|
+
*/
|
|
468
|
+
mergeRecord(previous, next) {
|
|
469
|
+
return {
|
|
470
|
+
...previous,
|
|
471
|
+
...next,
|
|
472
|
+
values: { ...previous.values, ...next.values },
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
withRecords(raw, records, totalCountDelta) {
|
|
476
|
+
return {
|
|
477
|
+
...raw,
|
|
478
|
+
records,
|
|
479
|
+
pagination: raw.pagination
|
|
480
|
+
? {
|
|
481
|
+
...raw.pagination,
|
|
482
|
+
totalCount: Math.max(0, raw.pagination.totalCount + totalCountDelta),
|
|
483
|
+
}
|
|
484
|
+
: raw.pagination,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
patchItem(key, next) {
|
|
488
|
+
this.itemsByKey.set({ ...this.itemsByKey(), [key]: next });
|
|
489
|
+
}
|
|
309
490
|
/**
|
|
310
491
|
* Appends `next` to `current`, dropping any card whose id is already
|
|
311
492
|
* present. Ids are the grid's `@for` track key, so duplicates are a runtime
|
|
@@ -378,8 +559,6 @@ class ClientListStateService {
|
|
|
378
559
|
existing.areaType !== item.areaType) {
|
|
379
560
|
return item;
|
|
380
561
|
}
|
|
381
|
-
const keepPaging = existing.config.isPaginated === item.config.isPaginated &&
|
|
382
|
-
existing.take === item.take;
|
|
383
562
|
return {
|
|
384
563
|
...item,
|
|
385
564
|
loading: existing.loading,
|
|
@@ -390,7 +569,12 @@ class ClientListStateService {
|
|
|
390
569
|
columns: existing.columns,
|
|
391
570
|
rows: existing.rows,
|
|
392
571
|
cards: existing.cards,
|
|
393
|
-
|
|
572
|
+
// Paging is settled by the caller, which compared the incoming config
|
|
573
|
+
// against the previous one. Re-deciding it here — with only the two
|
|
574
|
+
// states and no view of what changed in the config — is what used to
|
|
575
|
+
// drop the user's page whenever their rows-per-page differed from the
|
|
576
|
+
// configured default.
|
|
577
|
+
skip: item.skip,
|
|
394
578
|
take: item.take,
|
|
395
579
|
expanded: existing.expanded,
|
|
396
580
|
dashboardData: existing.dashboardData,
|
|
@@ -1760,6 +1944,40 @@ function toScalar(value) {
|
|
|
1760
1944
|
item['displayName']);
|
|
1761
1945
|
}
|
|
1762
1946
|
|
|
1947
|
+
/**
|
|
1948
|
+
* Whether the offset a list is currently sitting on still describes the same
|
|
1949
|
+
* result set, i.e. this reconfigure changed nothing about what gets fetched.
|
|
1950
|
+
*
|
|
1951
|
+
* Hosts rebuild the `configurations` array from a `computed()`, so *any*
|
|
1952
|
+
* unrelated state change — a permission refresh after a write, a language
|
|
1953
|
+
* switch, a re-rendered header template — hands the list a brand new array
|
|
1954
|
+
* with identical content. That must not move the user off their page, and
|
|
1955
|
+
* must not cost a request.
|
|
1956
|
+
*
|
|
1957
|
+
* The page size is compared **as configured**, never as currently applied. The
|
|
1958
|
+
* table owns its rows-per-page: the user picks it from the paginator and
|
|
1959
|
+
* `mt-table` restores a persisted value, and either of those legitimately
|
|
1960
|
+
* leaves the runtime `take` different from the configured one. Reading that
|
|
1961
|
+
* difference as drift to correct is what used to reset the paginator and
|
|
1962
|
+
* refetch page 1 on every reconfigure.
|
|
1963
|
+
*/
|
|
1964
|
+
function preservesPaging(existing, areaType, next) {
|
|
1965
|
+
if (!existing ||
|
|
1966
|
+
existing.type !== 'form' ||
|
|
1967
|
+
existing.areaType !== areaType ||
|
|
1968
|
+
next.type !== 'form') {
|
|
1969
|
+
return false;
|
|
1970
|
+
}
|
|
1971
|
+
const current = existing.config;
|
|
1972
|
+
return (current.isPaginated === next.isPaginated &&
|
|
1973
|
+
current.take === next.take &&
|
|
1974
|
+
current.contextKey === next.contextKey &&
|
|
1975
|
+
serializeFilters(current.filters) === serializeFilters(next.filters));
|
|
1976
|
+
}
|
|
1977
|
+
function serializeFilters(filters) {
|
|
1978
|
+
return JSON.stringify(filters ?? []);
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1763
1981
|
const DEFAULT_AREA_TYPE = 'table';
|
|
1764
1982
|
const DEFAULT_MODE = 'auto';
|
|
1765
1983
|
const DEFAULT_TYPE = 'form';
|
|
@@ -1791,6 +2009,14 @@ class ClientList {
|
|
|
1791
2009
|
inFlightRequestSignatures = new Map();
|
|
1792
2010
|
fulfilledRequestSignatures = new Map();
|
|
1793
2011
|
runtimeFilters = new Map();
|
|
2012
|
+
/**
|
|
2013
|
+
* The `propertyKeys` the last table request carried, per item. `undefined`
|
|
2014
|
+
* means the request left the projection to the backend, which is a distinct
|
|
2015
|
+
* state from "not loaded yet" — hence the map rather than a nullable field.
|
|
2016
|
+
*/
|
|
2017
|
+
requestPropertyKeys = new Map();
|
|
2018
|
+
/** Single-record read-backs, kept apart from the page loads they must not cancel. */
|
|
2019
|
+
recordSubscriptions = new Map();
|
|
1794
2020
|
constructor() {
|
|
1795
2021
|
effect(() => {
|
|
1796
2022
|
const configs = this.configurations();
|
|
@@ -1860,6 +2086,93 @@ class ClientList {
|
|
|
1860
2086
|
return;
|
|
1861
2087
|
this.loadItem(item, true);
|
|
1862
2088
|
}
|
|
2089
|
+
/**
|
|
2090
|
+
* The query settings this list renders with, shaped as the `returnRecord`
|
|
2091
|
+
* block of a `process-submit` payload. Send it with a create/update so the
|
|
2092
|
+
* backend projects the written record the way this list reads records, then
|
|
2093
|
+
* hand the response to {@link applyWriteResult} — that pair replaces the
|
|
2094
|
+
* full page re-fetch a write used to cost.
|
|
2095
|
+
*
|
|
2096
|
+
* Returns `null` for an informative dashboard (no records to project) or an
|
|
2097
|
+
* unknown key.
|
|
2098
|
+
*/
|
|
2099
|
+
getReturnRecordRequest(itemKey) {
|
|
2100
|
+
const item = this.state.itemsByKey()[itemKey];
|
|
2101
|
+
if (!item || item.type !== 'form')
|
|
2102
|
+
return null;
|
|
2103
|
+
const includeState = [...CLIENT_LIST_DEFAULT_INCLUDE_STATE];
|
|
2104
|
+
const culture = this.transloco.getActiveLang();
|
|
2105
|
+
if (item.areaType === 'cards') {
|
|
2106
|
+
return { projection: 'Card', includeState, culture };
|
|
2107
|
+
}
|
|
2108
|
+
const propertyKeys = this.requestPropertyKeys.get(itemKey);
|
|
2109
|
+
return {
|
|
2110
|
+
projection: 'Table',
|
|
2111
|
+
surfaceKey: 'table',
|
|
2112
|
+
includeState,
|
|
2113
|
+
// Omitted, not empty, when the table let the backend pick the
|
|
2114
|
+
// projection — an empty array would read as "no columns".
|
|
2115
|
+
...(propertyKeys?.length ? { propertyKeys } : {}),
|
|
2116
|
+
culture,
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
/**
|
|
2120
|
+
* Applies a create/update to the list in place, without re-fetching the
|
|
2121
|
+
* page. Pass the `process-submit` response as-is.
|
|
2122
|
+
*
|
|
2123
|
+
* Three outcomes, in the order they are tried:
|
|
2124
|
+
* - the write carried a usable record projection → splice it in, no request;
|
|
2125
|
+
* - it did not (`recordProjectionStatus: 'Unavailable'`, or the list has
|
|
2126
|
+
* nothing to splice into) → read that one record back by id. The write
|
|
2127
|
+
* already succeeded; this only reads, and never re-submits;
|
|
2128
|
+
* - nothing identifies the record at all → reload the page, as before.
|
|
2129
|
+
*
|
|
2130
|
+
* An approval-queued submit is a no-op: nothing has been written yet.
|
|
2131
|
+
*
|
|
2132
|
+
* Note that the list cannot know where the backend would have sorted a new
|
|
2133
|
+
* record, so an insert lands at the top of the page in view.
|
|
2134
|
+
*/
|
|
2135
|
+
applyWriteResult(itemKey, result) {
|
|
2136
|
+
const item = this.state.itemsByKey()[itemKey];
|
|
2137
|
+
if (!item || item.type !== 'form')
|
|
2138
|
+
return;
|
|
2139
|
+
if (this.isPendingApprovalWrite(result))
|
|
2140
|
+
return;
|
|
2141
|
+
const recordId = this.toFiniteNumber(result?.recordId);
|
|
2142
|
+
const record = this.toWriteRecord(result, recordId);
|
|
2143
|
+
if (record) {
|
|
2144
|
+
// A failed splice means the list holds no page to splice into, so
|
|
2145
|
+
// reading the record back would not help either.
|
|
2146
|
+
if (this.state.upsertRecord(itemKey, record, item.config)) {
|
|
2147
|
+
this.afterRecordApplied(itemKey);
|
|
2148
|
+
}
|
|
2149
|
+
else {
|
|
2150
|
+
this.reloadByKey(itemKey);
|
|
2151
|
+
}
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
2154
|
+
if (recordId != null) {
|
|
2155
|
+
this.readRecordBack(item, recordId);
|
|
2156
|
+
return;
|
|
2157
|
+
}
|
|
2158
|
+
this.reloadByKey(itemKey);
|
|
2159
|
+
}
|
|
2160
|
+
/**
|
|
2161
|
+
* Drops a deleted record from the list in place. Falls back to a reload when
|
|
2162
|
+
* the record is not part of the loaded set, so a stale list still corrects
|
|
2163
|
+
* itself.
|
|
2164
|
+
*/
|
|
2165
|
+
removeRecord(itemKey, recordId) {
|
|
2166
|
+
const item = this.state.itemsByKey()[itemKey];
|
|
2167
|
+
if (!item || item.type !== 'form')
|
|
2168
|
+
return;
|
|
2169
|
+
if (!this.state.removeRecord(itemKey, recordId, item.config)) {
|
|
2170
|
+
this.reloadByKey(itemKey);
|
|
2171
|
+
return;
|
|
2172
|
+
}
|
|
2173
|
+
this.runtimeActions.invalidateRow(itemKey, recordId);
|
|
2174
|
+
this.afterRecordApplied(itemKey, { invalidateRuntimeActions: false });
|
|
2175
|
+
}
|
|
1863
2176
|
toggleExpanded(key) {
|
|
1864
2177
|
this.state.toggleExpanded(key);
|
|
1865
2178
|
}
|
|
@@ -2110,6 +2423,89 @@ class ClientList {
|
|
|
2110
2423
|
config.onActionExecuted?.(action, ctx);
|
|
2111
2424
|
}
|
|
2112
2425
|
}
|
|
2426
|
+
/**
|
|
2427
|
+
* A submit that only queued an approval request has written nothing — the
|
|
2428
|
+
* record it describes does not exist yet, so the list must not move.
|
|
2429
|
+
*/
|
|
2430
|
+
isPendingApprovalWrite(result) {
|
|
2431
|
+
return (String(result?.status ?? '')
|
|
2432
|
+
.trim()
|
|
2433
|
+
.toLowerCase() === 'pendingapproval');
|
|
2434
|
+
}
|
|
2435
|
+
/**
|
|
2436
|
+
* The canonical record from a write response, or `null` when there is none
|
|
2437
|
+
* to trust. A backend that has not shipped the canonical projection yet
|
|
2438
|
+
* answers with neither `recordProjectionStatus: 'Available'` nor `values`,
|
|
2439
|
+
* and falls through to the read-back path — which is why this is a plain
|
|
2440
|
+
* shape check and not a parse of alternative record shapes.
|
|
2441
|
+
*/
|
|
2442
|
+
toWriteRecord(result, recordId) {
|
|
2443
|
+
if (result?.recordProjectionStatus !== 'Available')
|
|
2444
|
+
return null;
|
|
2445
|
+
const record = result.record;
|
|
2446
|
+
if (!record?.values)
|
|
2447
|
+
return null;
|
|
2448
|
+
const id = this.toFiniteNumber(record.id) ?? recordId;
|
|
2449
|
+
if (id == null)
|
|
2450
|
+
return null;
|
|
2451
|
+
return { ...record, id };
|
|
2452
|
+
}
|
|
2453
|
+
/**
|
|
2454
|
+
* Reads one record back through `fetch/query` with this list's own settings.
|
|
2455
|
+
* Taken when the write could not project the record itself — the write has
|
|
2456
|
+
* already happened, so this reads and never re-submits.
|
|
2457
|
+
*/
|
|
2458
|
+
readRecordBack(item, recordId) {
|
|
2459
|
+
const config = item.config;
|
|
2460
|
+
const request = this.getReturnRecordRequest(item.key);
|
|
2461
|
+
if (!request || !this.hasValidIdentifiers(config)) {
|
|
2462
|
+
this.reloadByKey(item.key);
|
|
2463
|
+
return;
|
|
2464
|
+
}
|
|
2465
|
+
// The host's own scope filters plus the record id. Runtime filters (the
|
|
2466
|
+
// search box, a column filter) are deliberately left out: a record the
|
|
2467
|
+
// user just wrote should come back whether or not it matches whatever
|
|
2468
|
+
// they happen to have typed into the list.
|
|
2469
|
+
const filters = [
|
|
2470
|
+
...config.filters,
|
|
2471
|
+
{
|
|
2472
|
+
key: CLIENT_LIST_RECORD_ID_FILTER_KEY,
|
|
2473
|
+
operator: 'Equals',
|
|
2474
|
+
value: recordId,
|
|
2475
|
+
},
|
|
2476
|
+
];
|
|
2477
|
+
this.recordSubscriptions.get(item.key)?.unsubscribe();
|
|
2478
|
+
const sub = this.api
|
|
2479
|
+
.getRecord(config.contextKey, request, filters)
|
|
2480
|
+
.subscribe({
|
|
2481
|
+
next: (response) => {
|
|
2482
|
+
this.recordSubscriptions.delete(item.key);
|
|
2483
|
+
const record = response.data?.records?.[0];
|
|
2484
|
+
if (!record || !this.state.upsertRecord(item.key, record, config)) {
|
|
2485
|
+
this.reloadByKey(item.key);
|
|
2486
|
+
return;
|
|
2487
|
+
}
|
|
2488
|
+
this.afterRecordApplied(item.key);
|
|
2489
|
+
},
|
|
2490
|
+
error: () => {
|
|
2491
|
+
this.recordSubscriptions.delete(item.key);
|
|
2492
|
+
this.reloadByKey(item.key);
|
|
2493
|
+
},
|
|
2494
|
+
});
|
|
2495
|
+
this.recordSubscriptions.set(item.key, sub);
|
|
2496
|
+
}
|
|
2497
|
+
/**
|
|
2498
|
+
* Runs the tail of a load for a list that changed without one: the row
|
|
2499
|
+
* action cache is stale (a written record's available actions follow its new
|
|
2500
|
+
* state), and hosts hang work off `dataLoaded` / `loaded`.
|
|
2501
|
+
*/
|
|
2502
|
+
afterRecordApplied(itemKey, options) {
|
|
2503
|
+
if (options?.invalidateRuntimeActions !== false) {
|
|
2504
|
+
this.runtimeActions.invalidateList(itemKey);
|
|
2505
|
+
}
|
|
2506
|
+
this.notifyDataLoaded(itemKey);
|
|
2507
|
+
this.loaded.emit(itemKey);
|
|
2508
|
+
}
|
|
2113
2509
|
toRuntimeActionsContext(item, row, config) {
|
|
2114
2510
|
const recordId = this.resolveRecordId(config, row);
|
|
2115
2511
|
if (recordId == null)
|
|
@@ -2147,6 +2543,9 @@ class ClientList {
|
|
|
2147
2543
|
ngOnDestroy() {
|
|
2148
2544
|
this.subscriptions.forEach((sub) => sub.unsubscribe());
|
|
2149
2545
|
this.subscriptions.clear();
|
|
2546
|
+
this.recordSubscriptions.forEach((sub) => sub.unsubscribe());
|
|
2547
|
+
this.recordSubscriptions.clear();
|
|
2548
|
+
this.requestPropertyKeys.clear();
|
|
2150
2549
|
this.rowActionsLoadingFnCache.clear();
|
|
2151
2550
|
this.runtimeActions.clearAll();
|
|
2152
2551
|
}
|
|
@@ -2184,6 +2583,10 @@ class ClientList {
|
|
|
2184
2583
|
const key = config.key ?? this.createItemKey(config, index);
|
|
2185
2584
|
const existing = previousItems[key];
|
|
2186
2585
|
const normalizedConfig = this.toNormalizedConfig(config, type, areaType, mode, isPaginated, take, showHeader, collapseEnabled, layout);
|
|
2586
|
+
// Paging is runtime state, not config state: it survives a reconfigure
|
|
2587
|
+
// that changed nothing about the fetch, and the *applied* page size
|
|
2588
|
+
// (the user's rows-per-page choice) is carried forward with it.
|
|
2589
|
+
const keepsPaging = preservesPaging(existing, areaType, normalizedConfig);
|
|
2187
2590
|
if (isInformative) {
|
|
2188
2591
|
return {
|
|
2189
2592
|
key,
|
|
@@ -2219,12 +2622,13 @@ class ClientList {
|
|
|
2219
2622
|
columns: [],
|
|
2220
2623
|
rows: [],
|
|
2221
2624
|
cards: [],
|
|
2222
|
-
//
|
|
2223
|
-
//
|
|
2224
|
-
// the
|
|
2225
|
-
//
|
|
2226
|
-
|
|
2227
|
-
|
|
2625
|
+
// For infinite scroll `skip` is the next offset into the loaded
|
|
2626
|
+
// set, so it travels with that set: kept while the fetch is
|
|
2627
|
+
// unchanged (the cards themselves are kept too, by
|
|
2628
|
+
// `mergeItemState`), and restarted at 0 the moment the fetch
|
|
2629
|
+
// changes and the loaded set stops meaning anything.
|
|
2630
|
+
skip: keepsPaging ? existing.skip : 0,
|
|
2631
|
+
take: keepsPaging ? existing.take : take,
|
|
2228
2632
|
expanded: existing?.expanded ?? config.collapse?.expandedByDefault ?? true,
|
|
2229
2633
|
dashboardData: null,
|
|
2230
2634
|
rawData: existing?.rawData ?? null,
|
|
@@ -2243,12 +2647,8 @@ class ClientList {
|
|
|
2243
2647
|
columns: [],
|
|
2244
2648
|
rows: [],
|
|
2245
2649
|
cards: [],
|
|
2246
|
-
skip: existing
|
|
2247
|
-
|
|
2248
|
-
existing.take === take
|
|
2249
|
-
? existing.skip
|
|
2250
|
-
: 0,
|
|
2251
|
-
take,
|
|
2650
|
+
skip: keepsPaging ? existing.skip : 0,
|
|
2651
|
+
take: keepsPaging ? existing.take : take,
|
|
2252
2652
|
expanded: existing?.expanded ?? config.collapse?.expandedByDefault ?? true,
|
|
2253
2653
|
dashboardData: null,
|
|
2254
2654
|
rawData: existing?.rawData ?? null,
|
|
@@ -2290,9 +2690,10 @@ class ClientList {
|
|
|
2290
2690
|
this.state.setLoading(key, true);
|
|
2291
2691
|
this.state.setError(key, null);
|
|
2292
2692
|
const sub = this.api.getRows(config.contextKey, query, filters).subscribe({
|
|
2293
|
-
next: (response) => {
|
|
2693
|
+
next: ({ response, propertyKeys }) => {
|
|
2294
2694
|
let hasLoadedData = false;
|
|
2295
2695
|
if (response.data) {
|
|
2696
|
+
this.requestPropertyKeys.set(key, propertyKeys);
|
|
2296
2697
|
this.state.setRowsResult(key, response.data, config, query.skip, query.take);
|
|
2297
2698
|
this.fulfilledRequestSignatures.set(key, requestSignature);
|
|
2298
2699
|
hasLoadedData = true;
|
|
@@ -2563,11 +2964,22 @@ class ClientList {
|
|
|
2563
2964
|
this.fulfilledRequestSignatures.delete(key);
|
|
2564
2965
|
}
|
|
2565
2966
|
}
|
|
2967
|
+
for (const [key, sub] of this.recordSubscriptions.entries()) {
|
|
2968
|
+
if (!activeKeys.has(key)) {
|
|
2969
|
+
sub.unsubscribe();
|
|
2970
|
+
this.recordSubscriptions.delete(key);
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2566
2973
|
for (const key of this.runtimeFilters.keys()) {
|
|
2567
2974
|
if (!activeKeys.has(key)) {
|
|
2568
2975
|
this.runtimeFilters.delete(key);
|
|
2569
2976
|
}
|
|
2570
2977
|
}
|
|
2978
|
+
for (const key of this.requestPropertyKeys.keys()) {
|
|
2979
|
+
if (!activeKeys.has(key)) {
|
|
2980
|
+
this.requestPropertyKeys.delete(key);
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2571
2983
|
for (const key of this.rowActionsLoadingFnCache.keys()) {
|
|
2572
2984
|
if (!activeKeys.has(key)) {
|
|
2573
2985
|
this.rowActionsLoadingFnCache.delete(key);
|
|
@@ -2694,5 +3106,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
2694
3106
|
* Generated bundle index. Do not edit.
|
|
2695
3107
|
*/
|
|
2696
3108
|
|
|
2697
|
-
export { CLIENT_LIST_RECORD_STATE_KEY, ClientList, ClientListApiService, ClientListRuntimeActionsService, ClientListStateService, ClientListToolbarService, defaultResolveRecordId };
|
|
3109
|
+
export { CLIENT_LIST_DEFAULT_INCLUDE_STATE, CLIENT_LIST_RECORD_ID_FILTER_KEY, CLIENT_LIST_RECORD_STATE_KEY, ClientList, ClientListApiService, ClientListRuntimeActionsService, ClientListStateService, ClientListToolbarService, defaultResolveRecordId };
|
|
2698
3110
|
//# sourceMappingURL=masterteam-client-components-client-list.mjs.map
|