@sinequa/atomic-angular 1.14.0 → 1.14.1

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.
@@ -7,7 +7,7 @@ import highlightWords from 'highlight-words';
7
7
  import { ActivatedRoute, Router, NavigationEnd, RouterLink, RouterLinkActive, RouterModule } from '@angular/router';
8
8
  import { withDevtools } from '@angular-architects/ngrx-toolkit';
9
9
  import { signalStore, signalStoreFeature, withState, withMethods, patchState, getState, withComputed } from '@ngrx/signals';
10
- import { EngineType, fetchApp, extraColumns, sysLang, fetchPrincipal, globalConfig, filter, getQueryParamsFromUrl, isRedirectPending, info, clearSessionTokens, login, error, setGlobalConfig, initializeAppConfig, notify, addConcepts, queryParamsFromUrl, patchUserSettings, deleteUserSettings, fetchUserSettings, warn, buildPathsAndLevels, escapeExpr, fetchQuery, isAuthenticated, isExpired, debug, AuditEventType, fetchSuggest, isObject, Audit, getMetadata, bisect, isNotInputEvent, fetchSponsoredLinks, translateAggregationToDateOptions, aggItemRegex, parseValueAndOperatorFromItem, fetchSuggestField, fetchSimilarDocuments, logout, fetchChangePassword, fetchSendPasswordResetEmail, expiresSoon, suggestionsToTreeAggregationNodes, labels, fetchLabels, guid, getRelativeDate, createUserProfile, deleteUserProfileProperty, patchUserProfile, isJsonable, addAuditAdditionalInfo, getToken, setToken, createHeaders } from '@sinequa/atomic';
10
+ import { EngineType, fetchApp, extraColumns, sysLang, fetchPrincipal, globalConfig, filter, getQueryParamsFromUrl, isRedirectPending, info, clearSessionTokens, login, error, setGlobalConfig, initializeAppConfig, notify, addConcepts, queryParamsFromUrl, patchUserSettings, deleteUserSettings, fetchUserSettings, warn, buildPathsAndLevels, escapeExpr, fetchQuery, flattenLegacyFilterValues, isAuthenticated, isExpired, debug, AuditEventType, fetchSuggest, isObject, Audit, getMetadata, bisect, isNotInputEvent, fetchSponsoredLinks, translateAggregationToDateOptions, aggItemRegex, parseValueAndOperatorFromItem, fetchSuggestField, fetchSimilarDocuments, logout, fetchChangePassword, fetchSendPasswordResetEmail, expiresSoon, suggestionsToTreeAggregationNodes, labels, fetchLabels, guid, getRelativeDate, createUserProfile, deleteUserProfileProperty, patchUserProfile, isJsonable, addAuditAdditionalInfo, getToken, setToken, createHeaders } from '@sinequa/atomic';
11
11
  import { takeUntilDestroyed, toObservable, toSignal } from '@angular/core/rxjs-interop';
12
12
  import { DatePipe, DATE_PIPE_DEFAULT_TIMEZONE, DATE_PIPE_DEFAULT_OPTIONS, Location, NgTemplateOutlet, NgStyle, NgClass, NgComponentOutlet } from '@angular/common';
13
13
  import { HttpClient, HttpParams, httpResource, HttpResponse, HttpContextToken, HttpHeaders } from '@angular/common/http';
@@ -3526,7 +3526,7 @@ class AggregationsService {
3526
3526
  else {
3527
3527
  // get current filters for the aggregation
3528
3528
  const filters = this.queryParamsStore.getFilter({ field: agg.column, name: agg.name });
3529
- flattenedValues = filters ? this.flattenFilters([filters]) : [];
3529
+ flattenedValues = filters ? flattenLegacyFilterValues([filters]) : [];
3530
3530
  }
3531
3531
  if (agg.isTree) {
3532
3532
  const selectedAgg = agg.items.map(item => this.selectNode(item, flattenedValues));
@@ -3563,34 +3563,44 @@ class AggregationsService {
3563
3563
  */
3564
3564
  toFilter(item, field, name, isDistribution) {
3565
3565
  if (isDistribution) {
3566
- const res = item.value.match(/.*:\(?([><=\d\-.AND ]+)\)?/);
3567
- if (res?.[1]) {
3568
- const expr = res?.[1].split(" AND ");
3569
- const filters = expr.map(e => {
3570
- const operator = e.indexOf(">=") !== -1 ? "gte" : "lt";
3571
- const value = e.substring(e.indexOf(" ") + 1);
3572
- return { name, field, operator, value, display: item.display || item.value };
3573
- });
3574
- if (filters.length === 2) {
3575
- return {
3576
- operator: "and",
3577
- filters,
3578
- display: filters[0].display || filters[0].value,
3579
- field,
3580
- name
3581
- };
3582
- }
3583
- else if (filters.length === 1) {
3584
- return { ...filters[0], field, name };
3585
- }
3586
- throw new Error("Failed to parse distribution expression");
3566
+ // Parsing lives in @sinequa/atomic — `filter.distribution` is the single implementation, and it
3567
+ // recognises all four bounds where this method only ever produced `gte` or `lt`, reading `>` as
3568
+ // `gte` and `<=` as `lt`. What is added here is what the store needs and a `Filter` has no room
3569
+ // for: the aggregation's `name` on every leaf, which is what makes `updateFilter` replace the
3570
+ // entry rather than merge into it, and the human-readable `display`.
3571
+ const parsed = filter.distribution(field, item.value);
3572
+ if (parsed) {
3573
+ return this.nameLeaves(parsed, field, name, item.display || String(item.value ?? ""));
3587
3574
  }
3575
+ throw new Error("Failed to parse distribution expression");
3588
3576
  }
3589
3577
  if (typeof item.value === "string") {
3590
3578
  return { name, field, value: item.value, operator: "contains", display: item.display };
3591
3579
  }
3592
3580
  return { name, field, value: item.value, display: item.display };
3593
3581
  }
3582
+ /**
3583
+ * Stamps the aggregation's `name` and the item's `display` onto a filter parsed by
3584
+ * `filter.distribution`.
3585
+ *
3586
+ * A `Filter` carries neither, and the store needs both: `name` is what
3587
+ * `QueryParamsStore.updateFilter` matches on to **replace** an aggregation's entry instead of merging
3588
+ * into it — the only way a selection can shrink — and `display` is the label the applied-filter chips
3589
+ * render. A combined filter also keeps `field` at its root, which is how the store finds the entry.
3590
+ */
3591
+ nameLeaves(parsed, field, name, display) {
3592
+ const leaves = parsed.filters;
3593
+ if (leaves?.length) {
3594
+ return {
3595
+ ...parsed,
3596
+ field,
3597
+ name,
3598
+ display,
3599
+ filters: leaves.map(leaf => ({ ...leaf, field, name, display }))
3600
+ };
3601
+ }
3602
+ return { ...parsed, field, name, display };
3603
+ }
3594
3604
  findNode(items, path = "") {
3595
3605
  if (!items || items.length === 0) {
3596
3606
  return undefined;
@@ -3638,27 +3648,6 @@ class AggregationsService {
3638
3648
  }
3639
3649
  return node;
3640
3650
  }
3641
- flattenFilters(filters) {
3642
- const flattenedValues = [];
3643
- function extractValues(filters) {
3644
- for (const filter of filters) {
3645
- if (filter.value) {
3646
- flattenedValues.push(filter.value);
3647
- if (filter.display)
3648
- flattenedValues.push(filter.display);
3649
- // we handle "values" field for trees
3650
- }
3651
- else if (filter.values) {
3652
- flattenedValues.push(...filter.values);
3653
- }
3654
- if (filter.filters) {
3655
- extractValues(filter.filters);
3656
- }
3657
- }
3658
- }
3659
- extractValues(filters);
3660
- return flattenedValues;
3661
- }
3662
3651
  /**
3663
3652
  * Recursively opens parent nodes in a tree structure if any of their child nodes are selected or opened.
3664
3653
  *
@@ -5397,6 +5386,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
5397
5386
 
5398
5387
  class UserProfileService {
5399
5388
  API_URL = `${globalConfig.backendUrl}/api/v2/user-profile`;
5389
+ // Keyed by the userId signal's identity rather than its value: every current caller
5390
+ // passes the same PrincipalStore.userId signal, so sharing the resource here collapses
5391
+ // what would otherwise be one httpResource (and one HTTP request) per caller into one
5392
+ // for the whole app -- and lets a `.set()` from one consumer (e.g. the profile edit
5393
+ // form) show up everywhere else that reads this resource.
5394
+ resources = new WeakMap();
5400
5395
  /**
5401
5396
  * Retrieves the user profile by user ID.
5402
5397
  *
@@ -5404,11 +5399,16 @@ class UserProfileService {
5404
5399
  * @returns An Observable that emits an the UserProfile object.
5405
5400
  */
5406
5401
  getUserProfile(userId) {
5407
- return httpResource(() => (userId() ? `${this.API_URL}/${userId()}` : undefined), {
5408
- parse: (response) => {
5409
- return response;
5410
- }
5411
- });
5402
+ let resource = this.resources.get(userId);
5403
+ if (!resource) {
5404
+ resource = httpResource(() => (userId() ? `${this.API_URL}/${userId()}` : undefined), {
5405
+ parse: response => {
5406
+ return response;
5407
+ }
5408
+ });
5409
+ this.resources.set(userId, resource);
5410
+ }
5411
+ return resource;
5412
5412
  }
5413
5413
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: UserProfileService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
5414
5414
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: UserProfileService, providedIn: "root" });
@@ -9475,41 +9475,57 @@ function injectAggregationBase(refs) {
9475
9475
  }
9476
9476
  function addCurrentFiltersToItems(aggregation) {
9477
9477
  const currentItems = aggregation?.items || [];
9478
- if (!aggregation?.isTree && !aggregation?.isDistribution) {
9479
- const currentFilters = isControlled() ? selectionAsFilter() : queryParamsStore.getFilter({ field: aggregation?.column, name: aggregation?.name });
9480
- if (currentFilters) {
9481
- if (Array.isArray(currentFilters.filters) && currentFilters.filters.length) {
9482
- currentFilters.filters.forEach((filter) => {
9483
- const found = currentItems.find(item => item.value?.toString().toLocaleLowerCase() === filter.value?.toLocaleLowerCase());
9484
- if (!found) {
9485
- currentItems.unshift({ value: filter.value, display: filter.display, $selected: true });
9486
- }
9487
- else {
9488
- found.$selected = true;
9489
- }
9490
- });
9478
+ if (aggregation?.isTree) {
9479
+ return currentItems;
9480
+ }
9481
+ const currentFilters = isControlled() ? selectionAsFilter() : queryParamsStore.getFilter({ field: aggregation?.column, name: aggregation?.name });
9482
+ if (aggregation?.isDistribution) {
9483
+ // A distribution item's `value` is the full backend expression (e.g.
9484
+ // `size`10 Kb to 100 Kb`:(>= 10240 AND < 102400)`), not comparable to what the applied
9485
+ // filter stores — its leaves only ever carry the bucket's numeric thresholds. Flatten the
9486
+ // filter and compare on `display` instead (or `value` when `valuesAreExpressions` is not
9487
+ // set) — same strategy as `AggregationsService.enrichAggregation`.
9488
+ const flattenedValues = currentFilters ? flattenLegacyFilterValues([currentFilters]) : [];
9489
+ currentItems.forEach(item => {
9490
+ const valueToSearch = aggregation.valuesAreExpressions ? item.display : item.value;
9491
+ if (flattenedValues.includes(valueToSearch?.toString() ?? "")) {
9492
+ item.$selected = true;
9491
9493
  }
9492
- else if (Array.isArray(currentFilters.values) && currentFilters.values.length) {
9493
- // multiple values stored as a string array, e.g. { values: ["alice_martin", "caroline_dubois"] }
9494
- // (no `filters` sub-array and no single `value`) — mark each matching item as selected
9495
- currentFilters.values.forEach(value => {
9496
- const found = currentItems.find(item => item.value?.toString().toLocaleLowerCase() === value?.toString().toLocaleLowerCase());
9497
- if (!found) {
9498
- currentItems.unshift({ value, display: value, $selected: true });
9499
- }
9500
- else {
9501
- found.$selected = true;
9502
- }
9503
- });
9504
- }
9505
- else if (currentFilters.value) {
9506
- const found = currentItems.find(item => item.value?.toString().toLocaleLowerCase() === currentFilters.value?.toLocaleLowerCase());
9494
+ });
9495
+ return currentItems;
9496
+ }
9497
+ if (currentFilters) {
9498
+ if (Array.isArray(currentFilters.filters) && currentFilters.filters.length) {
9499
+ currentFilters.filters.forEach((filter) => {
9500
+ const found = currentItems.find(item => item.value?.toString().toLocaleLowerCase() === filter.value?.toLocaleLowerCase());
9507
9501
  if (!found) {
9508
- currentItems.push({ value: currentFilters.value, display: currentFilters.display, $selected: true });
9502
+ currentItems.unshift({ value: filter.value, display: filter.display, $selected: true });
9509
9503
  }
9510
9504
  else {
9511
9505
  found.$selected = true;
9512
9506
  }
9507
+ });
9508
+ }
9509
+ else if (Array.isArray(currentFilters.values) && currentFilters.values.length) {
9510
+ // multiple values stored as a string array, e.g. { values: ["alice_martin", "caroline_dubois"] }
9511
+ // (no `filters` sub-array and no single `value`) — mark each matching item as selected
9512
+ currentFilters.values.forEach(value => {
9513
+ const found = currentItems.find(item => item.value?.toString().toLocaleLowerCase() === value?.toString().toLocaleLowerCase());
9514
+ if (!found) {
9515
+ currentItems.unshift({ value, display: value, $selected: true });
9516
+ }
9517
+ else {
9518
+ found.$selected = true;
9519
+ }
9520
+ });
9521
+ }
9522
+ else if (currentFilters.value) {
9523
+ const found = currentItems.find(item => item.value?.toString().toLocaleLowerCase() === currentFilters.value?.toLocaleLowerCase());
9524
+ if (!found) {
9525
+ currentItems.push({ value: currentFilters.value, display: currentFilters.display, $selected: true });
9526
+ }
9527
+ else {
9528
+ found.$selected = true;
9513
9529
  }
9514
9530
  }
9515
9531
  }
@@ -15598,6 +15614,15 @@ class AggregationListComponent {
15598
15614
  const filters = this.queryParamsStore.getFilter({ field: field ?? undefined, name: this.listItemName(item) });
15599
15615
  if (!filters)
15600
15616
  return false;
15617
+ const agg = this.aggregation();
15618
+ if (agg?.isDistribution) {
15619
+ // Same reasoning as `injectAggregationBase.addCurrentFiltersToItems`: a distribution item's
15620
+ // `value` is the full backend expression, not comparable to the applied filter's stored
15621
+ // thresholds — flatten it and compare on `display` (or `value` without `valuesAreExpressions`).
15622
+ const flattenedValues = flattenLegacyFilterValues([filters]);
15623
+ const valueToSearch = agg.valuesAreExpressions ? item.display : item.value;
15624
+ return flattenedValues.includes(valueToSearch?.toString() ?? "");
15625
+ }
15601
15626
  const values = [item.value];
15602
15627
  return values.some(v => v === filters.value) || !!(filters.values?.length && filters.values.some((value) => values.some(v => v === value)));
15603
15628
  }