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
- 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);
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;