commons-shared-web-ui 0.0.78 → 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.
|
@@ -6067,7 +6067,12 @@ class SmartFormComponent {
|
|
|
6067
6067
|
const payloadPath = cfg.payloadPrefix ? `${cfg.payloadPrefix}.${payloadField}` : payloadField;
|
|
6068
6068
|
const apiUrl = (cfg.dataApiUrl || '')
|
|
6069
6069
|
.replace(/\{classCode\}/g, encodeURIComponent(code))
|
|
6070
|
-
.replace(/\{templateCode\}/g, encodeURIComponent(templateCode))
|
|
6070
|
+
.replace(/\{templateCode\}/g, encodeURIComponent(templateCode))
|
|
6071
|
+
// Multiple hierarchy templates can share the same templateCode (see 'byId' selection
|
|
6072
|
+
// above, distinguished by templateName with one flagged isDefault) — hierarchyTemplateCode
|
|
6073
|
+
// alone can't disambiguate between them server-side either, so the resolved template's own
|
|
6074
|
+
// unique numeric id is offered as a second placeholder for configs that need it.
|
|
6075
|
+
.replace(/\{templateId\}/g, encodeURIComponent(template?.id ?? ''));
|
|
6071
6076
|
const optionConfig = {
|
|
6072
6077
|
apiUrl,
|
|
6073
6078
|
dataPath: cfg.dataPath,
|
|
@@ -6113,7 +6118,15 @@ class SmartFormComponent {
|
|
|
6113
6118
|
}
|
|
6114
6119
|
if (nodeOvr['placeholder'])
|
|
6115
6120
|
fieldDef['placeholder'] = nodeOvr['placeholder'];
|
|
6121
|
+
if (nodeOvr['disabled'])
|
|
6122
|
+
fieldDef['disabled'] = true;
|
|
6116
6123
|
fields.push(fieldDef);
|
|
6124
|
+
// `leaf: true` stops the walk from descending past this classCode — e.g. a filter
|
|
6125
|
+
// that should offer Country/State/District but not whatever the tenant's template
|
|
6126
|
+
// configures below District (Ward, Sub-District, ...). Without this every consumer
|
|
6127
|
+
// of a given hierarchy template is forced to render its full depth.
|
|
6128
|
+
if (nodeOvr['leaf'])
|
|
6129
|
+
return;
|
|
6117
6130
|
const children = childrenMap.get(code) || [];
|
|
6118
6131
|
if (children.length > 1) {
|
|
6119
6132
|
// Branch point: dynamically build Area Type selector from children
|
|
@@ -14191,6 +14204,12 @@ class SmartTableComponent {
|
|
|
14191
14204
|
deleteModalOpen = false;
|
|
14192
14205
|
deleteModalConfig = {};
|
|
14193
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();
|
|
14194
14213
|
searchSubject = new Subject();
|
|
14195
14214
|
stickyHeaders;
|
|
14196
14215
|
resizeObserver = null;
|
|
@@ -14231,12 +14250,14 @@ class SmartTableComponent {
|
|
|
14231
14250
|
this.isSortActive = true;
|
|
14232
14251
|
}
|
|
14233
14252
|
this.loadFilterOptions();
|
|
14253
|
+
this.loadColumnLookups();
|
|
14234
14254
|
if (this.tableData !== undefined) {
|
|
14235
14255
|
// External-data mode: sync incoming data, skip internal API call
|
|
14236
14256
|
this.data = this.tableData;
|
|
14237
14257
|
if (this.totalItemsCount !== undefined) {
|
|
14238
14258
|
this.totalItems = this.totalItemsCount;
|
|
14239
14259
|
}
|
|
14260
|
+
this.loadColumnLookups();
|
|
14240
14261
|
}
|
|
14241
14262
|
else {
|
|
14242
14263
|
this.loadData();
|
|
@@ -14247,6 +14268,7 @@ class SmartTableComponent {
|
|
|
14247
14268
|
// External data changed — sync directly without an API call
|
|
14248
14269
|
if (changes['tableData']) {
|
|
14249
14270
|
this.data = this.tableData || [];
|
|
14271
|
+
this.loadColumnLookups();
|
|
14250
14272
|
}
|
|
14251
14273
|
// Total count updated from parent
|
|
14252
14274
|
if (changes['totalItemsCount'] && this.totalItemsCount !== undefined) {
|
|
@@ -14257,6 +14279,7 @@ class SmartTableComponent {
|
|
|
14257
14279
|
this.sortColumnsBySequence();
|
|
14258
14280
|
this.applyPaginationDefaults();
|
|
14259
14281
|
this.loadFilterOptions();
|
|
14282
|
+
this.loadColumnLookups();
|
|
14260
14283
|
if (this.tableData === undefined) {
|
|
14261
14284
|
this.loadData();
|
|
14262
14285
|
}
|
|
@@ -14467,6 +14490,7 @@ class SmartTableComponent {
|
|
|
14467
14490
|
}
|
|
14468
14491
|
}
|
|
14469
14492
|
this.syncSelection();
|
|
14493
|
+
this.loadColumnLookups();
|
|
14470
14494
|
});
|
|
14471
14495
|
}
|
|
14472
14496
|
/**
|
|
@@ -14827,6 +14851,12 @@ class SmartTableComponent {
|
|
|
14827
14851
|
// Support nested properties via labelPath or key
|
|
14828
14852
|
const path = col.labelPath || col.key;
|
|
14829
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
|
+
}
|
|
14830
14860
|
// Formatting (Date, etc.)
|
|
14831
14861
|
if (col.type === 'date' && val) {
|
|
14832
14862
|
if (col.dateFormat) {
|
|
@@ -14846,7 +14876,11 @@ class SmartTableComponent {
|
|
|
14846
14876
|
return val;
|
|
14847
14877
|
}
|
|
14848
14878
|
getBadgeClass(row, col) {
|
|
14849
|
-
|
|
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);
|
|
14850
14884
|
const strVal = String(val);
|
|
14851
14885
|
// Config approach
|
|
14852
14886
|
if (col.badgeConfig && col.badgeConfig[strVal]) {
|
|
@@ -14877,6 +14911,197 @@ class SmartTableComponent {
|
|
|
14877
14911
|
replaceParams(template, row) {
|
|
14878
14912
|
return template.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => row[key] || '');
|
|
14879
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
|
+
}
|
|
14880
15105
|
loadFilterOptions() {
|
|
14881
15106
|
if (!this.config.filters)
|
|
14882
15107
|
return;
|