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
- const val = this.getCellValue(row, col);
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;