commons-shared-web-ui 0.0.79 → 0.0.80
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.
|
@@ -14204,6 +14204,12 @@ class SmartTableComponent {
|
|
|
14204
14204
|
deleteModalOpen = false;
|
|
14205
14205
|
deleteModalConfig = {};
|
|
14206
14206
|
pendingDeleteAction = null;
|
|
14207
|
+
/** code -> label maps for column lookups, bucketed by lookup cache key. */
|
|
14208
|
+
lookupMaps = {};
|
|
14209
|
+
/** Guards against firing the same lookup request twice. */
|
|
14210
|
+
lookupInFlight = new Set();
|
|
14211
|
+
/** Full-list lookups already attempted — stops an empty/failed list refetching on every page. */
|
|
14212
|
+
lookupLoaded = new Set();
|
|
14207
14213
|
searchSubject = new Subject();
|
|
14208
14214
|
stickyHeaders;
|
|
14209
14215
|
resizeObserver = null;
|
|
@@ -14244,12 +14250,14 @@ class SmartTableComponent {
|
|
|
14244
14250
|
this.isSortActive = true;
|
|
14245
14251
|
}
|
|
14246
14252
|
this.loadFilterOptions();
|
|
14253
|
+
this.loadColumnLookups();
|
|
14247
14254
|
if (this.tableData !== undefined) {
|
|
14248
14255
|
// External-data mode: sync incoming data, skip internal API call
|
|
14249
14256
|
this.data = this.tableData;
|
|
14250
14257
|
if (this.totalItemsCount !== undefined) {
|
|
14251
14258
|
this.totalItems = this.totalItemsCount;
|
|
14252
14259
|
}
|
|
14260
|
+
this.loadColumnLookups();
|
|
14253
14261
|
}
|
|
14254
14262
|
else {
|
|
14255
14263
|
this.loadData();
|
|
@@ -14260,6 +14268,7 @@ class SmartTableComponent {
|
|
|
14260
14268
|
// External data changed — sync directly without an API call
|
|
14261
14269
|
if (changes['tableData']) {
|
|
14262
14270
|
this.data = this.tableData || [];
|
|
14271
|
+
this.loadColumnLookups();
|
|
14263
14272
|
}
|
|
14264
14273
|
// Total count updated from parent
|
|
14265
14274
|
if (changes['totalItemsCount'] && this.totalItemsCount !== undefined) {
|
|
@@ -14270,6 +14279,7 @@ class SmartTableComponent {
|
|
|
14270
14279
|
this.sortColumnsBySequence();
|
|
14271
14280
|
this.applyPaginationDefaults();
|
|
14272
14281
|
this.loadFilterOptions();
|
|
14282
|
+
this.loadColumnLookups();
|
|
14273
14283
|
if (this.tableData === undefined) {
|
|
14274
14284
|
this.loadData();
|
|
14275
14285
|
}
|
|
@@ -14480,6 +14490,7 @@ class SmartTableComponent {
|
|
|
14480
14490
|
}
|
|
14481
14491
|
}
|
|
14482
14492
|
this.syncSelection();
|
|
14493
|
+
this.loadColumnLookups();
|
|
14483
14494
|
});
|
|
14484
14495
|
}
|
|
14485
14496
|
/**
|
|
@@ -14840,6 +14851,12 @@ class SmartTableComponent {
|
|
|
14840
14851
|
// Support nested properties via labelPath or key
|
|
14841
14852
|
const path = col.labelPath || col.key;
|
|
14842
14853
|
let val = this.getValueByPath(row, path);
|
|
14854
|
+
// Code -> label resolution (roles, MDM master data, ...). Falls back to the
|
|
14855
|
+
// raw code until the lookup response lands, so the cell is never blank.
|
|
14856
|
+
const lookupCfg = this.getLookupConfig(col);
|
|
14857
|
+
if (lookupCfg) {
|
|
14858
|
+
val = this.resolveLookupLabel(val, lookupCfg);
|
|
14859
|
+
}
|
|
14843
14860
|
// Formatting (Date, etc.)
|
|
14844
14861
|
if (col.type === 'date' && val) {
|
|
14845
14862
|
if (col.dateFormat) {
|
|
@@ -14859,7 +14876,11 @@ class SmartTableComponent {
|
|
|
14859
14876
|
return val;
|
|
14860
14877
|
}
|
|
14861
14878
|
getBadgeClass(row, col) {
|
|
14862
|
-
|
|
14879
|
+
// A lookup column keeps its badgeConfig keyed on the raw code rather than
|
|
14880
|
+
// the resolved label, so pick the variant from the underlying value.
|
|
14881
|
+
const val = this.getLookupConfig(col)
|
|
14882
|
+
? this.normalizeRuleValue(this.getValueByPath(row, col.labelPath || col.key))
|
|
14883
|
+
: this.getCellValue(row, col);
|
|
14863
14884
|
const strVal = String(val);
|
|
14864
14885
|
// Config approach
|
|
14865
14886
|
if (col.badgeConfig && col.badgeConfig[strVal]) {
|
|
@@ -14890,6 +14911,197 @@ class SmartTableComponent {
|
|
|
14890
14911
|
replaceParams(template, row) {
|
|
14891
14912
|
return template.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => row[key] || '');
|
|
14892
14913
|
}
|
|
14914
|
+
// --- Column Lookups (code -> label) ---
|
|
14915
|
+
/**
|
|
14916
|
+
* Merges a column's inline lookup config with the shared definition it
|
|
14917
|
+
* references (`TableConfig.lookups`). Inline properties win.
|
|
14918
|
+
*/
|
|
14919
|
+
getLookupConfig(col) {
|
|
14920
|
+
const raw = col?.lookupConfig;
|
|
14921
|
+
if (!raw)
|
|
14922
|
+
return null;
|
|
14923
|
+
const shared = raw.ref ? this.config?.lookups?.[raw.ref] : undefined;
|
|
14924
|
+
const merged = shared ? { ...shared, ...raw } : raw;
|
|
14925
|
+
return merged.apiUrl ? merged : null;
|
|
14926
|
+
}
|
|
14927
|
+
/** Bucket the resolved code -> label map lives in. Identical endpoints share one. */
|
|
14928
|
+
getLookupCacheKey(cfg) {
|
|
14929
|
+
if (cfg.cacheKey)
|
|
14930
|
+
return cfg.cacheKey;
|
|
14931
|
+
if (cfg.ref)
|
|
14932
|
+
return cfg.ref;
|
|
14933
|
+
return [
|
|
14934
|
+
(cfg.apiMethod || 'GET').toUpperCase(),
|
|
14935
|
+
cfg.apiUrl,
|
|
14936
|
+
JSON.stringify(cfg.queryParams || {}),
|
|
14937
|
+
JSON.stringify(cfg.apiPayload || {})
|
|
14938
|
+
].join('|');
|
|
14939
|
+
}
|
|
14940
|
+
/**
|
|
14941
|
+
* Fires the lookup request for every column that declares one.
|
|
14942
|
+
* - Full-list mode (no `paramName`): one request per endpoint, ever.
|
|
14943
|
+
* - Targeted mode (`paramName` set): requests only the codes on the current
|
|
14944
|
+
* page that are not cached yet, so it re-runs as the user pages/searches.
|
|
14945
|
+
*/
|
|
14946
|
+
loadColumnLookups() {
|
|
14947
|
+
if (!this.config?.columns)
|
|
14948
|
+
return;
|
|
14949
|
+
this.config.columns.forEach(col => {
|
|
14950
|
+
const cfg = this.getLookupConfig(col);
|
|
14951
|
+
if (!cfg)
|
|
14952
|
+
return;
|
|
14953
|
+
const cacheKey = this.getLookupCacheKey(cfg);
|
|
14954
|
+
if (!this.lookupMaps[cacheKey]) {
|
|
14955
|
+
this.lookupMaps[cacheKey] = new Map();
|
|
14956
|
+
}
|
|
14957
|
+
const map = this.lookupMaps[cacheKey];
|
|
14958
|
+
if (cfg.paramName) {
|
|
14959
|
+
const missing = this.collectColumnCodes(col)
|
|
14960
|
+
.filter(code => !map.has(this.normalizeLookupKey(code, cfg)));
|
|
14961
|
+
if (!missing.length)
|
|
14962
|
+
return;
|
|
14963
|
+
const requestKey = cacheKey + '::' + missing.slice().sort().join(',');
|
|
14964
|
+
if (this.lookupInFlight.has(requestKey))
|
|
14965
|
+
return;
|
|
14966
|
+
this.lookupInFlight.add(requestKey);
|
|
14967
|
+
this.fetchLookup(cfg, cacheKey, requestKey, missing);
|
|
14968
|
+
}
|
|
14969
|
+
else {
|
|
14970
|
+
if (this.lookupInFlight.has(cacheKey) || this.lookupLoaded.has(cacheKey))
|
|
14971
|
+
return;
|
|
14972
|
+
this.lookupInFlight.add(cacheKey);
|
|
14973
|
+
this.fetchLookup(cfg, cacheKey, cacheKey, null);
|
|
14974
|
+
}
|
|
14975
|
+
});
|
|
14976
|
+
}
|
|
14977
|
+
/** Distinct, non-empty codes held by `col` across the rows currently loaded. */
|
|
14978
|
+
collectColumnCodes(col) {
|
|
14979
|
+
const path = col.labelPath || col.key;
|
|
14980
|
+
const codes = new Set();
|
|
14981
|
+
(this.data || []).forEach(row => {
|
|
14982
|
+
const raw = this.getValueByPath(row, path);
|
|
14983
|
+
const values = Array.isArray(raw) ? raw : [raw];
|
|
14984
|
+
values.forEach(v => {
|
|
14985
|
+
const code = this.normalizeRuleValue(v);
|
|
14986
|
+
if (code !== null && code !== undefined && code !== '' && typeof code !== 'object') {
|
|
14987
|
+
codes.add(String(code));
|
|
14988
|
+
}
|
|
14989
|
+
});
|
|
14990
|
+
});
|
|
14991
|
+
return Array.from(codes);
|
|
14992
|
+
}
|
|
14993
|
+
/** Substitutes the `{codes}` / `{value}` placeholder in a url or param value. */
|
|
14994
|
+
applyLookupPlaceholders(input, codes) {
|
|
14995
|
+
return input.replace(/\{codes\}/g, codes).replace(/\{value\}/g, codes);
|
|
14996
|
+
}
|
|
14997
|
+
/**
|
|
14998
|
+
* Issues the lookup request with any verb, then folds the response into the
|
|
14999
|
+
* code -> label map. `codes` is null in full-list mode.
|
|
15000
|
+
*/
|
|
15001
|
+
fetchLookup(cfg, cacheKey, requestKey, codes) {
|
|
15002
|
+
const method = (cfg.apiMethod || 'GET').toUpperCase();
|
|
15003
|
+
const codeCsv = (codes || []).join(cfg.paramSeparator || ',');
|
|
15004
|
+
let headers = this.getHeaders();
|
|
15005
|
+
Object.keys(cfg.headers || {}).forEach(key => {
|
|
15006
|
+
headers = headers.set(key, cfg.headers[key]);
|
|
15007
|
+
});
|
|
15008
|
+
let params = new HttpParams();
|
|
15009
|
+
const addParam = (key, value) => {
|
|
15010
|
+
if (value === null || value === undefined)
|
|
15011
|
+
return;
|
|
15012
|
+
params = params.append(key, this.applyLookupPlaceholders(String(value), codeCsv));
|
|
15013
|
+
};
|
|
15014
|
+
Object.keys(cfg.queryParams || {}).forEach(key => {
|
|
15015
|
+
const value = cfg.queryParams[key];
|
|
15016
|
+
if (Array.isArray(value)) {
|
|
15017
|
+
value.forEach(item => addParam(key, item));
|
|
15018
|
+
}
|
|
15019
|
+
else {
|
|
15020
|
+
addParam(key, value);
|
|
15021
|
+
}
|
|
15022
|
+
});
|
|
15023
|
+
const body = cfg.apiPayload !== undefined && cfg.apiPayload !== null
|
|
15024
|
+
? JSON.parse(JSON.stringify(cfg.apiPayload))
|
|
15025
|
+
: {};
|
|
15026
|
+
// Targeted mode: attach the page's codes as a query param (GET) or body key.
|
|
15027
|
+
if (codes && cfg.paramName) {
|
|
15028
|
+
const format = cfg.paramFormat || (method === 'GET' ? 'csv' : 'array');
|
|
15029
|
+
if (method === 'GET') {
|
|
15030
|
+
if (format === 'repeat') {
|
|
15031
|
+
codes.forEach(code => { params = params.append(cfg.paramName, code); });
|
|
15032
|
+
}
|
|
15033
|
+
else {
|
|
15034
|
+
params = params.set(cfg.paramName, codeCsv);
|
|
15035
|
+
}
|
|
15036
|
+
}
|
|
15037
|
+
else {
|
|
15038
|
+
body[cfg.paramName] = format === 'csv' ? codeCsv : codes;
|
|
15039
|
+
}
|
|
15040
|
+
}
|
|
15041
|
+
const url = this.applyLookupPlaceholders(cfg.apiUrl, codeCsv);
|
|
15042
|
+
const request$ = method === 'GET'
|
|
15043
|
+
? this.http.get(url, { headers, params })
|
|
15044
|
+
: this.http.request(method, url, { body, headers, params });
|
|
15045
|
+
request$.pipe(finalize(() => {
|
|
15046
|
+
this.lookupInFlight.delete(requestKey);
|
|
15047
|
+
// Full-list mode is a one-shot fetch: never re-request it, even if the
|
|
15048
|
+
// list came back empty or the call failed.
|
|
15049
|
+
if (!codes)
|
|
15050
|
+
this.lookupLoaded.add(cacheKey);
|
|
15051
|
+
}), catchError(err => {
|
|
15052
|
+
console.error('[SmartTable] Lookup failed for ' + cfg.apiUrl, err);
|
|
15053
|
+
return of(null);
|
|
15054
|
+
})).subscribe((response) => {
|
|
15055
|
+
if (response === null || response === undefined)
|
|
15056
|
+
return;
|
|
15057
|
+
const extracted = this.getValueByPath(response, cfg.dataPath || '');
|
|
15058
|
+
const items = Array.isArray(extracted) ? extracted : (extracted ? [extracted] : []);
|
|
15059
|
+
const map = this.lookupMaps[cacheKey] || (this.lookupMaps[cacheKey] = new Map());
|
|
15060
|
+
items.forEach((item) => {
|
|
15061
|
+
const code = this.getValueByPath(item, cfg.valueKey || 'code');
|
|
15062
|
+
if (code === null || code === undefined || code === '')
|
|
15063
|
+
return;
|
|
15064
|
+
const label = this.coerceLookupLabel(this.getValueByPath(item, cfg.labelKey || 'name'), code);
|
|
15065
|
+
map.set(this.normalizeLookupKey(code, cfg), label);
|
|
15066
|
+
});
|
|
15067
|
+
this.cdr.markForCheck();
|
|
15068
|
+
});
|
|
15069
|
+
}
|
|
15070
|
+
/** A label must be printable — objects/arrays degrade to the raw code. */
|
|
15071
|
+
coerceLookupLabel(label, code) {
|
|
15072
|
+
if (label === null || label === undefined || label === '')
|
|
15073
|
+
return String(code);
|
|
15074
|
+
if (typeof label === 'object')
|
|
15075
|
+
return String(code);
|
|
15076
|
+
return String(label);
|
|
15077
|
+
}
|
|
15078
|
+
/** Map key for a code, honouring `caseInsensitive` (default true). */
|
|
15079
|
+
normalizeLookupKey(code, cfg) {
|
|
15080
|
+
const str = String(code);
|
|
15081
|
+
return cfg.caseInsensitive === false ? str : str.toLowerCase();
|
|
15082
|
+
}
|
|
15083
|
+
/**
|
|
15084
|
+
* Turns the raw cell value into its label(s). Accepts a single code, an array
|
|
15085
|
+
* of codes, or objects wrapping a code. Unresolved codes are shown as-is
|
|
15086
|
+
* (`fallback: 'code'`, the default) or dropped (`fallback: 'empty'`).
|
|
15087
|
+
* Returns null when nothing is left, so the caller applies `emptyValue`.
|
|
15088
|
+
*/
|
|
15089
|
+
resolveLookupLabel(value, cfg) {
|
|
15090
|
+
if (value === null || value === undefined || value === '')
|
|
15091
|
+
return value;
|
|
15092
|
+
const map = this.lookupMaps[this.getLookupCacheKey(cfg)];
|
|
15093
|
+
const values = Array.isArray(value) ? value : [value];
|
|
15094
|
+
const labels = values.map(entry => {
|
|
15095
|
+
const code = this.normalizeRuleValue(entry);
|
|
15096
|
+
if (code === null || code === undefined || code === '' || typeof code === 'object')
|
|
15097
|
+
return null;
|
|
15098
|
+
const hit = map?.get(this.normalizeLookupKey(code, cfg));
|
|
15099
|
+
if (hit !== undefined)
|
|
15100
|
+
return hit;
|
|
15101
|
+
return cfg.fallback === 'empty' ? null : String(code);
|
|
15102
|
+
}).filter(label => label !== null && label !== '');
|
|
15103
|
+
return labels.length ? labels.join(cfg.separator ?? ', ') : null;
|
|
15104
|
+
}
|
|
14893
15105
|
loadFilterOptions() {
|
|
14894
15106
|
if (!this.config.filters)
|
|
14895
15107
|
return;
|