@sinequa/atomic-angular 1.19.2 → 1.19.3

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, 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';
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, Audit, isObject, 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';
@@ -4404,116 +4404,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
4404
4404
  }]
4405
4405
  }] });
4406
4406
 
4407
- class JsonMethodPluginService {
4408
- http = inject(HttpClient);
4409
- API_URL = `${globalConfig.backendUrl}/api/v1`;
4410
- /**
4411
- * Call a JsonMethod plugin using an HTTP POST
4412
- *
4413
- * @param method The name of the JsonMethod plugin
4414
- * @param query Parameters to pass to the plugin
4415
- * @param options HTTP options for the request
4416
- * @returns An observable of the plugin's return value
4417
- */
4418
- post(method, query, options) {
4419
- if (!isObject(query)) {
4420
- return throwError(() => ({ error: 'invalid query object' }));
4421
- }
4422
- return this.http.post(`${this.API_URL}/plugin/${method}`, query, options);
4423
- }
4424
- /**
4425
- * Call a JsonMethod plugin using an HTTP GET
4426
- *
4427
- * @param method The name of the JsonMethod plugin
4428
- * @param query Parameters to pass to the plugin
4429
- * @param options HTTP options for the request
4430
- * @returns An observable of the plugin's return value
4431
- */
4432
- get(method, query, options) {
4433
- return this.http.get(`${this.API_URL}/plugin/${method}`, {
4434
- params: query,
4435
- ...options
4436
- });
4437
- }
4438
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: JsonMethodPluginService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4439
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: JsonMethodPluginService, providedIn: 'root' });
4440
- }
4441
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: JsonMethodPluginService, decorators: [{
4442
- type: Injectable,
4443
- args: [{
4444
- providedIn: 'root'
4445
- }]
4446
- }] });
4447
-
4448
- /**
4449
- * The route an audit event names: the path, without its leading slash, query string or fragment.
4450
- * `/search?q=irsn#p2` and `/search` are the same route — arriving on one from the other is a new
4451
- * query, not a navigation.
4452
- */
4453
- function routeNameOf(url) {
4454
- return url.slice(1).split(/[?#]/)[0];
4455
- }
4456
- class NavigationService {
4457
- router = inject(Router);
4458
- auditService = inject(AuditService);
4459
- // The route of the previous navigation, used only to dedupe audit route-change notifications in
4460
- // navigationEnd$ below. Kept separate from urlAfterNavigation so that getter stays correct
4461
- // regardless of whether anything subscribes to navigationEnd$.
4462
- //
4463
- // It used to hold the full url (`/search?q=irsn`) while the comparison was against the bare route
4464
- // name (`search`): the two could never be equal, so the dedupe never fired and every navigation
4465
- // was audited — including the query-param changes that make up most of a search session.
4466
- previousRouteName = null;
4467
- /**
4468
- * The URL of the current navigation, used to redirect the user after login.
4469
- *
4470
- * Sourced directly from `Router.url`, which Angular keeps up to date on every navigation with no
4471
- * subscription required — unlike a field updated as a side effect of `navigationEnd$` below,
4472
- * which only happens for whoever actually subscribes to it.
4473
- *
4474
- * The authentication screens are never returned: sending someone back to `/logout` after they
4475
- * signed in defeats the sign-in, and back to `/login` loops. The exclusion used to be a literal
4476
- * `!== "/login"`, which let through `/logout`, `/login?mode=changepassword` — same screen, one
4477
- * query param — and any application whose `loginPath` is not spelled `/login`.
4478
- */
4479
- get urlAfterNavigation() {
4480
- const url = this.router.url;
4481
- return isAuthRoute(url, globalConfig.loginPath) ? null : url;
4482
- }
4483
- /**
4484
- * Observable that emits events of type `NavigationEnd` from the Angular Router.
4485
- *
4486
- * This observable performs the following operations:
4487
- * - Maps all router events to `RouterEvent`.
4488
- * - Filters the events to only include instances of `NavigationEnd`.
4489
- * - Taps into the event stream to notify the audit service when the route itself changes — a new
4490
- * query, a filter or another page of results stays on the same route and is not one.
4491
- * - Shares the replayed value with a buffer size of 1 to ensure subscribers receive the latest emitted value.
4492
- *
4493
- * @type Observable<RouterEvent>
4494
- */
4495
- navigationEnd$ = this.router.events.pipe(map(event => event), filter$1((event) => event instanceof NavigationEnd), tap(event => {
4496
- const routeName = routeNameOf(event.url);
4497
- // The empty name is the application root, which has never been audited.
4498
- if (routeName && routeName !== this.previousRouteName) {
4499
- this.auditService.notifyRouteChange(routeName);
4500
- }
4501
- // Every route is remembered, the authentication screens included. Skipping them would swallow
4502
- // the return: leaving `/search` for `/login` and coming back is two route changes, and the
4503
- // second one would compare against a `search` the user has meanwhile left. (The exclusion that
4504
- // used to sit here never ran — the dedupe above it could not fire.)
4505
- this.previousRouteName = routeName;
4506
- }), shareReplay(1));
4507
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: NavigationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4508
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: NavigationService, providedIn: "root" });
4509
- }
4510
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: NavigationService, decorators: [{
4511
- type: Injectable,
4512
- args: [{
4513
- providedIn: "root"
4514
- }]
4515
- }] });
4516
-
4517
4407
  // pre-configured highlights's colors
4518
4408
  const HIGHLIGHTS = new InjectionToken('highlights', {
4519
4409
  factory: () => [
@@ -4799,17 +4689,45 @@ class PreviewService {
4799
4689
  * @returns the html id
4800
4690
  */
4801
4691
  getEntityId(entity, value, index) {
4802
- if (!this.previewData)
4803
- return undefined;
4804
- // Combine filters for better performance.
4805
- // NB: positionInCategories[entity] is a zero-based index, so the first occurrence is 0.
4806
- // Use a presence check (not truthiness) to avoid dropping that first occurrence.
4807
- const entitySearched = this.previewData.highlightsPerLocation
4808
- .filter(item => item.positionInCategories[entity] !== undefined && item.values.some(v => v === value))
4809
- .map(e => ({ id: e.positionInCategories[entity] }));
4810
- const id = entitySearched[index]?.id;
4811
- return id;
4692
+ return this.getEntityLocations(entity, value)[index];
4693
+ }
4694
+ /**
4695
+ * Get the ids of every occurrence of an entity value that is actually highlighted in the preview,
4696
+ * ordered by position in the document.
4697
+ *
4698
+ * This is the source of truth for entity navigation: the `count` carried by the indexed record can be
4699
+ * greater than the number of occurrences located in the converted document (ES-32245).
4700
+ *
4701
+ * @param entity the entity type (i.e. "geo")
4702
+ * @param value the entity value (i.e. "AUSTRALIA")
4703
+ * @returns the html ids (`positionInCategories[entity]`), empty when nothing is highlighted
4704
+ */
4705
+ getEntityLocations(entity, value) {
4706
+ const data = this.previewData;
4707
+ if (!data)
4708
+ return [];
4709
+ let perEntity = this.entityLocationsCache.get(data);
4710
+ if (!perEntity) {
4711
+ perEntity = new Map();
4712
+ this.entityLocationsCache.set(data, perEntity);
4713
+ }
4714
+ const key = `${entity}\u0000${value}`;
4715
+ let ids = perEntity.get(key);
4716
+ if (!ids) {
4717
+ ids = [];
4718
+ for (const item of data.highlightsPerLocation) {
4719
+ // NB: positionInCategories[entity] is a zero-based index, so the first occurrence is 0.
4720
+ // Use a presence check (not truthiness) to avoid dropping that first occurrence.
4721
+ const position = item.positionInCategories[entity];
4722
+ if (position !== undefined && item.values.some(v => v === value))
4723
+ ids.push(position);
4724
+ }
4725
+ perEntity.set(key, ids);
4726
+ }
4727
+ return ids;
4812
4728
  }
4729
+ /** Memoized `getEntityLocations` results, dropped with the `PreviewData` they were computed from. */
4730
+ entityLocationsCache = new WeakMap();
4813
4731
  /**
4814
4732
  * Send a message to the prewiew iFrame with the required data to retrieve HTML content for a specific highlight category
4815
4733
  *
@@ -4973,6 +4891,171 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
4973
4891
  }]
4974
4892
  }], ctorParameters: () => [{ type: i0.DestroyRef }] });
4975
4893
 
4894
+ /**
4895
+ * Cursor over the occurrences of an entity value highlighted in the preview.
4896
+ *
4897
+ * The number of occurrences is the one actually located in the preview (`PreviewService.getEntityLocations`),
4898
+ * not the `count` of the indexed record, which can be greater (ES-32245).
4899
+ */
4900
+ class EntityNavigationService {
4901
+ previewService = inject(PreviewService);
4902
+ lastPreviewData;
4903
+ /** The current position, shared by the entities panels */
4904
+ navigation = signal(undefined, ...(ngDevMode ? [{ debugName: "navigation" }] : []));
4905
+ /** The value hovered in the panel */
4906
+ hovering = signal(undefined, ...(ngDevMode ? [{ debugName: "hovering" }] : []));
4907
+ /** The 1-based position of the hovered value, `0` when it is not the one being navigated */
4908
+ hoverIndex = computed(() => (this.navigation()?.value === this.hovering() ? (this.navigation()?.index ?? 0) : 0), ...(ngDevMode ? [{ debugName: "hoverIndex" }] : []));
4909
+ /**
4910
+ * Number of occurrences of `value` highlighted in the preview.
4911
+ * `fallback` (the indexed count) is only used while no preview is loaded.
4912
+ */
4913
+ total(entity, value, fallback = 0) {
4914
+ if (!this.previewService.previewData)
4915
+ return fallback;
4916
+ return this.previewService.getEntityLocations(entity, value).length;
4917
+ }
4918
+ navigate(entity, value, direction) {
4919
+ const ids = this.previewService.getEntityLocations(entity, value);
4920
+ const total = ids.length;
4921
+ if (total === 0) {
4922
+ warn(`No highlighted occurrence for entity ${entity}:`, value);
4923
+ notify.warning(`No occurrence of "${value}" can be reached in the document.`);
4924
+ return;
4925
+ }
4926
+ // The cursor belongs to one document: start over when another preview has been loaded
4927
+ if (this.lastPreviewData !== this.previewService.previewData) {
4928
+ this.lastPreviewData = this.previewService.previewData;
4929
+ this.navigation.set(undefined);
4930
+ }
4931
+ const current = this.navigation();
4932
+ // Start from the first (next) or last (prev) occurrence when the value changes
4933
+ const position = current?.value === value ? current.index : direction === "next" ? 0 : 1;
4934
+ const index = ((position - 1 + (direction === "next" ? 1 : -1) + total) % total) + 1;
4935
+ this.navigation.set({ value, index });
4936
+ const id = ids[index - 1];
4937
+ this.previewService.events.set("scrollTo");
4938
+ this.previewService.sendMessage({ action: "select", id: `${entity}_${id}`, usePassageHighlighter: false });
4939
+ debug(`${direction} navigation:`, { id, entity, value, index, total });
4940
+ }
4941
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: EntityNavigationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4942
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: EntityNavigationService, providedIn: "root" });
4943
+ }
4944
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: EntityNavigationService, decorators: [{
4945
+ type: Injectable,
4946
+ args: [{ providedIn: "root" }]
4947
+ }] });
4948
+
4949
+ class JsonMethodPluginService {
4950
+ http = inject(HttpClient);
4951
+ API_URL = `${globalConfig.backendUrl}/api/v1`;
4952
+ /**
4953
+ * Call a JsonMethod plugin using an HTTP POST
4954
+ *
4955
+ * @param method The name of the JsonMethod plugin
4956
+ * @param query Parameters to pass to the plugin
4957
+ * @param options HTTP options for the request
4958
+ * @returns An observable of the plugin's return value
4959
+ */
4960
+ post(method, query, options) {
4961
+ if (!isObject(query)) {
4962
+ return throwError(() => ({ error: 'invalid query object' }));
4963
+ }
4964
+ return this.http.post(`${this.API_URL}/plugin/${method}`, query, options);
4965
+ }
4966
+ /**
4967
+ * Call a JsonMethod plugin using an HTTP GET
4968
+ *
4969
+ * @param method The name of the JsonMethod plugin
4970
+ * @param query Parameters to pass to the plugin
4971
+ * @param options HTTP options for the request
4972
+ * @returns An observable of the plugin's return value
4973
+ */
4974
+ get(method, query, options) {
4975
+ return this.http.get(`${this.API_URL}/plugin/${method}`, {
4976
+ params: query,
4977
+ ...options
4978
+ });
4979
+ }
4980
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: JsonMethodPluginService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4981
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: JsonMethodPluginService, providedIn: 'root' });
4982
+ }
4983
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: JsonMethodPluginService, decorators: [{
4984
+ type: Injectable,
4985
+ args: [{
4986
+ providedIn: 'root'
4987
+ }]
4988
+ }] });
4989
+
4990
+ /**
4991
+ * The route an audit event names: the path, without its leading slash, query string or fragment.
4992
+ * `/search?q=irsn#p2` and `/search` are the same route — arriving on one from the other is a new
4993
+ * query, not a navigation.
4994
+ */
4995
+ function routeNameOf(url) {
4996
+ return url.slice(1).split(/[?#]/)[0];
4997
+ }
4998
+ class NavigationService {
4999
+ router = inject(Router);
5000
+ auditService = inject(AuditService);
5001
+ // The route of the previous navigation, used only to dedupe audit route-change notifications in
5002
+ // navigationEnd$ below. Kept separate from urlAfterNavigation so that getter stays correct
5003
+ // regardless of whether anything subscribes to navigationEnd$.
5004
+ //
5005
+ // It used to hold the full url (`/search?q=irsn`) while the comparison was against the bare route
5006
+ // name (`search`): the two could never be equal, so the dedupe never fired and every navigation
5007
+ // was audited — including the query-param changes that make up most of a search session.
5008
+ previousRouteName = null;
5009
+ /**
5010
+ * The URL of the current navigation, used to redirect the user after login.
5011
+ *
5012
+ * Sourced directly from `Router.url`, which Angular keeps up to date on every navigation with no
5013
+ * subscription required — unlike a field updated as a side effect of `navigationEnd$` below,
5014
+ * which only happens for whoever actually subscribes to it.
5015
+ *
5016
+ * The authentication screens are never returned: sending someone back to `/logout` after they
5017
+ * signed in defeats the sign-in, and back to `/login` loops. The exclusion used to be a literal
5018
+ * `!== "/login"`, which let through `/logout`, `/login?mode=changepassword` — same screen, one
5019
+ * query param — and any application whose `loginPath` is not spelled `/login`.
5020
+ */
5021
+ get urlAfterNavigation() {
5022
+ const url = this.router.url;
5023
+ return isAuthRoute(url, globalConfig.loginPath) ? null : url;
5024
+ }
5025
+ /**
5026
+ * Observable that emits events of type `NavigationEnd` from the Angular Router.
5027
+ *
5028
+ * This observable performs the following operations:
5029
+ * - Maps all router events to `RouterEvent`.
5030
+ * - Filters the events to only include instances of `NavigationEnd`.
5031
+ * - Taps into the event stream to notify the audit service when the route itself changes — a new
5032
+ * query, a filter or another page of results stays on the same route and is not one.
5033
+ * - Shares the replayed value with a buffer size of 1 to ensure subscribers receive the latest emitted value.
5034
+ *
5035
+ * @type Observable<RouterEvent>
5036
+ */
5037
+ navigationEnd$ = this.router.events.pipe(map(event => event), filter$1((event) => event instanceof NavigationEnd), tap(event => {
5038
+ const routeName = routeNameOf(event.url);
5039
+ // The empty name is the application root, which has never been audited.
5040
+ if (routeName && routeName !== this.previousRouteName) {
5041
+ this.auditService.notifyRouteChange(routeName);
5042
+ }
5043
+ // Every route is remembered, the authentication screens included. Skipping them would swallow
5044
+ // the return: leaving `/search` for `/login` and coming back is two route changes, and the
5045
+ // second one would compare against a `search` the user has meanwhile left. (The exclusion that
5046
+ // used to sit here never ran — the dedupe above it could not fire.)
5047
+ this.previousRouteName = routeName;
5048
+ }), shareReplay(1));
5049
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: NavigationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
5050
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: NavigationService, providedIn: "root" });
5051
+ }
5052
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: NavigationService, decorators: [{
5053
+ type: Injectable,
5054
+ args: [{
5055
+ providedIn: "root"
5056
+ }]
5057
+ }] });
5058
+
4976
5059
  /**
4977
5060
  * @deprecated This service is deprecated and should not be used directly. Please use the PrincipalStore instead.
4978
5061
  */
@@ -10281,12 +10364,13 @@ class ArticleEntities {
10281
10364
  cn = cn;
10282
10365
  appStore = inject(AppStore);
10283
10366
  previewService = inject(PreviewService);
10367
+ entityNavigation = inject(EntityNavigationService);
10284
10368
  article = input.required(...(ngDevMode ? [{ debugName: "article" }] : []));
10285
10369
  previewHighlights = computed(() => {
10286
10370
  const highlights = this.appStore.getWebServiceByType("preview")?.highlights
10287
10371
  ?.split(",")
10288
- .filter((h) => h !== "extractslocations" && h !== "matchlocations" && h !== "matchingpassages");
10289
- return highlights?.map((highlight) => ({
10372
+ .filter(h => h !== "extractslocations" && h !== "matchlocations" && h !== "matchingpassages");
10373
+ return highlights?.map(highlight => ({
10290
10374
  name: highlight,
10291
10375
  entity: highlight,
10292
10376
  metadata: this.getMetadata(this.article(), highlight)
@@ -10296,54 +10380,24 @@ class ArticleEntities {
10296
10380
  const m = article[entity];
10297
10381
  return m ?? [];
10298
10382
  }
10299
- navigation = signal(undefined, ...(ngDevMode ? [{ debugName: "navigation" }] : []));
10300
- hovering = signal(undefined, ...(ngDevMode ? [{ debugName: "hovering" }] : []));
10301
- hoverIndex = computed(() => this.navigation()?.value === this.hovering() ? (this.navigation()?.index ?? 0) : 0, ...(ngDevMode ? [{ debugName: "hoverIndex" }] : []));
10383
+ navigation = this.entityNavigation.navigation;
10384
+ hovering = this.entityNavigation.hovering;
10385
+ hoverIndex = this.entityNavigation.hoverIndex;
10386
+ /** Number of occurrences of the entity value highlighted in the preview (not the indexed `count`, see ES-32245) */
10387
+ total(entity, data) {
10388
+ return this.entityNavigation.total(entity, data.value, data.count);
10389
+ }
10302
10390
  scrollTo(type, index, usePassageHighlighter = false) {
10303
10391
  // if previewData contains "matchingpassages" then we use the passage highlighter otherwise we use "extractslocations"
10304
10392
  const category = type || this.previewService.highlightCategory;
10305
10393
  this.previewService.events.set("scrollTo");
10306
10394
  this.previewService.sendMessage({ action: "select", id: `${category}_${index}`, usePassageHighlighter });
10307
10395
  }
10308
- navigate(entity, data, direction) {
10309
- if (!data.count || data.count <= 0) {
10310
- warn(`Invalid count for entity ${entity}:`, data.count);
10311
- return;
10312
- }
10313
- const currentNavigation = this.navigation();
10314
- const isCurrentValue = currentNavigation?.value === data.value;
10315
- let newIndex;
10316
- if (direction === "next") {
10317
- newIndex = isCurrentValue && currentNavigation.index < data.count ? currentNavigation.index + 1 : 1;
10318
- }
10319
- else {
10320
- newIndex = isCurrentValue && currentNavigation.index > 1 ? currentNavigation.index - 1 : data.count;
10321
- }
10322
- this.navigation.set({
10323
- value: data.value,
10324
- index: newIndex
10325
- });
10326
- const id = this.previewService.getEntityId(entity, data.value, newIndex - 1);
10327
- if (id !== undefined) {
10328
- this.scrollTo(entity, id);
10329
- debug(`${direction} navigation:`, {
10330
- id,
10331
- entity,
10332
- value: data.value,
10333
- index: newIndex,
10334
- totalCount: data.count
10335
- });
10336
- }
10337
- else {
10338
- warn(`Unable to get ID for ${direction} navigation:`, { entity, value: data.value, index: newIndex });
10339
- notify.warning(`Unable to navigate to the ${direction} occurrence of "${data.value}".`);
10340
- }
10341
- }
10342
10396
  navigateNext(entity, data) {
10343
- this.navigate(entity, data, "next");
10397
+ this.entityNavigation.navigate(entity, data.value, "next");
10344
10398
  }
10345
10399
  navigatePrev(entity, data) {
10346
- this.navigate(entity, data, "prev");
10400
+ this.entityNavigation.navigate(entity, data.value, "prev");
10347
10401
  }
10348
10402
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: ArticleEntities, deps: [], target: i0.ɵɵFactoryTarget.Component });
10349
10403
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.29", type: ArticleEntities, isStandalone: true, selector: "article-entities,ArticleEntities,articleentities", inputs: { article: { classPropertyName: "article", publicName: "article", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
@@ -10387,13 +10441,13 @@ class ArticleEntities {
10387
10441
  <chevronleft />
10388
10442
  </button>
10389
10443
 
10390
- <span>{{ hoverIndex() }} / {{ data.count }}</span>
10444
+ <span>{{ hoverIndex() }} / {{ total(entity, data) }}</span>
10391
10445
 
10392
10446
  <button class="hover:scale-125" aria-label="next entity" role="button" (click)="navigateNext(entity, data)">
10393
10447
  <chevronright />
10394
10448
  </button>
10395
10449
  } @else {
10396
- {{ data.count }}
10450
+ {{ total(entity, data) }}
10397
10451
  }
10398
10452
  </p>
10399
10453
  </li>
@@ -10449,13 +10503,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
10449
10503
  <chevronleft />
10450
10504
  </button>
10451
10505
 
10452
- <span>{{ hoverIndex() }} / {{ data.count }}</span>
10506
+ <span>{{ hoverIndex() }} / {{ total(entity, data) }}</span>
10453
10507
 
10454
10508
  <button class="hover:scale-125" aria-label="next entity" role="button" (click)="navigateNext(entity, data)">
10455
10509
  <chevronright />
10456
10510
  </button>
10457
10511
  } @else {
10458
- {{ data.count }}
10512
+ {{ total(entity, data) }}
10459
10513
  }
10460
10514
  </p>
10461
10515
  </li>
@@ -13654,12 +13708,13 @@ class EntitiesPanel {
13654
13708
  cn = cn;
13655
13709
  appStore = inject(AppStore);
13656
13710
  previewService = inject(PreviewService);
13711
+ entityNavigation = inject(EntityNavigationService);
13657
13712
  article = input.required(...(ngDevMode ? [{ debugName: "article" }] : []));
13658
13713
  previewHighlights = computed(() => {
13659
13714
  const highlights = this.appStore.getWebServiceByType("preview")?.highlights
13660
13715
  ?.split(",")
13661
- .filter((h) => h !== "extractslocations" && h !== "matchlocations" && h !== "matchingpassages");
13662
- return highlights?.map((highlight) => ({
13716
+ .filter(h => h !== "extractslocations" && h !== "matchlocations" && h !== "matchingpassages");
13717
+ return highlights?.map(highlight => ({
13663
13718
  name: highlight,
13664
13719
  entity: highlight,
13665
13720
  metadata: this.getMetadata(this.article(), highlight)
@@ -13669,54 +13724,24 @@ class EntitiesPanel {
13669
13724
  const m = article[entity];
13670
13725
  return m ?? [];
13671
13726
  }
13672
- navigation = signal(undefined, ...(ngDevMode ? [{ debugName: "navigation" }] : []));
13673
- hovering = signal(undefined, ...(ngDevMode ? [{ debugName: "hovering" }] : []));
13674
- hoverIndex = computed(() => this.navigation()?.value === this.hovering() ? (this.navigation()?.index ?? 0) : 0, ...(ngDevMode ? [{ debugName: "hoverIndex" }] : []));
13727
+ navigation = this.entityNavigation.navigation;
13728
+ hovering = this.entityNavigation.hovering;
13729
+ hoverIndex = this.entityNavigation.hoverIndex;
13730
+ /** Number of occurrences of the entity value highlighted in the preview (not the indexed `count`, see ES-32245) */
13731
+ total(entity, data) {
13732
+ return this.entityNavigation.total(entity, data.value, data.count);
13733
+ }
13675
13734
  scrollTo(type, index, usePassageHighlighter = false) {
13676
13735
  // if previewData contains "matchingpassages" then we use the passage highlighter otherwise we use "extractslocations"
13677
13736
  const category = type || this.previewService.highlightCategory;
13678
13737
  this.previewService.events.set("scrollTo");
13679
13738
  this.previewService.sendMessage({ action: "select", id: `${category}_${index}`, usePassageHighlighter });
13680
13739
  }
13681
- navigate(entity, data, direction) {
13682
- if (!data.count || data.count <= 0) {
13683
- warn(`Invalid count for entity ${entity}:`, data.count);
13684
- return;
13685
- }
13686
- const currentNavigation = this.navigation();
13687
- const isCurrentValue = currentNavigation?.value === data.value;
13688
- let newIndex;
13689
- if (direction === "next") {
13690
- newIndex = isCurrentValue && currentNavigation.index < data.count ? currentNavigation.index + 1 : 1;
13691
- }
13692
- else {
13693
- newIndex = isCurrentValue && currentNavigation.index > 1 ? currentNavigation.index - 1 : data.count;
13694
- }
13695
- this.navigation.set({
13696
- value: data.value,
13697
- index: newIndex
13698
- });
13699
- const id = this.previewService.getEntityId(entity, data.value, newIndex - 1);
13700
- if (id !== undefined) {
13701
- this.scrollTo(entity, id);
13702
- debug(`${direction} navigation:`, {
13703
- id,
13704
- entity,
13705
- value: data.value,
13706
- index: newIndex,
13707
- totalCount: data.count
13708
- });
13709
- }
13710
- else {
13711
- warn(`Unable to get ID for ${direction} navigation:`, { entity, value: data.value, index: newIndex });
13712
- notify.warning(`Unable to navigate to the ${direction} occurrence of "${data.value}".`);
13713
- }
13714
- }
13715
13740
  navigateNext(entity, data) {
13716
- this.navigate(entity, data, "next");
13741
+ this.entityNavigation.navigate(entity, data.value, "next");
13717
13742
  }
13718
13743
  navigatePrev(entity, data) {
13719
- this.navigate(entity, data, "prev");
13744
+ this.entityNavigation.navigate(entity, data.value, "prev");
13720
13745
  }
13721
13746
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: EntitiesPanel, deps: [], target: i0.ɵɵFactoryTarget.Component });
13722
13747
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.29", type: EntitiesPanel, isStandalone: true, selector: "entities-panel,EntitiesPanel,entitiesPanel", inputs: { article: { classPropertyName: "article", publicName: "article", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
@@ -13760,13 +13785,13 @@ class EntitiesPanel {
13760
13785
  <chevronleft />
13761
13786
  </button>
13762
13787
 
13763
- <span>{{ hoverIndex() }} / {{ data.count }}</span>
13788
+ <span>{{ hoverIndex() }} / {{ total(entity, data) }}</span>
13764
13789
 
13765
13790
  <button class="hover:scale-125" aria-label="next entity" role="button" (click)="navigateNext(entity, data)">
13766
13791
  <chevronright />
13767
13792
  </button>
13768
13793
  } @else {
13769
- {{ data.count }}
13794
+ {{ total(entity, data) }}
13770
13795
  }
13771
13796
  </p>
13772
13797
  </li>
@@ -13822,13 +13847,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
13822
13847
  <chevronleft />
13823
13848
  </button>
13824
13849
 
13825
- <span>{{ hoverIndex() }} / {{ data.count }}</span>
13850
+ <span>{{ hoverIndex() }} / {{ total(entity, data) }}</span>
13826
13851
 
13827
13852
  <button class="hover:scale-125" aria-label="next entity" role="button" (click)="navigateNext(entity, data)">
13828
13853
  <chevronright />
13829
13854
  </button>
13830
13855
  } @else {
13831
- {{ data.count }}
13856
+ {{ total(entity, data) }}
13832
13857
  }
13833
13858
  </p>
13834
13859
  </li>
@@ -18796,5 +18821,5 @@ const queryNameResolver = () => {
18796
18821
  * Generated bundle index. Do not edit.
18797
18822
  */
18798
18823
 
18799
- export { AGGREGATIONS_NAMES, AGGREGATIONS_NAMES_PRESET_DEFAULT, APP_FEATURES, AdvancedFiltersComponent, AdvancedSearch, AdvancedSearchComponent, AggregationComponent, AggregationDateComponent, AggregationDateRangeDialogComponent, AggregationItemDirective, AggregationListComponent, AggregationPanelComponent, AggregationTreeComponent, AggregationsService, AggregationsStore, Alert, AlertDialog, AlertsComponent, AppService, AppStore, ApplicationService, ApplicationStore, ArticleEntities, ArticleExtracts, ArticleLabels, ArticleSimilarDocuments, AsideFiltersComponent, AuditFeedbackType, AuditService, AuthGuard, AuthPageComponent, AutocompleteService, BOOKMARKS_CONFIG, BOOKMARKS_OPTIONS, BackdropComponent, BackdropService, BookmarkButtonComponent, BookmarksComponent, COLLECTIONS_CONFIG, COLLECTIONS_OPTIONS, COMPONENTS_FOR_DOCUMENT_TYPE, ChangePasswordComponent, ChildMarkerDirective, CollectionsComponent, CollectionsDialog, DRAWER_COMPONENT, DRAWER_STACK_MAX_COUNT, DateComponent, DeleteCollectionDialog, DidYouMeanComponent, DocumentLocatorComponent, DrawerAdvancedFiltersComponent, DrawerComponent, DrawerNavbarComponent, DrawerPreviewComponent, DrawerService, DrawerStackComponent, DrawerStackService, DropdownInputComponent, DropdownListComponent, ErrorComponent, ExportDialog, ExportService, FILTERS_BREAKPOINT, FILTER_DATE_ALLOW_CUSTOM_RANGE, FeatureFlagsDialogComponent, FeedbackDialogComponent, FileSizePipe, FilterButtonComponent, FiltersBarComponent, HIGHLIGHTS, HighlightWordPipe, InfinityScrollDirective, InlineWorker, JsonMethodPluginService, KeyboardNavigatorDirective, LabelService, LabelsEditDialog, LoadingComponent, MetadataComponent, MissingTermsComponent, MoreButtonComponent, MoreComponent, MultiSelectLabelsComponent, MultiSelectionToolbarComponent, NON_SEARCHABLE_COLUMNS, NON_SEARCHABLE_DEFAULTS, NavbarTabsComponent, NavigationService, NoResultComponent, OpenArticleOnCtrlEnterDirective, OperatorPipe, OverflowItemDirective, OverflowManagerDirective, OverflowStopDirective, OverrideUserDialogComponent, PREVIEW_CONFIG, PagerComponent, PreviewNavigator, PreviewService, PrincipalService, PrincipalStore, QueryParamsStore, QueryService, RECENT_SEARCHES_CONFIG, RECENT_SEARCHES_OPTIONS, ROUTE_COMPONENTS, RecentSearchesComponent, ResetUserSettingsDialogComponent, SAVED_SEARCHES_CONFIG, SAVED_SEARCHES_OPTIONS, SavedSearchDialog, SavedSearchesComponent, SavedSearchesService, SearchFeedbackComponent, SearchInputFooter, SearchService, SelectArticleDirective, SelectArticleOnClickDirective, SelectionHistoryService, SelectionService, SelectionStore, ShowBookmarkDirective, SidebarNavComponent, SignInComponent, SignedOutComponent, SortSelectorComponent, SourceComponent, SourceIconPipe, SponsoredResultsComponent, SyslangPipe, THEMES, TextChunkService, ThemeProviderDirective, ThemeSelectorComponent, ThemeStore, ThemeToggleComponent, TranslocoDateImpurePipe, UserProfileDialog, UserProfileFormComponent, UserProfileService, UserSettingsStore, __resetSignInMutexForTests, applyThemeToNativeElement, auditInterceptorFn, authInterceptorFn, bodyInterceptorFn, bootstrapApp, buildQuery, debouncedSignal, errorInterceptorFn, getCurrentPath, getCurrentQueryName, getQueryNameFromRoute, injectRouteNavigation, isAuthRoute, isSignInInFlight, legacyFilterToFilter, processCssVars, queryNameResolver, signIn, themeColorNameToCssVariable, themeColorsToCssVariables, toastInterceptorFn, withAggregationsFeatures, withAlertsFeatures, withAppFeatures, withApplicationFeatures, withAssistantFeatures, withBasketsFeatures, withBookmarkFeatures, withBootstrapApp, withExtractsFeatures, withFetch, withMultiSelectionFeatures, withQueryParamsFeatures, withRecentSearchesFeatures, withSavedSearchesFeatures, withSelectionFeatures, withThemeBodyHook, withThemes, withThemesFeatures, withUserSettingsFeatures };
18824
+ export { AGGREGATIONS_NAMES, AGGREGATIONS_NAMES_PRESET_DEFAULT, APP_FEATURES, AdvancedFiltersComponent, AdvancedSearch, AdvancedSearchComponent, AggregationComponent, AggregationDateComponent, AggregationDateRangeDialogComponent, AggregationItemDirective, AggregationListComponent, AggregationPanelComponent, AggregationTreeComponent, AggregationsService, AggregationsStore, Alert, AlertDialog, AlertsComponent, AppService, AppStore, ApplicationService, ApplicationStore, ArticleEntities, ArticleExtracts, ArticleLabels, ArticleSimilarDocuments, AsideFiltersComponent, AuditFeedbackType, AuditService, AuthGuard, AuthPageComponent, AutocompleteService, BOOKMARKS_CONFIG, BOOKMARKS_OPTIONS, BackdropComponent, BackdropService, BookmarkButtonComponent, BookmarksComponent, COLLECTIONS_CONFIG, COLLECTIONS_OPTIONS, COMPONENTS_FOR_DOCUMENT_TYPE, ChangePasswordComponent, ChildMarkerDirective, CollectionsComponent, CollectionsDialog, DRAWER_COMPONENT, DRAWER_STACK_MAX_COUNT, DateComponent, DeleteCollectionDialog, DidYouMeanComponent, DocumentLocatorComponent, DrawerAdvancedFiltersComponent, DrawerComponent, DrawerNavbarComponent, DrawerPreviewComponent, DrawerService, DrawerStackComponent, DrawerStackService, DropdownInputComponent, DropdownListComponent, EntityNavigationService, ErrorComponent, ExportDialog, ExportService, FILTERS_BREAKPOINT, FILTER_DATE_ALLOW_CUSTOM_RANGE, FeatureFlagsDialogComponent, FeedbackDialogComponent, FileSizePipe, FilterButtonComponent, FiltersBarComponent, HIGHLIGHTS, HighlightWordPipe, InfinityScrollDirective, InlineWorker, JsonMethodPluginService, KeyboardNavigatorDirective, LabelService, LabelsEditDialog, LoadingComponent, MetadataComponent, MissingTermsComponent, MoreButtonComponent, MoreComponent, MultiSelectLabelsComponent, MultiSelectionToolbarComponent, NON_SEARCHABLE_COLUMNS, NON_SEARCHABLE_DEFAULTS, NavbarTabsComponent, NavigationService, NoResultComponent, OpenArticleOnCtrlEnterDirective, OperatorPipe, OverflowItemDirective, OverflowManagerDirective, OverflowStopDirective, OverrideUserDialogComponent, PREVIEW_CONFIG, PagerComponent, PreviewNavigator, PreviewService, PrincipalService, PrincipalStore, QueryParamsStore, QueryService, RECENT_SEARCHES_CONFIG, RECENT_SEARCHES_OPTIONS, ROUTE_COMPONENTS, RecentSearchesComponent, ResetUserSettingsDialogComponent, SAVED_SEARCHES_CONFIG, SAVED_SEARCHES_OPTIONS, SavedSearchDialog, SavedSearchesComponent, SavedSearchesService, SearchFeedbackComponent, SearchInputFooter, SearchService, SelectArticleDirective, SelectArticleOnClickDirective, SelectionHistoryService, SelectionService, SelectionStore, ShowBookmarkDirective, SidebarNavComponent, SignInComponent, SignedOutComponent, SortSelectorComponent, SourceComponent, SourceIconPipe, SponsoredResultsComponent, SyslangPipe, THEMES, TextChunkService, ThemeProviderDirective, ThemeSelectorComponent, ThemeStore, ThemeToggleComponent, TranslocoDateImpurePipe, UserProfileDialog, UserProfileFormComponent, UserProfileService, UserSettingsStore, __resetSignInMutexForTests, applyThemeToNativeElement, auditInterceptorFn, authInterceptorFn, bodyInterceptorFn, bootstrapApp, buildQuery, debouncedSignal, errorInterceptorFn, getCurrentPath, getCurrentQueryName, getQueryNameFromRoute, injectRouteNavigation, isAuthRoute, isSignInInFlight, legacyFilterToFilter, processCssVars, queryNameResolver, signIn, themeColorNameToCssVariable, themeColorsToCssVariables, toastInterceptorFn, withAggregationsFeatures, withAlertsFeatures, withAppFeatures, withApplicationFeatures, withAssistantFeatures, withBasketsFeatures, withBookmarkFeatures, withBootstrapApp, withExtractsFeatures, withFetch, withMultiSelectionFeatures, withQueryParamsFeatures, withRecentSearchesFeatures, withSavedSearchesFeatures, withSelectionFeatures, withThemeBodyHook, withThemes, withThemesFeatures, withUserSettingsFeatures };
18800
18825
  //# sourceMappingURL=sinequa-atomic-angular.mjs.map