@praxisui/list 8.0.0-beta.85 → 8.0.0-beta.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.
@@ -31,7 +31,7 @@ import { MatTooltipModule } from '@angular/material/tooltip';
31
31
  import * as i1$1 from '@angular/forms';
32
32
  import { FormsModule, FormControl, ReactiveFormsModule, FormGroup } from '@angular/forms';
33
33
  import { BehaviorSubject, combineLatest, of, Subject, debounceTime, takeUntil, firstValueFrom, distinctUntilChanged as distinctUntilChanged$1, Subscription } from 'rxjs';
34
- import { auditTime, switchMap, map, catchError, finalize, shareReplay, debounceTime as debounceTime$1, distinctUntilChanged, tap, take, takeUntil as takeUntil$1 } from 'rxjs/operators';
34
+ import { map, distinctUntilChanged, switchMap, catchError, finalize, shareReplay, debounceTime as debounceTime$1, tap, take, takeUntil as takeUntil$1 } from 'rxjs/operators';
35
35
  import { PraxisRichContent } from '@praxisui/rich-content';
36
36
  import { SETTINGS_PANEL_DATA, SettingsPanelService } from '@praxisui/settings-panel';
37
37
  import * as i3$3 from '@angular/material/tabs';
@@ -60,9 +60,7 @@ function evalExpr(expr, ctx, options) {
60
60
  return expr.replace(/\$\{([^}]+)\}/g, (_, raw) => {
61
61
  try {
62
62
  const { baseExpr, pipe } = splitFirstPipe(raw);
63
- const value = baseExpr
64
- .split('.')
65
- .reduce((acc, k) => (acc == null ? undefined : acc[k]), ctx);
63
+ const value = resolveTemplateExpressionValue(baseExpr, ctx);
66
64
  let out = value == null ? '' : String(value);
67
65
  if (pipe) {
68
66
  const { name, args } = parsePipe(pipe);
@@ -73,7 +71,9 @@ function evalExpr(expr, ctx, options) {
73
71
  }
74
72
  if (name === 'date') {
75
73
  const [localeArg, styleArg] = parseTwoArgs(args);
76
- const dt = value ? new Date(value) : null;
74
+ const dt = value
75
+ ? new Date(value)
76
+ : null;
77
77
  if (dt && !isNaN(dt.getTime())) {
78
78
  const presentation = resolveTemplatePresentation('date', {
79
79
  localeArg,
@@ -132,6 +132,30 @@ function evalExpr(expr, ctx, options) {
132
132
  }
133
133
  });
134
134
  }
135
+ function resolveTemplateExpressionValue(expr, ctx) {
136
+ const candidates = expr
137
+ .split(/\?\?/)
138
+ .map((candidate) => candidate.trim())
139
+ .filter(Boolean);
140
+ for (const candidate of candidates.length ? candidates : [expr]) {
141
+ const value = resolvePathValue(candidate, ctx);
142
+ if (isPresentTemplateValue(value)) {
143
+ return value;
144
+ }
145
+ }
146
+ return undefined;
147
+ }
148
+ function resolvePathValue(expr, ctx) {
149
+ return expr
150
+ .split('.')
151
+ .map((part) => part.trim())
152
+ .reduce((acc, key) => (acc == null ? undefined : acc[key]), ctx);
153
+ }
154
+ function isPresentTemplateValue(value) {
155
+ return typeof value === 'string'
156
+ ? value.trim().length > 0
157
+ : value !== undefined && value !== null;
158
+ }
135
159
  /**
136
160
  * Evaluate a template definition with basic pipe support.
137
161
  * Supported top-level pipes:
@@ -436,8 +460,8 @@ class ListDataService {
436
460
  const changed = sig !== this.lastSig;
437
461
  this.lastSig = sig;
438
462
  if (changed) {
439
- this.config$.next(config);
440
463
  const ps = config.layout?.pageSize ?? 10;
464
+ this.config$.next(null);
441
465
  this.pageable$.next({
442
466
  ...this.pageable$.value,
443
467
  pageNumber: 0,
@@ -445,6 +469,7 @@ class ListDataService {
445
469
  sort: config.dataSource?.sort,
446
470
  });
447
471
  this.query$.next(config.dataSource?.query || {});
472
+ this.config$.next(config);
448
473
  }
449
474
  }
450
475
  refresh() {
@@ -458,7 +483,12 @@ class ListDataService {
458
483
  this.pageable$,
459
484
  this.query$,
460
485
  this.refresh$,
461
- ]).pipe(auditTime(0), switchMap(([cfg, pageable, query]) => {
486
+ ]).pipe(map(([cfg, pageable, query]) => ({
487
+ cfg,
488
+ pageable,
489
+ query,
490
+ signature: this.buildRequestSignature(cfg, pageable, query),
491
+ })), distinctUntilChanged((previous, current) => previous.signature === current.signature), switchMap(({ cfg, pageable, query }) => {
462
492
  if (!cfg)
463
493
  return of([]);
464
494
  const ds = cfg.dataSource;
@@ -582,6 +612,19 @@ class ListDataService {
582
612
  localData: this.buildLocalDataSignature(dataSource?.data),
583
613
  });
584
614
  }
615
+ buildRequestSignature(config, pageable, query) {
616
+ if (!config)
617
+ return 'no-config';
618
+ const dataSource = config.dataSource;
619
+ return this.safeSerialize({
620
+ resourcePath: dataSource?.resourcePath || '',
621
+ localData: this.buildLocalDataSignature(dataSource?.data),
622
+ query: query || {},
623
+ pageNumber: pageable.pageNumber,
624
+ pageSize: pageable.pageSize,
625
+ sort: pageable.sort || [],
626
+ });
627
+ }
585
628
  buildLocalDataSignature(data) {
586
629
  if (!Array.isArray(data))
587
630
  return 'none';
@@ -11212,7 +11255,14 @@ class PraxisList {
11212
11255
  listId;
11213
11256
  componentInstanceId;
11214
11257
  configPersistenceStrategy = 'local-first';
11215
- queryContext;
11258
+ runtimeQueryContext;
11259
+ set queryContext(value) {
11260
+ this.runtimeQueryContext = value ?? null;
11261
+ this.applyRuntimeQueryContext();
11262
+ }
11263
+ get queryContext() {
11264
+ return this.runtimeQueryContext;
11265
+ }
11216
11266
  form;
11217
11267
  set enableCustomization(value) {
11218
11268
  this.customizationEnabled = value;
@@ -11971,7 +12021,8 @@ class PraxisList {
11971
12021
  : [];
11972
12022
  const items = itemDefs
11973
12023
  .map((node) => this.evaluateTemplateNode(node, item, true))
11974
- .filter((node) => !!node);
12024
+ .filter((node) => !!node)
12025
+ .filter((node) => !this.isEmptyComposeNode(node));
11975
12026
  const direction = compose.direction ||
11976
12027
  (compose.orientation === 'vertical' ? 'column' : 'row');
11977
12028
  return {
@@ -12012,6 +12063,15 @@ class PraxisList {
12012
12063
  style: this.joinStyles(this.metricCssVar('--p-list-metric-progress-fill', color), this.metricCssVar('--p-list-metric-progress-track', progress.trackColor)),
12013
12064
  };
12014
12065
  }
12066
+ isEmptyComposeNode(node) {
12067
+ if (node.type === 'compose') {
12068
+ return !Array.isArray(node.items) || node.items.length === 0;
12069
+ }
12070
+ if (node.type === 'component')
12071
+ return false;
12072
+ const value = node.value;
12073
+ return value === null || value === undefined || String(value).trim() === '';
12074
+ }
12015
12075
  metricLayout(metric, icon, caption, subcaption) {
12016
12076
  const explicit = metric?.layout;
12017
12077
  if (explicit)
@@ -13003,7 +13063,7 @@ class PraxisList {
13003
13063
  }
13004
13064
  applyRuntimeQueryContext() {
13005
13065
  this.initializeDataStreams();
13006
- const normalized = normalizePraxisDataQueryContext(this.queryContext);
13066
+ const normalized = normalizePraxisDataQueryContext(this.runtimeQueryContext);
13007
13067
  const baseQuery = this.config?.dataSource?.query || {};
13008
13068
  const nextQuery = {
13009
13069
  ...baseQuery,
@@ -13051,6 +13111,7 @@ class PraxisList {
13051
13111
  if (plan.runtime?.applyConfig) {
13052
13112
  this.data.setConfig(this.config);
13053
13113
  this.lastQuery = this.config?.dataSource?.query || {};
13114
+ this.lastRuntimeQueryContextSignature = '';
13054
13115
  this.applyRuntimeQueryContext();
13055
13116
  }
13056
13117
  if (plan.runtime?.rebindSelection) {
@@ -13666,9 +13727,7 @@ class PraxisList {
13666
13727
  }
13667
13728
  // trackBy helpers for ngFor
13668
13729
  trackBySection = (_, s) => s?.key ?? _;
13669
- trackByItem = (i, it) => this.config?.selection?.compareBy
13670
- ? (it?.[this.config.selection.compareBy] ?? i)
13671
- : (it?.id ?? i);
13730
+ trackByItem = (i, it) => this.itemStableKey(it) ?? i;
13672
13731
  isActionLoading(actionId, item, index) {
13673
13732
  const key = this.buildActionLoadingKey(actionId, item, index);
13674
13733
  return this.actionLoadingState[key] === true;
@@ -13752,13 +13811,8 @@ class PraxisList {
13752
13811
  return merged;
13753
13812
  }
13754
13813
  buildActionLoadingKey(actionId, item, index) {
13755
- const compareBy = this.config?.selection?.compareBy;
13756
- const idCandidate = compareBy && item && typeof item === 'object'
13757
- ? item?.[compareBy]
13758
- : item?.id;
13759
- if (idCandidate !== null &&
13760
- idCandidate !== undefined &&
13761
- idCandidate !== '') {
13814
+ const idCandidate = this.itemStableKey(item);
13815
+ if (idCandidate !== null && idCandidate !== undefined) {
13762
13816
  return `${actionId}::${String(idCandidate)}`;
13763
13817
  }
13764
13818
  if (item && typeof item === 'object') {
@@ -13816,8 +13870,8 @@ class PraxisList {
13816
13870
  }
13817
13871
  }
13818
13872
  itemExpansionKey(item, index) {
13819
- const candidate = item?.id;
13820
- if (candidate !== null && candidate !== undefined && candidate !== '') {
13873
+ const candidate = this.itemStableKey(item, false);
13874
+ if (candidate !== null && candidate !== undefined) {
13821
13875
  return String(candidate);
13822
13876
  }
13823
13877
  if (item && typeof item === 'object') {
@@ -13825,6 +13879,36 @@ class PraxisList {
13825
13879
  }
13826
13880
  return `idx-${index}`;
13827
13881
  }
13882
+ itemStableKey(item, includeSelectionCompareBy = true) {
13883
+ if (!item || typeof item !== 'object')
13884
+ return null;
13885
+ const fields = [
13886
+ includeSelectionCompareBy ? this.config?.selection?.compareBy : undefined,
13887
+ 'id',
13888
+ 'uuid',
13889
+ 'key',
13890
+ 'value',
13891
+ 'code',
13892
+ 'codigo',
13893
+ 'slug',
13894
+ 'cpf',
13895
+ 'email',
13896
+ ].filter((field) => !!field);
13897
+ for (const field of fields) {
13898
+ const candidate = item?.[field];
13899
+ if (this.isStablePrimitiveKey(candidate)) {
13900
+ return candidate;
13901
+ }
13902
+ }
13903
+ return null;
13904
+ }
13905
+ isStablePrimitiveKey(value) {
13906
+ if (value === null || value === undefined || value === '')
13907
+ return false;
13908
+ return (typeof value === 'string' ||
13909
+ typeof value === 'number' ||
13910
+ typeof value === 'boolean');
13911
+ }
13828
13912
  t(key, fallback) {
13829
13913
  return this.i18n.t(key, undefined, fallback, PRAXIS_LIST_I18N_NAMESPACE);
13830
13914
  }
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "@praxisui/list",
3
- "version": "8.0.0-beta.85",
3
+ "version": "8.0.0-beta.87",
4
4
  "description": "List components and helpers for Praxis UI.",
5
5
  "peerDependencies": {
6
6
  "@angular/common": "^21.0.0",
7
7
  "@angular/core": "^21.0.0",
8
8
  "@angular/material": "^21.0.0",
9
- "@praxisui/dynamic-fields": "^8.0.0-beta.85",
9
+ "@praxisui/dynamic-fields": "^8.0.0-beta.87",
10
10
  "rxjs": ">=7 <9",
11
11
  "@angular/forms": "^21.0.0",
12
12
  "@angular/router": "^21.0.0",
13
- "@praxisui/ai": "^8.0.0-beta.85",
14
- "@praxisui/core": "^8.0.0-beta.85",
15
- "@praxisui/rich-content": "^8.0.0-beta.85",
16
- "@praxisui/settings-panel": "^8.0.0-beta.85"
13
+ "@praxisui/ai": "^8.0.0-beta.87",
14
+ "@praxisui/core": "^8.0.0-beta.87",
15
+ "@praxisui/rich-content": "^8.0.0-beta.87",
16
+ "@praxisui/settings-panel": "^8.0.0-beta.87"
17
17
  },
18
18
  "dependencies": {
19
19
  "tslib": "^2.3.0",
@@ -401,6 +401,7 @@ declare class ListDataService<T = any> {
401
401
  getQuerySnapshot(): Record<string, any>;
402
402
  groupedStream(): Observable<ListSection<T>[]>;
403
403
  private buildConfigSignature;
404
+ private buildRequestSignature;
404
405
  private buildLocalDataSignature;
405
406
  private ensureObjectId;
406
407
  private safeSerialize;
@@ -464,7 +465,9 @@ declare class PraxisList implements OnInit, OnChanges, OnDestroy {
464
465
  listId: string;
465
466
  componentInstanceId?: string;
466
467
  configPersistenceStrategy: 'local-first' | 'input-first';
467
- queryContext?: PraxisDataQueryContext | null;
468
+ private runtimeQueryContext?;
469
+ set queryContext(value: PraxisDataQueryContext | null | undefined);
470
+ get queryContext(): PraxisDataQueryContext | null | undefined;
468
471
  form?: FormGroup | null;
469
472
  set enableCustomization(value: boolean);
470
473
  get enableCustomization(): boolean;
@@ -614,6 +617,7 @@ declare class PraxisList implements OnInit, OnChanges, OnDestroy {
614
617
  private buildMetricView;
615
618
  private buildComposeView;
616
619
  private buildMetricProgress;
620
+ private isEmptyComposeNode;
617
621
  private metricLayout;
618
622
  private metricAlign;
619
623
  private metricIconPosition;
@@ -793,7 +797,7 @@ declare class PraxisList implements OnInit, OnChanges, OnDestroy {
793
797
  ratingIconStyle(def?: any): string;
794
798
  ratingRange(def?: any): number[];
795
799
  trackBySection: (_: number, s: ListSection<any>) => string | number;
796
- trackByItem: (i: number, it: any) => any;
800
+ trackByItem: (i: number, it: any) => string | number | boolean;
797
801
  isActionLoading(actionId: string, item: any, index: number): boolean;
798
802
  private evaluateActionVisibility;
799
803
  private evaluateRuntimeCondition;
@@ -807,6 +811,8 @@ declare class PraxisList implements OnInit, OnChanges, OnDestroy {
807
811
  private toggleExpanded;
808
812
  private syncExpansionState;
809
813
  private itemExpansionKey;
814
+ private itemStableKey;
815
+ private isStablePrimitiveKey;
810
816
  t(key: string, fallback: string): string;
811
817
  private tWithLocale;
812
818
  private ensureExpansionItemObjectId;