@sinequa/atomic-angular 0.0.146

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.
Files changed (174) hide show
  1. package/assets/tailwind.css +334 -0
  2. package/esm2022/lib/assistant/index.mjs +2 -0
  3. package/esm2022/lib/assistant/signalR.web.service.mjs +81 -0
  4. package/esm2022/lib/components/dropdown-input.mjs +97 -0
  5. package/esm2022/lib/components/dropdown-list.mjs +35 -0
  6. package/esm2022/lib/components/dropdown.mjs +127 -0
  7. package/esm2022/lib/components/index.mjs +7 -0
  8. package/esm2022/lib/components/menu/index.mjs +3 -0
  9. package/esm2022/lib/components/menu/menu-item.mjs +22 -0
  10. package/esm2022/lib/components/menu/menu.mjs +99 -0
  11. package/esm2022/lib/components/metadata/index.mjs +2 -0
  12. package/esm2022/lib/components/metadata/metadata.component.mjs +65 -0
  13. package/esm2022/lib/components/theme/index.mjs +3 -0
  14. package/esm2022/lib/components/theme/theme-selector.component.mjs +67 -0
  15. package/esm2022/lib/components/theme/theme-toggle.component.mjs +67 -0
  16. package/esm2022/lib/directives/index.mjs +5 -0
  17. package/esm2022/lib/directives/infinite-scroll.directive.mjs +47 -0
  18. package/esm2022/lib/directives/select-article-on-click.directive.mjs +39 -0
  19. package/esm2022/lib/directives/show-bookmark.directive.mjs +55 -0
  20. package/esm2022/lib/directives/theme-provider.directive.mjs +31 -0
  21. package/esm2022/lib/guards/auth.guard.mjs +27 -0
  22. package/esm2022/lib/guards/index.mjs +3 -0
  23. package/esm2022/lib/guards/initialization.guard.mjs +41 -0
  24. package/esm2022/lib/interceptors/audit.interceptor.mjs +23 -0
  25. package/esm2022/lib/interceptors/auth.interceptor.mjs +49 -0
  26. package/esm2022/lib/interceptors/body.interceptor.mjs +24 -0
  27. package/esm2022/lib/interceptors/error.interceptor.mjs +35 -0
  28. package/esm2022/lib/interceptors/index.mjs +7 -0
  29. package/esm2022/lib/interceptors/toast.interceptor.mjs +27 -0
  30. package/esm2022/lib/models/aggregation.mjs +2 -0
  31. package/esm2022/lib/models/article-metadata.mjs +2 -0
  32. package/esm2022/lib/models/autocomplete.mjs +2 -0
  33. package/esm2022/lib/models/custom-json.mjs +2 -0
  34. package/esm2022/lib/models/filter-dropdown.mjs +2 -0
  35. package/esm2022/lib/models/index.mjs +7 -0
  36. package/esm2022/lib/models/user-settings.mjs +2 -0
  37. package/esm2022/lib/pipes/highlight-word.pipe.mjs +33 -0
  38. package/esm2022/lib/pipes/index.mjs +3 -0
  39. package/esm2022/lib/pipes/source-icon.pipe.mjs +43 -0
  40. package/esm2022/lib/public-api.mjs +18 -0
  41. package/esm2022/lib/resolvers/index.mjs +2 -0
  42. package/esm2022/lib/resolvers/query-name-resolver.mjs +14 -0
  43. package/esm2022/lib/resources/index.mjs +2 -0
  44. package/esm2022/lib/resources/themes.mjs +53 -0
  45. package/esm2022/lib/services/application.service.mjs +245 -0
  46. package/esm2022/lib/services/autocomplete.service.mjs +85 -0
  47. package/esm2022/lib/services/drawer/backdrop.service.mjs +23 -0
  48. package/esm2022/lib/services/drawer/drawer-stack.service.mjs +152 -0
  49. package/esm2022/lib/services/drawer/drawer.service.mjs +38 -0
  50. package/esm2022/lib/services/index.mjs +12 -0
  51. package/esm2022/lib/services/label.service.mjs +161 -0
  52. package/esm2022/lib/services/navigation.service.mjs +59 -0
  53. package/esm2022/lib/services/saved-searches.service.mjs +75 -0
  54. package/esm2022/lib/services/search.service.mjs +89 -0
  55. package/esm2022/lib/services/selection-history.service.mjs +92 -0
  56. package/esm2022/lib/services/selection.service.mjs +87 -0
  57. package/esm2022/lib/stores/aggregations.store.mjs +59 -0
  58. package/esm2022/lib/stores/app.store.mjs +270 -0
  59. package/esm2022/lib/stores/application.store.mjs +87 -0
  60. package/esm2022/lib/stores/index.mjs +9 -0
  61. package/esm2022/lib/stores/principal.store.mjs +47 -0
  62. package/esm2022/lib/stores/query-params.store.mjs +207 -0
  63. package/esm2022/lib/stores/selection.store.mjs +45 -0
  64. package/esm2022/lib/stores/theme.store.mjs +116 -0
  65. package/esm2022/lib/stores/user-settings.store.mjs +391 -0
  66. package/esm2022/lib/tokens/highlights.mjs +32 -0
  67. package/esm2022/lib/tokens/index.mjs +2 -0
  68. package/esm2022/lib/utils/debounced-signal.mjs +38 -0
  69. package/esm2022/lib/utils/index.mjs +8 -0
  70. package/esm2022/lib/utils/inline-worker.mjs +40 -0
  71. package/esm2022/lib/utils/query.mjs +58 -0
  72. package/esm2022/lib/utils/routes.mjs +28 -0
  73. package/esm2022/lib/utils/tailwind-utils.mjs +6 -0
  74. package/esm2022/lib/utils/theme-body-hook.mjs +18 -0
  75. package/esm2022/lib/utils/theme-registry.mjs +6 -0
  76. package/esm2022/lib/web-services/aggregations.service.mjs +104 -0
  77. package/esm2022/lib/web-services/app.service.mjs +48 -0
  78. package/esm2022/lib/web-services/audit.service.mjs +122 -0
  79. package/esm2022/lib/web-services/index.mjs +10 -0
  80. package/esm2022/lib/web-services/json-method-plugin.service.mjs +54 -0
  81. package/esm2022/lib/web-services/preview.service.mjs +327 -0
  82. package/esm2022/lib/web-services/principal.service.mjs +46 -0
  83. package/esm2022/lib/web-services/query.service.mjs +123 -0
  84. package/esm2022/lib/web-services/text-chunck.service.mjs +46 -0
  85. package/esm2022/public-api.mjs +5 -0
  86. package/esm2022/sinequa-atomic-angular.mjs +5 -0
  87. package/fesm2022/sinequa-atomic-angular.mjs +4420 -0
  88. package/fesm2022/sinequa-atomic-angular.mjs.map +1 -0
  89. package/index.d.ts +5 -0
  90. package/lib/assistant/index.d.ts +1 -0
  91. package/lib/assistant/signalR.web.service.d.ts +46 -0
  92. package/lib/components/dropdown-input.d.ts +18 -0
  93. package/lib/components/dropdown-list.d.ts +8 -0
  94. package/lib/components/dropdown.d.ts +50 -0
  95. package/lib/components/index.d.ts +6 -0
  96. package/lib/components/menu/index.d.ts +2 -0
  97. package/lib/components/menu/menu-item.d.ts +8 -0
  98. package/lib/components/menu/menu.d.ts +24 -0
  99. package/lib/components/metadata/index.d.ts +1 -0
  100. package/lib/components/metadata/metadata.component.d.ts +24 -0
  101. package/lib/components/theme/index.d.ts +2 -0
  102. package/lib/components/theme/theme-selector.component.d.ts +70 -0
  103. package/lib/components/theme/theme-toggle.component.d.ts +10 -0
  104. package/lib/directives/index.d.ts +4 -0
  105. package/lib/directives/infinite-scroll.directive.d.ts +30 -0
  106. package/lib/directives/select-article-on-click.directive.d.ts +14 -0
  107. package/lib/directives/show-bookmark.directive.d.ts +60 -0
  108. package/lib/directives/theme-provider.directive.d.ts +20 -0
  109. package/lib/guards/auth.guard.d.ts +7 -0
  110. package/lib/guards/index.d.ts +2 -0
  111. package/lib/guards/initialization.guard.d.ts +20 -0
  112. package/lib/interceptors/audit.interceptor.d.ts +13 -0
  113. package/lib/interceptors/auth.interceptor.d.ts +14 -0
  114. package/lib/interceptors/body.interceptor.d.ts +11 -0
  115. package/lib/interceptors/error.interceptor.d.ts +9 -0
  116. package/lib/interceptors/index.d.ts +5 -0
  117. package/lib/interceptors/toast.interceptor.d.ts +13 -0
  118. package/lib/models/aggregation.d.ts +12 -0
  119. package/lib/models/article-metadata.d.ts +5 -0
  120. package/lib/models/autocomplete.d.ts +5 -0
  121. package/lib/models/custom-json.d.ts +69 -0
  122. package/lib/models/filter-dropdown.d.ts +10 -0
  123. package/lib/models/index.d.ts +6 -0
  124. package/lib/models/user-settings.d.ts +38 -0
  125. package/lib/pipes/highlight-word.pipe.d.ts +22 -0
  126. package/lib/pipes/index.d.ts +2 -0
  127. package/lib/pipes/source-icon.pipe.d.ts +51 -0
  128. package/lib/public-api.d.ts +14 -0
  129. package/lib/resolvers/index.d.ts +1 -0
  130. package/lib/resolvers/query-name-resolver.d.ts +9 -0
  131. package/lib/resources/index.d.ts +1 -0
  132. package/lib/resources/themes.d.ts +51 -0
  133. package/lib/services/application.service.d.ts +183 -0
  134. package/lib/services/autocomplete.service.d.ts +96 -0
  135. package/lib/services/drawer/backdrop.service.d.ts +9 -0
  136. package/lib/services/drawer/drawer-stack.service.d.ts +70 -0
  137. package/lib/services/drawer/drawer.service.d.ts +15 -0
  138. package/lib/services/index.d.ts +11 -0
  139. package/lib/services/label.service.d.ts +114 -0
  140. package/lib/services/navigation.service.d.ts +33 -0
  141. package/lib/services/saved-searches.service.d.ts +149 -0
  142. package/lib/services/search.service.d.ts +159 -0
  143. package/lib/services/selection-history.service.d.ts +50 -0
  144. package/lib/services/selection.service.d.ts +123 -0
  145. package/lib/stores/aggregations.store.d.ts +25 -0
  146. package/lib/stores/app.store.d.ts +110 -0
  147. package/lib/stores/application.store.d.ts +83 -0
  148. package/lib/stores/index.d.ts +8 -0
  149. package/lib/stores/principal.store.d.ts +47 -0
  150. package/lib/stores/query-params.store.d.ts +128 -0
  151. package/lib/stores/selection.store.d.ts +52 -0
  152. package/lib/stores/theme.store.d.ts +66 -0
  153. package/lib/stores/user-settings.store.d.ts +111 -0
  154. package/lib/tokens/highlights.d.ts +8 -0
  155. package/lib/tokens/index.d.ts +1 -0
  156. package/lib/utils/debounced-signal.d.ts +25 -0
  157. package/lib/utils/index.d.ts +7 -0
  158. package/lib/utils/inline-worker.d.ts +11 -0
  159. package/lib/utils/query.d.ts +26 -0
  160. package/lib/utils/routes.d.ts +16 -0
  161. package/lib/utils/tailwind-utils.d.ts +2 -0
  162. package/lib/utils/theme-body-hook.d.ts +6 -0
  163. package/lib/utils/theme-registry.d.ts +3 -0
  164. package/lib/web-services/aggregations.service.d.ts +57 -0
  165. package/lib/web-services/app.service.d.ts +30 -0
  166. package/lib/web-services/audit.service.d.ts +72 -0
  167. package/lib/web-services/index.d.ts +9 -0
  168. package/lib/web-services/json-method-plugin.service.d.ts +41 -0
  169. package/lib/web-services/preview.service.d.ts +283 -0
  170. package/lib/web-services/principal.service.d.ts +28 -0
  171. package/lib/web-services/query.service.d.ts +29 -0
  172. package/lib/web-services/text-chunck.service.d.ts +22 -0
  173. package/package.json +28 -0
  174. package/public-api.d.ts +1 -0
@@ -0,0 +1,4420 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, Injectable, signal, input, viewChild, afterNextRender, Component, HostListener, output, ViewEncapsulation, computed, inject, assertInInjectionContext, runInInjectionContext, effect, Injector, model, Directive, EventEmitter, ElementRef, Pipe, Inject } from '@angular/core';
3
+ import { HubConnectionBuilder, LogLevel } from '@microsoft/signalr';
4
+ import { globalConfig, getMetadata, getQueryParamsFromUrl, queryParamsFromUrl, addConcepts, fetchUserSettings, deleteUserSettings, patchUserSettings, isAuthenticated, isJsonable, addAuditAdditionalInfo, getToken, setToken, login, setGlobalConfig, labels, fetchSuggest, fetchLabels, buildPathsAndLevels, logout, escapeExpr, isObject, Audit } from '@sinequa/atomic';
5
+ import { autoUpdate, computePosition, offset, flip, shift } from '@floating-ui/dom';
6
+ import { NgClass, NgStyle, Location } from '@angular/common';
7
+ import { clsx } from 'clsx';
8
+ import { twMerge } from 'tailwind-merge';
9
+ import { withDevtools } from '@angular-architects/ngrx-toolkit';
10
+ import { signalStore, signalStoreFeature, withState, withMethods, patchState, getState, withComputed } from '@ngrx/signals';
11
+ import { catchError, EMPTY, firstValueFrom, map, Subject, filter, tap, shareReplay, BehaviorSubject, Subscription, of, from, forkJoin, switchMap, throwError } from 'rxjs';
12
+ import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
13
+ import { ActivatedRoute, Router, NavigationEnd } from '@angular/router';
14
+ import * as i1 from '@angular/forms';
15
+ import { FormsModule } from '@angular/forms';
16
+ import { catchError as catchError$1 } from 'rxjs/operators';
17
+ import { toast } from 'ngx-sonner';
18
+ import highlightWords from 'highlight-words';
19
+ import { DomSanitizer } from '@angular/platform-browser';
20
+
21
+ /** A token that is used to inject the transports allowed to use by hub connection.
22
+ *
23
+ * Expects a {@link HttpTransportType} value.
24
+ */
25
+ const SIGNAL_R_TRANSPORTS = new InjectionToken('SIGNAL_R_TRANSPORTS');
26
+ /** A token that is used to inject the log level for the hub connection.
27
+ *
28
+ * Expects a {@link LogLevel} value.
29
+ */
30
+ const SIGNAL_R_LOG_LEVEL = new InjectionToken('SIGNAL_R_LOG_LEVEL');
31
+ /**
32
+ * A service to connect the Sinequa server to the client via SignalR
33
+ */
34
+ class SignalRWebService {
35
+ constructor() {
36
+ this.endpoints = `${globalConfig.backendUrl}/endpoints/v1`;
37
+ }
38
+ /**
39
+ * Builds a SignalR connection to the given endpoint
40
+ * @param endpointName Name of the endpoint to connect to
41
+ * @param options Options for the connection. It must overrides the default options
42
+ * @param logLevel The log level for the connection
43
+ * @param automaticReconnect Whether the connection should automatically attempt to reconnect
44
+ * @returns A SignalR connection
45
+ */
46
+ buildConnection(endpointName, options, logLevel = LogLevel.Information, automaticReconnect = false) {
47
+ const url = `${this.endpoints}/${endpointName}`;
48
+ const connectionBuilder = new HubConnectionBuilder()
49
+ .withUrl(url, options)
50
+ .configureLogging(logLevel);
51
+ if (automaticReconnect) {
52
+ connectionBuilder.withAutomaticReconnect();
53
+ }
54
+ return connectionBuilder.build();
55
+ }
56
+ /**
57
+ * Starts a SignalR connection.
58
+ * @param connection A SignalR connection
59
+ */
60
+ async startConnection(connection) {
61
+ if (!connection) {
62
+ throw new Error("Please provide a valid connection to start");
63
+ }
64
+ try {
65
+ await connection.start();
66
+ console.log(`The SignalR connection has been successfully established! \n url: ${connection.baseUrl} \n connectionId: ${connection.connectionId}`);
67
+ }
68
+ catch (error) {
69
+ throw error;
70
+ }
71
+ }
72
+ /**
73
+ * Stops a SignalR connection.
74
+ * @param connection A SignalR connection
75
+ */
76
+ async stopConnection(connection) {
77
+ if (!connection) {
78
+ throw new Error("Please provide a valid connection to stop");
79
+ }
80
+ try {
81
+ await connection.stop();
82
+ console.log(`The SignalR connection has been successfully stopped! \n url: ${connection.baseUrl} \n connectionId: ${connection.connectionId}`);
83
+ }
84
+ catch (error) {
85
+ throw error;
86
+ }
87
+ }
88
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: SignalRWebService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
89
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: SignalRWebService, providedIn: "root" }); }
90
+ }
91
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: SignalRWebService, decorators: [{
92
+ type: Injectable,
93
+ args: [{
94
+ providedIn: "root"
95
+ }]
96
+ }] });
97
+
98
+ class DropdownComponent {
99
+ mouseleave(event) {
100
+ this.close();
101
+ }
102
+ /**
103
+ * Initializes the dropdown component and sets up automatic updates for its position.
104
+ *
105
+ * The constructor uses `afterNextRender` to ensure that the DOM elements are available
106
+ * before calling `autoUpdate`. The `autoUpdate` function monitors the trigger and dropdown
107
+ * elements, and if the dropdown is open, it recalculates its position.
108
+ *
109
+ * @constructor
110
+ */
111
+ constructor() {
112
+ this.isOpen = signal(false);
113
+ this.position = input('bottom-start');
114
+ this.autoClose = input(false);
115
+ this.disabled = input();
116
+ this.dropdown = viewChild('dropdownWrapper');
117
+ this.trigger = viewChild('trigger');
118
+ this.width = signal(0);
119
+ afterNextRender(() => {
120
+ autoUpdate(this.trigger()?.nativeElement, this.dropdown()?.nativeElement, () => {
121
+ if (!this.isOpen())
122
+ return;
123
+ this.calculatePosition();
124
+ });
125
+ });
126
+ }
127
+ /**
128
+ * Toggles the dropdown's open state.
129
+ * If the dropdown is disabled, the method returns immediately without making any changes.
130
+ * Otherwise, it updates the `isOpen` state to its opposite value and recalculates the dropdown's position.
131
+ */
132
+ toggle() {
133
+ if (this.disabled())
134
+ return;
135
+ this.isOpen.update(() => !this.isOpen());
136
+ this.calculatePosition();
137
+ }
138
+ /**
139
+ * Closes the dropdown by setting the `isOpen` state to `false`.
140
+ */
141
+ close() {
142
+ this.isOpen.set(false);
143
+ }
144
+ /**
145
+ * Calculates and sets the position of the dropdown element relative to the trigger element.
146
+ * Utilizes the `computePosition` function with specified middleware for offset, flip, and shift.
147
+ * Updates the dropdown's `left` and `top` styles based on the computed position.
148
+ * Also sets the width of the dropdown to match the trigger element's width.
149
+ *
150
+ * @returns {void}
151
+ */
152
+ calculatePosition() {
153
+ computePosition(this.trigger()?.nativeElement, this.dropdown()?.nativeElement, {
154
+ placement: this.position(),
155
+ middleware: [offset(8), flip(), shift()],
156
+ }).then(({ x, y }) => {
157
+ this.dropdown().nativeElement.style.left = x + 'px';
158
+ this.dropdown().nativeElement.style.top = y + 'px';
159
+ this.width.set(this.trigger()?.nativeElement.offsetWidth);
160
+ });
161
+ }
162
+ // Onclick outside the dropdown, close it
163
+ clickout(event) {
164
+ if (
165
+ // Check if the click was outside the dropdown
166
+ // and the dropdown is open
167
+ // and the click was not on the button
168
+ // then close the dropdown
169
+ !this.dropdown()?.nativeElement.contains(event.target)
170
+ && this.isOpen
171
+ && !this.trigger()?.nativeElement.contains(event.target)) {
172
+ this.isOpen.set(false);
173
+ }
174
+ }
175
+ /**
176
+ * Handles the click event on the dropdown content.
177
+ * If the `autoClose` method returns true, it triggers the `close` method to close the dropdown.
178
+ */
179
+ contentClicked() {
180
+ if (this.autoClose())
181
+ this.close();
182
+ }
183
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: DropdownComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
184
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "18.2.5", type: DropdownComponent, isStandalone: true, selector: "sq-dropdown, Dropdown", inputs: { position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, autoClose: { classPropertyName: "autoClose", publicName: "autoClose", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "document:click": "clickout($event)" } }, viewQueries: [{ propertyName: "dropdown", first: true, predicate: ["dropdownWrapper"], descendants: true, isSignal: true }, { propertyName: "trigger", first: true, predicate: ["trigger"], descendants: true, isSignal: true }], ngImport: i0, template: `
185
+ <div #trigger class="select-none dropdown" (click)="toggle()">
186
+ <ng-content></ng-content>
187
+ </div>
188
+
189
+ <div #dropdownWrapper
190
+ class="dropdown-wrapper absolute text-sm min-w-fit"
191
+ [style.display]="isOpen() ? 'block' : 'none'"
192
+ [style.width]="width() + 'px'"
193
+ (click)="contentClicked()"
194
+ (mouseleave)="mouseleave($event)"
195
+ >
196
+ <ng-content select="[dropdown-content]"></ng-content>
197
+ </div>
198
+ `, isInline: true, styles: [".dropdown-wrapper{z-index:var(--z-dropdown, 100)}\n"] }); }
199
+ }
200
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: DropdownComponent, decorators: [{
201
+ type: Component,
202
+ args: [{ selector: 'sq-dropdown, Dropdown', standalone: true, template: `
203
+ <div #trigger class="select-none dropdown" (click)="toggle()">
204
+ <ng-content></ng-content>
205
+ </div>
206
+
207
+ <div #dropdownWrapper
208
+ class="dropdown-wrapper absolute text-sm min-w-fit"
209
+ [style.display]="isOpen() ? 'block' : 'none'"
210
+ [style.width]="width() + 'px'"
211
+ (click)="contentClicked()"
212
+ (mouseleave)="mouseleave($event)"
213
+ >
214
+ <ng-content select="[dropdown-content]"></ng-content>
215
+ </div>
216
+ `, styles: [".dropdown-wrapper{z-index:var(--z-dropdown, 100)}\n"] }]
217
+ }], ctorParameters: () => [], propDecorators: { clickout: [{
218
+ type: HostListener,
219
+ args: ['document:click', ['$event']]
220
+ }] } });
221
+
222
+ class DropdownListComponent {
223
+ constructor() {
224
+ this.items = input.required();
225
+ this.onClick = output();
226
+ }
227
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: DropdownListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
228
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.5", type: DropdownListComponent, isStandalone: true, selector: "sq-dropdown-list, DropdownList", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { onClick: "onClick" }, ngImport: i0, template: `
229
+ @for (item of items(); track $index) {
230
+ <span class="pill pill-sm pill-ghost bg-primary flex place-content-center items-center font-semibold text-white float-left m-1 select-none"
231
+ role="button">
232
+ {{ item.display || item.value }}
233
+ <i class="ms-1 fa-fw far fa-circle-xmark cursor-pointer" (click)="onClick.emit(item)"></i>
234
+ </span>
235
+ }
236
+ `, isInline: true }); }
237
+ }
238
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: DropdownListComponent, decorators: [{
239
+ type: Component,
240
+ args: [{
241
+ selector: 'sq-dropdown-list, DropdownList',
242
+ standalone: true,
243
+ template: `
244
+ @for (item of items(); track $index) {
245
+ <span class="pill pill-sm pill-ghost bg-primary flex place-content-center items-center font-semibold text-white float-left m-1 select-none"
246
+ role="button">
247
+ {{ item.display || item.value }}
248
+ <i class="ms-1 fa-fw far fa-circle-xmark cursor-pointer" (click)="onClick.emit(item)"></i>
249
+ </span>
250
+ }
251
+ `
252
+ }]
253
+ }] });
254
+
255
+ class DropdownInputComponent {
256
+ constructor() {
257
+ this.label = input();
258
+ this.placeholder = input('Start typing to search...');
259
+ this.noResultLabel = input('No results to be displayed...');
260
+ this.suggestions = input([]);
261
+ this.selected = input.required();
262
+ this.onFocus = output(); // emits the input value
263
+ this.onKeyUp = output(); // emits the input value
264
+ this.removeItem = output();
265
+ this.addItem = output();
266
+ }
267
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: DropdownInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
268
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.5", type: DropdownInputComponent, isStandalone: true, selector: "sq-dropdown-input, DropdownInput", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, noResultLabel: { classPropertyName: "noResultLabel", publicName: "noResultLabel", isSignal: true, isRequired: false, transformFunction: null }, suggestions: { classPropertyName: "suggestions", publicName: "suggestions", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { onFocus: "onFocus", onKeyUp: "onKeyUp", removeItem: "removeItem", addItem: "addItem" }, ngImport: i0, template: `
269
+ @if (label()) {
270
+ <p class="font-semibold">{{ label() }}</p>
271
+ }
272
+ <Dropdown class="w-full block">
273
+ <input
274
+ #searchInput
275
+ class="w-1/2 grow focus:outline-none rounded border border-neutral-200 group-hover:bg-white p-2"
276
+ type="text"
277
+ autocomplete="off"
278
+ spellcheck="false"
279
+ [attr.aria-label]="placeholder()"
280
+ [attr.placeholder]="placeholder()"
281
+ (focus)="onFocus.emit(searchInput.value)"
282
+ (keyup)="onKeyUp.emit(searchInput.value)"
283
+ />
284
+
285
+ <DropdownList class="mb-4" [items]="selected()" (onClick)="removeItem.emit($event)" />
286
+
287
+ <div dropdown-content class="dropdown-content w-max mt-[-1.5rem]">
288
+ @if (!suggestions().length) {
289
+ {{ noResultLabel() }}
290
+ } @else {
291
+ <ul class="data-list-xs flex-grow overflow-auto max-h-44 my-2">
292
+ @for (suggestion of suggestions(); track $index) {
293
+ <li class="p-2 flex data-list-item items-baseline gap-2 cursor-pointer"
294
+ (click)="addItem.emit(suggestion)">
295
+ {{ suggestion.display || suggestion.value }}
296
+ </li>
297
+ }
298
+ </ul>
299
+ }
300
+ </div>
301
+ </Dropdown>
302
+ `, isInline: true, dependencies: [{ kind: "component", type: DropdownComponent, selector: "sq-dropdown, Dropdown", inputs: ["position", "autoClose", "disabled"] }, { kind: "component", type: DropdownListComponent, selector: "sq-dropdown-list, DropdownList", inputs: ["items"], outputs: ["onClick"] }] }); }
303
+ }
304
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: DropdownInputComponent, decorators: [{
305
+ type: Component,
306
+ args: [{
307
+ selector: 'sq-dropdown-input, DropdownInput',
308
+ standalone: true,
309
+ imports: [DropdownComponent, DropdownListComponent],
310
+ template: `
311
+ @if (label()) {
312
+ <p class="font-semibold">{{ label() }}</p>
313
+ }
314
+ <Dropdown class="w-full block">
315
+ <input
316
+ #searchInput
317
+ class="w-1/2 grow focus:outline-none rounded border border-neutral-200 group-hover:bg-white p-2"
318
+ type="text"
319
+ autocomplete="off"
320
+ spellcheck="false"
321
+ [attr.aria-label]="placeholder()"
322
+ [attr.placeholder]="placeholder()"
323
+ (focus)="onFocus.emit(searchInput.value)"
324
+ (keyup)="onKeyUp.emit(searchInput.value)"
325
+ />
326
+
327
+ <DropdownList class="mb-4" [items]="selected()" (onClick)="removeItem.emit($event)" />
328
+
329
+ <div dropdown-content class="dropdown-content w-max mt-[-1.5rem]">
330
+ @if (!suggestions().length) {
331
+ {{ noResultLabel() }}
332
+ } @else {
333
+ <ul class="data-list-xs flex-grow overflow-auto max-h-44 my-2">
334
+ @for (suggestion of suggestions(); track $index) {
335
+ <li class="p-2 flex data-list-item items-baseline gap-2 cursor-pointer"
336
+ (click)="addItem.emit(suggestion)">
337
+ {{ suggestion.display || suggestion.value }}
338
+ </li>
339
+ }
340
+ </ul>
341
+ }
342
+ </div>
343
+ </Dropdown>
344
+ `
345
+ }]
346
+ }] });
347
+
348
+ function cn(...inputs) {
349
+ return twMerge(clsx(inputs));
350
+ }
351
+
352
+ class MenuComponent {
353
+ constructor() {
354
+ this.cn = cn;
355
+ this.default = "z-10 bg-white rounded-md border border-gray-300 shadow absolute text-sm";
356
+ this.isOpen = signal(false);
357
+ this.position = input('bottom-start');
358
+ this.className = input();
359
+ this.disabled = input();
360
+ this.autoClose = input(false);
361
+ this.dropdown = viewChild('dropdown');
362
+ this.trigger = viewChild('trigger');
363
+ this.width = signal(0);
364
+ }
365
+ ngAfterViewInit() {
366
+ autoUpdate(this.trigger()?.nativeElement, this.dropdown()?.nativeElement, () => {
367
+ if (!this.isOpen())
368
+ return;
369
+ this.calculatePosition();
370
+ });
371
+ }
372
+ toggle(e) {
373
+ e.stopPropagation();
374
+ if (this.disabled())
375
+ return;
376
+ this.isOpen.update(() => !this.isOpen());
377
+ this.calculatePosition();
378
+ }
379
+ close() {
380
+ this.isOpen.set(false);
381
+ }
382
+ calculatePosition() {
383
+ computePosition(this.trigger()?.nativeElement, this.dropdown()?.nativeElement, {
384
+ placement: this.position(),
385
+ middleware: [offset(8), flip(), shift()],
386
+ }).then(({ x, y }) => {
387
+ this.dropdown().nativeElement.style.left = x + 'px';
388
+ this.dropdown().nativeElement.style.top = y + 'px';
389
+ this.width.set(this.trigger()?.nativeElement.offsetWidth);
390
+ });
391
+ }
392
+ contentClicked(e) {
393
+ e.stopPropagation();
394
+ if (this.autoClose())
395
+ this.close();
396
+ }
397
+ // Onclick outside the dropdown, close it
398
+ clickout(event) {
399
+ if (!this.dropdown()?.nativeElement.contains(event.target) &&
400
+ this.isOpen &&
401
+ !this.trigger()?.nativeElement.contains(event.target)) {
402
+ this.isOpen.set(false);
403
+ }
404
+ }
405
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: MenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
406
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "18.2.5", type: MenuComponent, isStandalone: true, selector: "sq-menu, Menu", inputs: { position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, autoClose: { classPropertyName: "autoClose", publicName: "autoClose", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "document:click": "clickout($event)" } }, viewQueries: [{ propertyName: "dropdown", first: true, predicate: ["dropdown"], descendants: true, isSignal: true }, { propertyName: "trigger", first: true, predicate: ["trigger"], descendants: true, isSignal: true }], ngImport: i0, template: `
407
+ <div #trigger class="select-none" (click)="toggle($event)">
408
+ <ng-content></ng-content>
409
+ </div>
410
+ <div #dropdown
411
+ [ngClass]= "cn(default, className(), disabled() && 'disabled cursor-default opacity-50 hover:bg-transparent')"
412
+ [style.display]="isOpen() ? 'block' : 'none'"
413
+ [style.width]="width() + 'px'"
414
+ (click)="contentClicked($event)"
415
+ >
416
+ <ng-content select="[menu-content]"></ng-content>
417
+ </div>
418
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], encapsulation: i0.ViewEncapsulation.None }); }
419
+ }
420
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: MenuComponent, decorators: [{
421
+ type: Component,
422
+ args: [{
423
+ selector: 'sq-menu, Menu',
424
+ standalone: true,
425
+ imports: [NgClass],
426
+ encapsulation: ViewEncapsulation.None,
427
+ template: `
428
+ <div #trigger class="select-none" (click)="toggle($event)">
429
+ <ng-content></ng-content>
430
+ </div>
431
+ <div #dropdown
432
+ [ngClass]= "cn(default, className(), disabled() && 'disabled cursor-default opacity-50 hover:bg-transparent')"
433
+ [style.display]="isOpen() ? 'block' : 'none'"
434
+ [style.width]="width() + 'px'"
435
+ (click)="contentClicked($event)"
436
+ >
437
+ <ng-content select="[menu-content]"></ng-content>
438
+ </div>
439
+ `
440
+ }]
441
+ }], propDecorators: { clickout: [{
442
+ type: HostListener,
443
+ args: ['document:click', ['$event']]
444
+ }] } });
445
+
446
+ class MenuItemComponent {
447
+ constructor(menu) {
448
+ this.menu = menu;
449
+ }
450
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: MenuItemComponent, deps: [{ token: MenuComponent }], target: i0.ɵɵFactoryTarget.Component }); }
451
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.5", type: MenuItemComponent, isStandalone: true, selector: "sq-menu-item, menu-item, MenuItem", host: { classAttribute: "data-list-item flex gap-2 items-center justify-start py-2 px-4 text-sm cursor-pointer" }, ngImport: i0, template: `
452
+ <ng-content></ng-content>
453
+ `, isInline: true, styles: [":host[disabled]{cursor:default}\n"] }); }
454
+ }
455
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: MenuItemComponent, decorators: [{
456
+ type: Component,
457
+ args: [{ selector: "sq-menu-item, menu-item, MenuItem", standalone: true, imports: [NgClass], template: `
458
+ <ng-content></ng-content>
459
+ `, host: {
460
+ class: 'data-list-item flex gap-2 items-center justify-start py-2 px-4 text-sm cursor-pointer'
461
+ }, styles: [":host[disabled]{cursor:default}\n"] }]
462
+ }], ctorParameters: () => [{ type: MenuComponent }] });
463
+
464
+ class MetadataComponent {
465
+ constructor() {
466
+ this.cn = cn;
467
+ this.default = "text-ellipsis";
468
+ this.override = input(false);
469
+ this.className = input();
470
+ this.article = input.required();
471
+ this.metadata = input.required();
472
+ this.onMetadataClick = output();
473
+ this.items = computed(() => getMetadata(this.article(), this.metadata()));
474
+ }
475
+ onClick(event, item) {
476
+ event.stopPropagation();
477
+ this.onMetadataClick.emit(item);
478
+ }
479
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: MetadataComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
480
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.5", type: MetadataComponent, isStandalone: true, selector: "sq-metadata, Metadata", inputs: { override: { classPropertyName: "override", publicName: "override", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, article: { classPropertyName: "article", publicName: "article", isSignal: true, isRequired: true, transformFunction: null }, metadata: { classPropertyName: "metadata", publicName: "metadata", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { onMetadataClick: "onMetadataClick" }, host: { properties: { "class.hidden": "items().length === 0" } }, exportAs: ["metadata"], ngImport: i0, template: `
481
+
482
+ @if(override()) {
483
+ <ng-content></ng-content>
484
+ }
485
+ @else {
486
+ <ng-content select="[before]"></ng-content>
487
+ @for(item of items(); track $index) {
488
+ @if(item.display) {
489
+ <span [ngClass]="cn(default, className())" (click)="onClick($event, {field: this.metadata() ,value: item.display })">{{ item.display }}</span>
490
+ }
491
+ }
492
+ <ng-content select="[after]"></ng-content>
493
+ }
494
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }] }); }
495
+ }
496
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: MetadataComponent, decorators: [{
497
+ type: Component,
498
+ args: [{
499
+ selector: 'sq-metadata, Metadata',
500
+ exportAs: 'metadata',
501
+ standalone: true,
502
+ imports: [NgClass],
503
+ template: `
504
+
505
+ @if(override()) {
506
+ <ng-content></ng-content>
507
+ }
508
+ @else {
509
+ <ng-content select="[before]"></ng-content>
510
+ @for(item of items(); track $index) {
511
+ @if(item.display) {
512
+ <span [ngClass]="cn(default, className())" (click)="onClick($event, {field: this.metadata() ,value: item.display })">{{ item.display }}</span>
513
+ }
514
+ }
515
+ <ng-content select="[after]"></ng-content>
516
+ }
517
+ `,
518
+ host: {
519
+ '[class.hidden]': 'items().length === 0'
520
+ }
521
+ }]
522
+ }] });
523
+
524
+ const THEMES = [
525
+ {
526
+ name: "Default",
527
+ id: "default",
528
+ private: false,
529
+ colors: {
530
+ "button-primary-background": "none",
531
+ background: "0 0% 100%",
532
+ foreground: "225 11% 7%",
533
+ card: "0 0% 100%",
534
+ "card-foreground": "225 11% 7%",
535
+ active: "219 97% 54%",
536
+ "active-foreground": "0 0% 98%",
537
+ "active-background": "219 97% 54% / 10%",
538
+ primary: "219 97% 54%",
539
+ "primary-foreground": "0 0% 98%",
540
+ secondary: "240 4.8% 95.9%",
541
+ "secondary-foreground": "240 5.9% 10%",
542
+ muted: "240 4.8% 95.9%",
543
+ "muted-foreground": "240 3.8% 46.1%",
544
+ accent: "240 4.8% 95.9%",
545
+ "accent-foreground": "240 5.9% 10%",
546
+ destructive: "0 84.2% 60.2%",
547
+ "destructive-foreground": "0 0% 98%",
548
+ border: "240 5.9% 90%",
549
+ input: "240 5.9% 90%",
550
+ ring: "225 11% 7%",
551
+ },
552
+ colorsDark: {
553
+ background: "240 10% 3.9%",
554
+ foreground: "0 0% 98%",
555
+ card: "240 10% 3.9%",
556
+ "card-foreground": "0 0% 98%",
557
+ active: "0 0% 98%",
558
+ "active-foreground": "240 5.9% 10%",
559
+ "active-background": "0 0% 98% / 10%",
560
+ primary: "0 0% 98%",
561
+ "primary-foreground": "240 5.9% 10%",
562
+ secondary: "240 3.7% 15.9%",
563
+ "secondary-foreground": "0 0% 98%",
564
+ muted: "240 3.7% 15.9%",
565
+ "muted-foreground": "240 5% 64.9%",
566
+ accent: "240 3.7% 15.9%",
567
+ "accent-foreground": "0 0% 98%",
568
+ destructive: "0 62.8% 30.6%",
569
+ "destructive-foreground": "0 0% 98%",
570
+ border: "240 3.7% 15.9%",
571
+ input: "240 3.7% 15.9%",
572
+ ring: "240 4.9% 83.9%",
573
+ },
574
+ }
575
+ ];
576
+
577
+ const AggregationsStore = signalStore(
578
+ // providing store at the root level
579
+ { providedIn: 'root' }, withDevtools('Aggregations'), withAggregationsFeatures());
580
+ function withAggregationsFeatures() {
581
+ return signalStoreFeature(withState({ aggregations: [] }), withMethods((store) => ({
582
+ /**
583
+ * Updates the state with the provided aggregations.
584
+ *
585
+ * @param {Aggregation[]} aggregations - The new aggregations to update the state with.
586
+ */
587
+ update(aggregations) {
588
+ patchState(store, (state) => {
589
+ return { ...state, aggregations };
590
+ });
591
+ },
592
+ /**
593
+ * Updates an existing aggregation in the store.
594
+ *
595
+ * @param aggregation - The aggregation object to update.
596
+ * @returns void
597
+ */
598
+ updateAggregation(aggregation) {
599
+ patchState(store, (state) => {
600
+ const index = state.aggregations.findIndex((a) => a.name === aggregation.name);
601
+ if (index !== -1) {
602
+ state.aggregations[index] = aggregation;
603
+ }
604
+ return { ...state };
605
+ });
606
+ },
607
+ /**
608
+ * Clears the aggregations in the store by setting the `aggregations` property to an empty array.
609
+ *
610
+ * @remarks
611
+ * This method uses the `patchState` function to update the state of the store.
612
+ */
613
+ clear() {
614
+ patchState(store, (state) => {
615
+ return { ...state, aggregations: [] };
616
+ });
617
+ },
618
+ /**
619
+ * Retrieves an aggregation by name or column.
620
+ *
621
+ * @param name - The name or column of the aggregation to retrieve.
622
+ * @param kind - The kind of aggregation to retrieve. Defaults to "name".
623
+ * @returns The matching aggregation, or undefined if not found.
624
+ */
625
+ getAggregation(name, kind = "name") {
626
+ if (kind === "column") {
627
+ return getState(store).aggregations.find((aggregation) => aggregation.column === name);
628
+ }
629
+ return getState(store).aggregations.find((aggregation) => aggregation.name === name);
630
+ }
631
+ })));
632
+ }
633
+
634
+ class AppService {
635
+ constructor() {
636
+ this.http = inject(HttpClient);
637
+ this.API_URL = `${globalConfig.backendUrl}/${globalConfig.apiPath}`;
638
+ }
639
+ /**
640
+ * Retrieves the application configuration from the server.
641
+ *
642
+ * @param appName - The name of the application to fetch the configuration for.
643
+ *
644
+ * @returns {Observable<CCApp>} An observable that emits the application configuration.
645
+ *
646
+ * @remarks
647
+ * This method constructs an HTTP GET request to fetch the application configuration
648
+ * using the `app` parameter from the global configuration. If the request fails,
649
+ * it logs the error to the console and returns an empty observable.
650
+ *
651
+ * @example
652
+ * ```typescript
653
+ * appService.getApp().subscribe(appConfig => {
654
+ * console.log(appConfig);
655
+ * });
656
+ * ```
657
+ */
658
+ getApp(appName) {
659
+ const app = appName || globalConfig.app;
660
+ const params = new HttpParams().set('app', app || '');
661
+ return this.http.get(this.API_URL + "/app", { params })
662
+ .pipe(catchError(error => {
663
+ console.error("AppService.getApp failure - error: ", error);
664
+ return EMPTY;
665
+ }));
666
+ }
667
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: AppService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
668
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: AppService, providedIn: 'root' }); }
669
+ }
670
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: AppService, decorators: [{
671
+ type: Injectable,
672
+ args: [{
673
+ providedIn: 'root'
674
+ }]
675
+ }] });
676
+
677
+ /**
678
+ * Injection token for a list of facet names to display in the application.
679
+ *
680
+ * This token provides a default list of facet names including:
681
+ * - Geo
682
+ * - Company
683
+ * - Person
684
+ * - DocFormat
685
+ * - Modified
686
+ * - Size
687
+ * - DocumentLanguages
688
+ * - Concepts
689
+ *
690
+ * @constant
691
+ * @type {InjectionToken<string[]>}
692
+ */
693
+ const AGGREGATIONS_NAMES_PRESET_DEFAULT = [
694
+ "Places", "Sources", "Company", "People", "Formats", "Modified", "Sizes", "Languages", "Concepts",
695
+ "DocFormat", "Geo", "Person", "Treepath", "DocumentLanguages", "Size"
696
+ ];
697
+ const AGGREGATIONS_NAMES = new InjectionToken('Facets list to display', { factory: () => AGGREGATIONS_NAMES_PRESET_DEFAULT });
698
+ const AppStore = signalStore({ providedIn: 'root' }, withDevtools('App'), withAppFeatures(), withAppCustomizationFeatures());
699
+ /**
700
+ * Basic app management features
701
+ */
702
+ function withAppFeatures() {
703
+ return signalStoreFeature(withState({ webServices: {}, queries: {} }),
704
+ /**
705
+ * Enhances the application store with various features and methods.
706
+ *
707
+ * This function integrates state management, computed properties, and methods
708
+ * to interact with the CCApp state, including initialization, updates,
709
+ * and retrieval of specific data such as web services, queries, and customization JSONs.
710
+ *
711
+ * Features:
712
+ * - State management with initial data.
713
+ * - Computed properties for customization JSON, sources, and filters.
714
+ * - Methods for initializing and updating the application state.
715
+ * - Methods for retrieving web services, labels, queries, and customization JSONs.
716
+ * - Methods for retrieving aggregation icons and customization items.
717
+ *
718
+ * @returns The enhanced application store with additional features and methods.
719
+ */
720
+ withMethods((store, appService = inject(AppService)) => ({
721
+ /**
722
+ * Initializes the application state by fetching the app data from the appService
723
+ * and updating the store with the retrieved data.
724
+ *
725
+ * @returns A promise that resolves when the app data has been fetched and the store has been updated.
726
+ */
727
+ initialize() {
728
+ return firstValueFrom(appService.getApp().pipe(map(app => patchState(store, app))));
729
+ },
730
+ /**
731
+ * Initializes the application state with the provided app name.
732
+ *
733
+ * @param appName - The name of the application to fetch the configuration for.
734
+ *
735
+ * @returns A promise that resolves when the app data has been fetched and the store has been updated.
736
+ */
737
+ initializeWithAppName(appName) {
738
+ return firstValueFrom(appService.getApp(appName).pipe(map(app => patchState(store, app))));
739
+ },
740
+ /**
741
+ * Updates the application state with the provided CCApp object.
742
+ *
743
+ * @param {CCApp} app - The application object containing the new state values.
744
+ */
745
+ update(app) {
746
+ patchState(store, (state) => {
747
+ return { ...state, ...app };
748
+ });
749
+ },
750
+ /**
751
+ * Returns the web service by type name
752
+ *
753
+ * @param type Web service type name
754
+ * @returns A {@link CCWebService} object or undefined if not found
755
+ */
756
+ getWebServiceByType(type) {
757
+ let webService = undefined;
758
+ Object.keys(store.webServices())
759
+ .forEach((key) => {
760
+ const w = store.webServices();
761
+ const ws = w[key];
762
+ // Check if the web service type matches the specified type in lowercase
763
+ if (ws.webServiceType.toLowerCase() === type.toLocaleLowerCase())
764
+ webService = ws;
765
+ });
766
+ return webService;
767
+ },
768
+ /**
769
+ * Retrieves the labels from the web service of type 'Labels'.
770
+ *
771
+ * @returns An object containing the private and public labels.
772
+ * If the labels are not found, returns an object with empty strings for both fields.
773
+ */
774
+ getLabels() {
775
+ const labels = this.getWebServiceByType('labels');
776
+ if (!labels)
777
+ return { private: '', public: '' };
778
+ const { publicLabelsField, privateLabelsField } = labels;
779
+ return ({ private: privateLabelsField, public: publicLabelsField });
780
+ },
781
+ /**
782
+ * Retrieves a query by its name from the store.
783
+ *
784
+ * @param name - The name of the query to retrieve.
785
+ * @returns The query object if found, otherwise `undefined`.
786
+ */
787
+ getQueryByName(name) {
788
+ const queries = store.queries();
789
+ return queries[name.toLowerCase()];
790
+ },
791
+ /**
792
+ * Retrieves a query by its index from the store.
793
+ *
794
+ * @param index - The index of the query to retrieve.
795
+ * @returns The query at the specified index, or `undefined` if the index is out of bounds.
796
+ */
797
+ getQueryByIndex(index) {
798
+ const queries = store.queries();
799
+ const keys = Object.keys(queries);
800
+ return queries[keys[index]];
801
+ },
802
+ /**
803
+ * Retrieves the default query.
804
+ * The default query is always the first query in the list of queries.
805
+ *
806
+ * @returns {CCQuery | undefined} The default query if it exists, otherwise undefined.
807
+ */
808
+ getDefaultQuery() {
809
+ return this.getQueryByIndex(0);
810
+ },
811
+ /**
812
+ * Retrieves the allowEmptySearch property for a specific query.
813
+ * @param queryName - The name of the query for which to retrieve allow empty serach property.
814
+ * @returns The allowEmptySearch property for the specified query, or false if not found.
815
+ */
816
+ allowEmptySearch(queryName) {
817
+ const queries = store.queries();
818
+ const keys = Object.keys(queries);
819
+ if (keys.length === 0)
820
+ return false;
821
+ const query = (queryName && queries[queryName.toLocaleLowerCase()]) || queries[keys[0]];
822
+ return query?.allowEmptySearch || false;
823
+ },
824
+ /**
825
+ * Retrieves the alias for a given column name from the application's state.
826
+ * If the column has aliases defined, the first alias is returned. Otherwise,
827
+ * the original column name is returned.
828
+ *
829
+ * @param column - The name of the column for which to retrieve the alias.
830
+ * @returns The alias of the column if it exists, otherwise the original column name.
831
+ */
832
+ getColumnAlias(column) {
833
+ const state = getState(store);
834
+ const schema = state.indexes?.['_']?.columns;
835
+ const col = schema[column];
836
+ if (col) {
837
+ return col.aliases?.[0] || column;
838
+ }
839
+ return column;
840
+ },
841
+ })), withMethods((store, aggregationsStore = inject(AggregationsStore), aggregationsNames = inject(AGGREGATIONS_NAMES)) => ({
842
+ /**
843
+ * Retrieves the sorted aggregations based on the provided query name.
844
+ * @param queryName - The name of the query.
845
+ * @returns An array of sorted aggregations.
846
+ */
847
+ getAuthorizedFilters(route) {
848
+ const { queryName } = route.snapshot.data;
849
+ const { aggregations = [] } = store.getQueryByName(queryName.toLowerCase()) || {};
850
+ const agg = [...aggregationsStore.aggregations().filter(aggregation => aggregationsNames.includes(aggregation.name.trim()))];
851
+ // sort the aggregations based on the query order
852
+ if (Array.isArray(aggregations)) {
853
+ const queryAggregationsOrder = aggregations.map(aggregation => aggregation.name.trim()) || [];
854
+ return agg.toSorted((a, b) => queryAggregationsOrder.indexOf(a.name) - queryAggregationsOrder.indexOf(b.name));
855
+ }
856
+ // return the default order
857
+ return agg;
858
+ }
859
+ })));
860
+ }
861
+ /**
862
+ * Management of customization JSONs features for the app
863
+ */
864
+ function withAppCustomizationFeatures() {
865
+ return signalStoreFeature(withState({ customJSONs: [], data: {} }), withComputed(({ customJSONs, data }) => ({
866
+ customizationJson: computed(() => data()),
867
+ sources: computed(() => {
868
+ if (customJSONs() === undefined || Array.isArray(customJSONs()) === false)
869
+ return data().sources || {};
870
+ const sourcesJson = customJSONs()?.find(json => json.name === "sources");
871
+ const sources = sourcesJson?.data['sources'] || {};
872
+ return sources ?? data().sources ?? {};
873
+ }),
874
+ filters: computed(() => {
875
+ if (customJSONs() === undefined || Array.isArray(customJSONs()) === false)
876
+ return data().filters || [];
877
+ const filtersJson = customJSONs()?.find(json => json.name === "filters");
878
+ const filters = filtersJson?.data['filters'] || [];
879
+ return filters ?? data().filters ?? [];
880
+ })
881
+ })), withMethods((store) => ({
882
+ /**
883
+ * Retrieves the customization json by name
884
+ * @param name - The name of the customization json
885
+ * @returns The customization json object or undefined if not found
886
+ */
887
+ getNamedCustomizationJson(name) {
888
+ return store.customJSONs().find(data => data.name === name);
889
+ },
890
+ /**
891
+ * Retrieves the icon associated with a given column's aggregation.
892
+ *
893
+ * This method searches through the store's filters to find an aggregation
894
+ * that matches the specified column. If a matching aggregation is found,
895
+ * its associated icon is returned. If no matching aggregation is found in
896
+ * the store's filters, the method falls back to searching in the
897
+ * customizationJson's filters.
898
+ *
899
+ * @param column - The name of the column for which to retrieve the aggregation icon.
900
+ * @returns The icon associated with the specified column's aggregation, or undefined if no matching aggregation is found.
901
+ */
902
+ getAggregationIcon(column) {
903
+ const predicate = (aggregation) => aggregation.column === column;
904
+ if (store.filters().length > 0) {
905
+ return store.filters().find(predicate)?.icon;
906
+ }
907
+ // fallback to customizationJson
908
+ return store.customizationJson().filters?.find(predicate)?.icon;
909
+ },
910
+ /**
911
+ * Retrieves the customization items for a given column from the store's filters.
912
+ * If no items are found in the store's filters, it falls back to the customization JSON.
913
+ *
914
+ * @param column - The name of the column for which to retrieve customization items.
915
+ * @returns An array of `CFilterItem` objects if found, otherwise `undefined`.
916
+ */
917
+ getAggregationItemsCustomization(column) {
918
+ const predicate = (aggregation) => aggregation.column === column;
919
+ if (store.filters().length > 0) {
920
+ return store.filters().find(predicate)?.items || [];
921
+ }
922
+ // fallback to customizationJson
923
+ return store.customizationJson().filters?.find(predicate)?.items || [];
924
+ },
925
+ /**
926
+ * Retrieves the customization for a specific aggregation column.
927
+ * @param column - The column name for which to retrieve the customization.
928
+ * @returns The customization object for the specified column, or undefined if not found.
929
+ */
930
+ getAggregationCustomization(column) {
931
+ const predicate = (aggregation) => aggregation.column === column;
932
+ if (store.filters().length > 0) {
933
+ return store.filters().find(predicate);
934
+ }
935
+ // fallback to customizationJson
936
+ return store.customizationJson().filters?.find(predicate);
937
+ },
938
+ })));
939
+ }
940
+
941
+ const ApplicationStore = signalStore(
942
+ // providing store at the root level
943
+ { providedIn: 'root', protectedState: false }, withDevtools('Application'), withApplicationFeatures(), withExtractsFeatures());
944
+ /**
945
+ * Enhances the application store with additional features.
946
+ *
947
+ * This function integrates several features into the application store, including state management,
948
+ * computed properties, and various methods for updating and retrieving application state.
949
+ *
950
+ * Features included:
951
+ * - State initialization with `intialState`.
952
+ * - Methods for updating the application state:
953
+ * - `updateAssistantReady`: Marks the assistant as ready.
954
+ * - `updateReadyState`: Sets the application store's ready state to true.
955
+ *
956
+ * @returns A configured signal store feature with state, computed properties, and methods.
957
+ */
958
+ function withApplicationFeatures() {
959
+ return signalStoreFeature(withState({ assistantReady: false, ready: false, hasLabelsAccess: false }), withMethods((store) => ({
960
+ /**
961
+ * Updates the application state to indicate that the assistant is ready.
962
+ * This function patches the current state by setting the `assistantReady` property to `true`.
963
+ */
964
+ updateAssistantReady() {
965
+ patchState(store, (state) => ({ ...state, assistantReady: true }));
966
+ },
967
+ /**
968
+ * Updates the ready state of the application store to true.
969
+ * This function patches the current state of the store by setting the `ready` property to `true`.
970
+ */
971
+ updateReadyState(value = true) {
972
+ patchState(store, (state) => ({ ...state, ready: value }));
973
+ },
974
+ /**
975
+ * Updates the `hasLabelsAccess` property in the application store state.
976
+ *
977
+ * @param value - A boolean value indicating whether the user has access to labels. Defaults to `true`.
978
+ */
979
+ updateHasLabelsAccess(value = true) {
980
+ patchState(store, (state) => ({ ...state, hasLabelsAccess: value }));
981
+ }
982
+ })));
983
+ }
984
+ /**
985
+ * Provides features related to extracts within the application store.
986
+ *
987
+ * This function integrates state management, computed properties, and methods
988
+ * for handling extracts in the application store.
989
+ *
990
+ * Features:
991
+ * - State management for extracts.
992
+ * - Computed property to get the count of extracts.
993
+ * - Methods to update and retrieve extracts.
994
+ *
995
+ * @returns A configured signal store feature with state, computed properties, and methods for extracts.
996
+ */
997
+ function withExtractsFeatures() {
998
+ return signalStoreFeature(withState({ extracts: new Map() }), withComputed(({ extracts }) => ({
999
+ extractsCount: computed(() => extracts().size),
1000
+ })), withMethods((store) => ({
1001
+ /**
1002
+ * Updates the extracts for a given ID in the application store.
1003
+ *
1004
+ * @param id - The unique identifier for the extracts to be updated.
1005
+ * @param extracts - An array of Extract objects to be associated with the given ID.
1006
+ */
1007
+ updateExtracts(id, extracts) {
1008
+ patchState(store, (state) => {
1009
+ state.extracts.set(id, extracts);
1010
+ return { ...state };
1011
+ });
1012
+ },
1013
+ /**
1014
+ * Retrieves extracts from the store based on the provided ID.
1015
+ *
1016
+ * @param id - The unique identifier for the extracts to retrieve.
1017
+ * @returns The extracts associated with the given ID.
1018
+ */
1019
+ getExtracts(id) {
1020
+ return store.extracts().get(id);
1021
+ }
1022
+ })));
1023
+ }
1024
+
1025
+ class PrincipalService {
1026
+ constructor() {
1027
+ this.http = inject(HttpClient);
1028
+ this.API_URL = `${globalConfig.backendUrl}/${globalConfig.apiPath}`;
1029
+ }
1030
+ /**
1031
+ * Retrieves the principal information from the server.
1032
+ *
1033
+ * @returns Observable<Principal> An observable that emits the principal information.
1034
+ *
1035
+ * @remarks
1036
+ * This method sends a GET request to the API endpoint to fetch the principal data.
1037
+ * It includes query parameters to specify the action and to indicate that no authentication is required.
1038
+ * In case of an error, it logs the error to the console and returns an empty observable.
1039
+ *
1040
+ * @example
1041
+ * ```typescript
1042
+ * principalService.getPrincipal().subscribe(principal => {
1043
+ * console.log(principal);
1044
+ * });
1045
+ * ```
1046
+ */
1047
+ getPrincipal() {
1048
+ const params = new HttpParams().set("action", "get");
1049
+ params.append("noAuthentication", "true");
1050
+ return this.http.get(this.API_URL + "/principal", { params })
1051
+ .pipe(catchError(error => {
1052
+ console.error("PrincipalService.getPrincipal failure - error: ", error);
1053
+ return EMPTY;
1054
+ }));
1055
+ }
1056
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: PrincipalService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1057
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: PrincipalService, providedIn: 'root' }); }
1058
+ }
1059
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: PrincipalService, decorators: [{
1060
+ type: Injectable,
1061
+ args: [{
1062
+ providedIn: 'root'
1063
+ }]
1064
+ }] });
1065
+
1066
+ const PrincipalStore = signalStore({ providedIn: 'root' }, withDevtools('Principal'), withState({ principal: {}, userOverrideActive: false }), withComputed(({ principal, userOverrideActive }) => ({
1067
+ allowUserOverride: computed(() => principal().isAdministrator && userOverrideActive() === false),
1068
+ isOverridingUser: computed(() => userOverrideActive() === true)
1069
+ })), withPrincipalFeatures());
1070
+ /**
1071
+ * Enhances the store with principal-related features.
1072
+ *
1073
+ * This function integrates state, methods, and computed properties related to the principal.
1074
+ *
1075
+ * @returns A feature that can be added to a signal store.
1076
+ *
1077
+ * @feature
1078
+ * - State:
1079
+ * - `principal`: An object representing the principal.
1080
+ * - `userOverrideActive`: A boolean indicating if user override is active.
1081
+ *
1082
+ * - Methods:
1083
+ * - `initialize()`: Initializes the principal state by fetching the principal data from the `PrincipalService`.
1084
+ *
1085
+ * - Computed Properties:
1086
+ * - `allowUserOverride`: A computed boolean indicating if user override is allowed based on the principal's administrator status and the `userOverrideActive` state.
1087
+ * - `isOverridingUser`: A computed boolean indicating if the user override is currently active.
1088
+ */
1089
+ function withPrincipalFeatures() {
1090
+ return signalStoreFeature(withMethods((store, principalService = inject(PrincipalService)) => ({
1091
+ /**
1092
+ * Initializes the principal store by fetching the principal data from the principal service.
1093
+ * It patches the store with the fetched principal data and a user override active flag from the global configuration.
1094
+ *
1095
+ * @returns A promise that resolves when the principal data has been fetched and the store has been patched.
1096
+ */
1097
+ initialize() {
1098
+ return firstValueFrom(principalService.getPrincipal()
1099
+ .pipe(map(principal => {
1100
+ const { userOverrideActive = false } = globalConfig;
1101
+ patchState(store, { principal, userOverrideActive });
1102
+ })));
1103
+ }
1104
+ })));
1105
+ }
1106
+
1107
+ class InlineWorker {
1108
+ constructor(func) {
1109
+ this.onMessage = new Subject();
1110
+ this.onError = new Subject();
1111
+ const WORKER_ENABLED = !!(Worker);
1112
+ if (WORKER_ENABLED) {
1113
+ const functionBody = func.toString().replace(/^[^{]*{\s*/, '').replace(/\s*}[^}]*$/, '');
1114
+ try {
1115
+ this.worker = new Worker(URL.createObjectURL(new Blob([functionBody], { type: 'text/javascript' })));
1116
+ this.worker.onmessage = (data) => {
1117
+ this.onMessage.next(data);
1118
+ };
1119
+ this.worker.onerror = (data) => {
1120
+ this.onError.next(data);
1121
+ };
1122
+ }
1123
+ catch (e) {
1124
+ throw new Error('WebWorker is not enabled');
1125
+ }
1126
+ }
1127
+ else {
1128
+ throw new Error('WebWorker is not enabled');
1129
+ }
1130
+ }
1131
+ postMessage(data) {
1132
+ this.worker.postMessage(data);
1133
+ }
1134
+ onmessage() {
1135
+ return this.onMessage.asObservable();
1136
+ }
1137
+ onerror() {
1138
+ return this.onError.asObservable();
1139
+ }
1140
+ terminate() {
1141
+ if (this.worker)
1142
+ this.worker.terminate();
1143
+ }
1144
+ }
1145
+
1146
+ /**
1147
+ * Retrieves the query name from the current route.
1148
+ *
1149
+ * @remarks
1150
+ * This function must be used within an injection context.
1151
+ *
1152
+ * This function traverses the route tree recursively to find the query name.
1153
+ * It first checks for a query parameter `t` in the route's snapshot. If the query name is not found
1154
+ * in the route data, it attempts to find it in the child routes of the "search" route.
1155
+ * If the query name is still not found, it falls back to the `queryName` defined in the route's data.
1156
+ *
1157
+ * @returns {string | undefined} The query name if found, otherwise `undefined`.
1158
+ */
1159
+ function getQueryNameFromRoute() {
1160
+ assertInInjectionContext(getQueryNameFromRoute);
1161
+ const route = inject(ActivatedRoute);
1162
+ const routes = inject(Router);
1163
+ const recursive = (route) => {
1164
+ if (route?.firstChild)
1165
+ return recursive(route.firstChild);
1166
+ // Get query name from route data
1167
+ const { t } = route.snapshot.queryParams;
1168
+ // ! This is a workaround for the case where the queryName is not defined in the route data
1169
+ // "search" is the parent route of the search results, and "t" is the query parameter for the tab
1170
+ const { queryName } = routes.config.filter(p => p.path === "search")[0].children?.find(route => route.path === t)?.data || {};
1171
+ if (queryName)
1172
+ return queryName;
1173
+ // fallback to queryName from route data
1174
+ if (route?.snapshot.data['queryName'])
1175
+ return route.snapshot.data['queryName'];
1176
+ return undefined;
1177
+ };
1178
+ return recursive(route);
1179
+ }
1180
+ /**
1181
+ * Builds a query object based on the provided partial query.
1182
+ * If any properties are missing in the partial query, default values are used.
1183
+ *
1184
+ * @remarks
1185
+ * This function must be used within an injection context.
1186
+ *
1187
+ * @param query - The partial query object.
1188
+ * @returns The complete query object.
1189
+ */
1190
+ function buildQuery(query) {
1191
+ assertInInjectionContext(buildQuery);
1192
+ const name = query?.name || getQueryNameFromRoute();
1193
+ const params = getQueryParamsFromUrl(window.location.toString());
1194
+ return {
1195
+ ...params, // default values retrieved from the URL
1196
+ ...query, // override with query
1197
+ name, // override with query name
1198
+ };
1199
+ }
1200
+
1201
+ /**
1202
+ * Retrieves the current path from the Angular `ActivatedRoute` service.
1203
+ *
1204
+ * @returns {string | undefined} The current path as a string, or `undefined` if the path cannot be determined.
1205
+ */
1206
+ function getCurrentPath() {
1207
+ assertInInjectionContext(getCurrentPath);
1208
+ const route = inject(ActivatedRoute);
1209
+ return route?.snapshot.url.toString();
1210
+ }
1211
+ /**
1212
+ * Retrieves the current query name based on the current path and router configuration.
1213
+ *
1214
+ * This function asserts that it is being called within an injection context and then
1215
+ * uses Angular's dependency injection to get the router configuration. It matches the
1216
+ * current path against the router's configuration to find the corresponding query name.
1217
+ *
1218
+ * @returns {string | undefined} The name of the current query if found, otherwise undefined.
1219
+ */
1220
+ function getCurrentQueryName() {
1221
+ assertInInjectionContext(getCurrentQueryName);
1222
+ const routes = inject(Router).config;
1223
+ const path = getCurrentPath();
1224
+ return routes.find(r => r.path === 'search')?.children?.find(r => r.path === path)?.data?.['queryName'];
1225
+ }
1226
+
1227
+ function withThemeBodyHook(app, params) {
1228
+ runInInjectionContext(app.injector, () => {
1229
+ const themeStore = inject(ThemeStore);
1230
+ const { scope, theme } = params || { scope: 'application', theme: 'Default' };
1231
+ themeStore.setCurrentTheme(scope, theme);
1232
+ applyThemeToNativeElement(window.document.body, themeStore.scopes()?.[scope]?.cssVars?.light);
1233
+ effect(() => {
1234
+ const bodyScope = themeStore.scopes()?.[scope];
1235
+ const vars = bodyScope.darkMode ? bodyScope.cssVars.dark : bodyScope.cssVars.light;
1236
+ if (vars)
1237
+ applyThemeToNativeElement(window.document.body, vars);
1238
+ });
1239
+ });
1240
+ return app;
1241
+ }
1242
+
1243
+ function withThemes(app, themes) {
1244
+ themes.forEach(theme => THEMES.push(theme));
1245
+ return app;
1246
+ }
1247
+
1248
+ /**
1249
+ * Creates a debounced signal that updates its value after a specified timeout.
1250
+ *
1251
+ * @template T - The type of the signal value.
1252
+ * @param Signal<T> input - The input signal whose value will be debounced.
1253
+ * @param number [timeOutMs=0] - The debounce timeout in milliseconds. Defaults to 0.
1254
+ * @returns Signal<T> - A new signal that updates its value after the specified debounce timeout.
1255
+ *
1256
+ * @example
1257
+ * ```ts
1258
+ * const input = signal('');
1259
+ * const debounced = debouncedSignal(input, 1000);
1260
+ *
1261
+ * contructor() {
1262
+ * effect(() => {
1263
+ * console.log(debounced());
1264
+ * // will log the input value after 1 second of inactivity.
1265
+ * });
1266
+ * }
1267
+ * ...
1268
+ * <input [ngModel]="input()" (ngModelChange)="input.set($event)">
1269
+ * ```
1270
+ */
1271
+ function debouncedSignal(input, timeOutMs = 0) {
1272
+ const debounceSignal = signal(input());
1273
+ let timeout;
1274
+ effect((onCleanUp) => {
1275
+ const value = input();
1276
+ clearTimeout(timeout);
1277
+ timeout = setTimeout(() => {
1278
+ debounceSignal.set(value);
1279
+ }, timeOutMs);
1280
+ onCleanUp(() => clearTimeout(timeout));
1281
+ });
1282
+ return debounceSignal;
1283
+ }
1284
+
1285
+ const QueryParamsStore = signalStore({ providedIn: 'root' }, withDevtools('QueryParams'), withQueryParamsFeatures());
1286
+ function withQueryParamsFeatures() {
1287
+ return signalStoreFeature(withState({}), withMethods((store, injector = inject(Injector)) => ({
1288
+ /**
1289
+ * Sets the state from the given URL by extracting query parameters and updating the store.
1290
+ *
1291
+ * @param url - The URL containing query parameters to set the state from.
1292
+ *
1293
+ * The function performs the following steps:
1294
+ * 1. Extracts the path from the URL.
1295
+ * 2. Parses the query parameters from the URL.
1296
+ * 3. Decodes and parses the filters from the query parameters.
1297
+ * 4. Converts the page parameter to a number if it exists.
1298
+ * 5. Updates the store state with the extracted and parsed values.
1299
+ */
1300
+ setFromUrl(url) {
1301
+ const path = url.split('?')[0];
1302
+ const { q: text, f, id, p, s: sort, c, t: tab } = queryParamsFromUrl(url);
1303
+ const filters = f ? JSON.parse(decodeURIComponent(f)) : [];
1304
+ const spellingCorrectionMode = c;
1305
+ let page;
1306
+ if (p) {
1307
+ page = parseInt(p, 10);
1308
+ }
1309
+ patchState(store, (state) => {
1310
+ return { ...state, path, text, filters, id, page, sort, spellingCorrectionMode, tab };
1311
+ });
1312
+ },
1313
+ /**
1314
+ * Adds a filter to the store's state.
1315
+ *
1316
+ * @param filter - The filter to be added to the state.
1317
+ *
1318
+ * The function calls the `updateFilter` method to add the filter to the state.
1319
+ */
1320
+ addFilter(filter) {
1321
+ this.updateFilter(filter);
1322
+ },
1323
+ /**
1324
+ * Updates the filter in the store's state.
1325
+ *
1326
+ * @param filter - The filter to be updated. If the filter is `undefined`, the state remains unchanged.
1327
+ *
1328
+ * The function performs the following operations:
1329
+ * - Adds the filter to the state if it doesn't already exist and has values.
1330
+ * - Removes the filter if the 'between' operator is selected but no values are provided.
1331
+ * - Removes the filter if no values are selected and the operator is not 'between'.
1332
+ * - Updates the filter values if the filter already exists.
1333
+ *
1334
+ * @remarks
1335
+ * - If the filter is `undefined`, the state remains unchanged.
1336
+ * - If the filter's operator is 'between' and both `start` and `end` are `undefined`, the filter is removed.
1337
+ * - If the filter's operator is not 'between' and `filters`, `value`, and `values` are all `undefined`, the filter is removed.
1338
+ * - If the filter already exists, its values are updated.
1339
+ */
1340
+ updateFilter(filter) {
1341
+ patchState(store, (state) => {
1342
+ if (filter === undefined)
1343
+ return state;
1344
+ const allFilters = state.filters || [];
1345
+ const existing = allFilters.findIndex((f) => f.field === filter.field);
1346
+ // Add filter if it doesn't exist and has values
1347
+ if (existing === -1) {
1348
+ return ({ ...state, filters: [...allFilters || [], filter] });
1349
+ }
1350
+ if (existing >= 0) {
1351
+ // remove filter if between operator is selected but no values are selected
1352
+ if (filter.operator === 'between' && filter.start === undefined && filter.end === undefined) {
1353
+ return ({ ...state, filters: allFilters.filter(f => f.field !== filter.field) });
1354
+ }
1355
+ // Remove filter if no values are selected
1356
+ if (filter.operator !== 'between' && filter.filters === undefined && filter.value === undefined && filter.values === undefined) {
1357
+ return ({ ...state, filters: (allFilters || []).filter(f => f?.field !== filter.field) });
1358
+ }
1359
+ // Update filter values
1360
+ if (existing >= 0) {
1361
+ const filters = allFilters.toSpliced(existing, 1, filter);
1362
+ return ({ ...state, filters });
1363
+ }
1364
+ }
1365
+ return state;
1366
+ });
1367
+ },
1368
+ /**
1369
+ * Removes a filter from the store based on the specified field.
1370
+ *
1371
+ * @param field - The field of the filter to be removed.
1372
+ */
1373
+ removeFilter(field) {
1374
+ if (!field)
1375
+ return;
1376
+ patchState(store, (state) => {
1377
+ const filters = state.filters?.filter(f => f.field !== field);
1378
+ return { ...state, filters };
1379
+ });
1380
+ },
1381
+ /**
1382
+ * Removes a filter from the state by its name.
1383
+ *
1384
+ * @param name - The name of the filter to be removed.
1385
+ */
1386
+ removeFilterByName(name) {
1387
+ if (!name)
1388
+ return;
1389
+ patchState(store, (state) => {
1390
+ const filters = state.filters?.filter(f => f.name !== name);
1391
+ return { ...state, filters };
1392
+ });
1393
+ },
1394
+ /**
1395
+ * Clears all filters from the state.
1396
+ *
1397
+ * This method updates the state by setting the `filters` property to an empty array.
1398
+ * It uses the `patchState` function to apply the state change.
1399
+ */
1400
+ clearFilters() {
1401
+ patchState(store, (state) => {
1402
+ return { ...state, filters: [] };
1403
+ });
1404
+ },
1405
+ /**
1406
+ * Updates the current state with the provided query parameters.
1407
+ *
1408
+ * @param params - A partial object containing the query parameters to be updated.
1409
+ */
1410
+ patch(params) {
1411
+ patchState(store, (state) => {
1412
+ return { ...state, ...params };
1413
+ });
1414
+ },
1415
+ /**
1416
+ * Retrieves a filter object based on the provided field or name.
1417
+ *
1418
+ * @param {string} fieldOrName - The field or name to search for in the filters.
1419
+ * @returns {Partial<LegacyFilter & {count: number}> | null} - The filter object with an additional count property, or null if not found.
1420
+ *
1421
+ * The method performs the following steps:
1422
+ * 1. Checks if the `fieldOrName` parameter is provided. If not, returns null.
1423
+ * 2. Searches for a filter with a matching field in the store's filters.
1424
+ * 3. If no filter is found by field, searches for a filter with a matching name.
1425
+ * 4. If a filter is found and has a `value` property, returns the filter with a count of 1.
1426
+ * 5. If a filter is found and does not have an array of filters, returns the filter with a count of 1.
1427
+ * 6. If a filter is found and has a `values` property, returns the filter with the count set to the length of the `values` array.
1428
+ * 7. If a filter is found and has an array of filters, returns the filter with the count set to the length of the `filters` array.
1429
+ * 8. If none of the above conditions are met, returns the filter with a count of 0.
1430
+ */
1431
+ getFilter(fieldOrName) {
1432
+ if (!fieldOrName)
1433
+ return null;
1434
+ let filter = getState(store).filters?.find(f => f?.field === fieldOrName);
1435
+ // if not found, return checks for the filter name
1436
+ if (!filter)
1437
+ filter = getState(store).filters?.find(f => f?.name === fieldOrName);
1438
+ // if filter is found, return null
1439
+ if (!filter)
1440
+ return null;
1441
+ if (filter.operator && filter.operator === 'or') {
1442
+ const count = Array.isArray(filter.filters) ? filter.filters.length : 1;
1443
+ return ({ ...filter, count });
1444
+ }
1445
+ if (filter.operator && filter.operator === 'and') {
1446
+ return ({ ...filter, count: 1 });
1447
+ }
1448
+ if (filter && filter.value)
1449
+ return ({ ...filter, count: 1 });
1450
+ if (filter && filter.values)
1451
+ return ({ ...filter, count: filter.values.length });
1452
+ if (filter && Array.isArray(filter.filters) === false)
1453
+ return ({ ...filter, count: 1 });
1454
+ if (filter && Array.isArray(filter.filters))
1455
+ return ({ ...filter, count: filter.filters.length });
1456
+ return ({ ...filter, count: 0 });
1457
+ },
1458
+ /**
1459
+ * Constructs and returns a query object based on the current state of the store.
1460
+ *
1461
+ * This method retrieves various parameters from the store's state, such as filters, sort order, tab, query text, query name, and spelling correction mode.
1462
+ * It processes the filters to separate out "concepts" filters and incorporates them into the query text.
1463
+ * The remaining filters are combined appropriately.
1464
+ * Finally, it builds and returns the query object using the processed parameters.
1465
+ *
1466
+ * @returns Query The constructed query object.
1467
+ */
1468
+ getQuery() {
1469
+ const { filters: allFilters = [], sort, tab, text: queryText = '', name, spellingCorrectionMode } = getState(store);
1470
+ let text = queryText;
1471
+ // remove concepts filters from the query to add them in the query expression
1472
+ const [conceptsFilters] = allFilters.filter(f => f.field === "concepts");
1473
+ if (conceptsFilters) {
1474
+ const concepts = conceptsFilters.filters ? conceptsFilters.filters.map(f => f.value) : [conceptsFilters.value];
1475
+ text = addConcepts(queryText, concepts);
1476
+ }
1477
+ const filters = allFilters.filter(f => f.field !== "concepts");
1478
+ const f = filters.length > 1
1479
+ ? { operator: "and", filters }
1480
+ : filters[0];
1481
+ const query = runInInjectionContext(injector, () => buildQuery({ filters: f, name, sort, spellingCorrectionMode, tab, text }));
1482
+ return query;
1483
+ }
1484
+ })));
1485
+ }
1486
+
1487
+ /**
1488
+ * @constant
1489
+ * @name SelectionStore
1490
+ * @description
1491
+ * A store that manages the selection state of articles. It is provided in the root of the application and includes
1492
+ * development tools for easier debugging. The store maintains the state with the following properties:
1493
+ * - `article`: The selected article.
1494
+ * - `id`: The ID of the selected article.
1495
+ * - `queryText`: The query text associated with the selection.
1496
+ *
1497
+ * @methods
1498
+ * - `update(article: Article, queryText?: string)`: Updates the store with a new article and optional query text.
1499
+ * - `updateQueryText(queryText: string)`: Updates the query text in the store.
1500
+ * - `clear()`: Clears the selection state, setting `article`, `id`, and `queryText` to `undefined`.
1501
+ */
1502
+ const SelectionStore = signalStore({ providedIn: 'root' }, withDevtools('Selection'), withSelectionFeatures());
1503
+ function withSelectionFeatures() {
1504
+ return signalStoreFeature(withState({}), withMethods((store) => ({
1505
+ /**
1506
+ * Updates the state with the provided article and optional query text.
1507
+ *
1508
+ * @param article - The article object to update the state with.
1509
+ * @param queryText - Optional query text to update the state with.
1510
+ */
1511
+ update(newState) {
1512
+ patchState(store, (state) => {
1513
+ return { ...state, ...newState };
1514
+ });
1515
+ },
1516
+ /**
1517
+ * Clears the current selection state by setting the `article`, `id`, and `queryText` properties to `undefined`.
1518
+ *
1519
+ * @remarks
1520
+ * This method uses the `patchState` function to update the state of the store.
1521
+ */
1522
+ clear() {
1523
+ patchState(store, () => {
1524
+ return ({ article: undefined, id: undefined, queryText: undefined, previewHighlights: undefined });
1525
+ });
1526
+ }
1527
+ })));
1528
+ }
1529
+
1530
+ const ThemeStore = signalStore({ providedIn: 'root' }, withDevtools('Theme'), withThemesFeatures());
1531
+ function withThemesFeatures() {
1532
+ return signalStoreFeature(withState({ scopes: {} }), withMethods((store) => ({
1533
+ loadDefaultTheme(scope, darkMode) {
1534
+ this.setCurrentTheme(scope, 'Default', darkMode);
1535
+ },
1536
+ /**
1537
+ * Sets the current theme for a given scope.
1538
+ *
1539
+ * @param {string} scope - The scope for which the theme is being set.
1540
+ * @param {string} themeName - The name of the theme to be applied.
1541
+ * @param {boolean} [darkMode] - Optional. Specifies whether dark mode should be enabled. If not provided, it defaults to the current scope's dark mode setting or false.
1542
+ * @returns {void}
1543
+ */
1544
+ setCurrentTheme(scope, themeName, darkMode) {
1545
+ const cssVars = processCssVars(themeName);
1546
+ const currentScope = store.scopes()[scope];
1547
+ patchState(store, (state => ({
1548
+ ...state,
1549
+ scopes: {
1550
+ ...store.scopes(),
1551
+ [scope]: {
1552
+ cssVars,
1553
+ darkMode: darkMode ?? currentScope?.darkMode ?? false,
1554
+ themeName: themeName
1555
+ }
1556
+ }
1557
+ })));
1558
+ },
1559
+ /**
1560
+ * Sets the dark mode preference for a given scope.
1561
+ *
1562
+ * @param scope - The scope for which to set the dark mode preference.
1563
+ * @param darkMode - A boolean indicating whether dark mode should be enabled (true) or disabled (false).
1564
+ *
1565
+ * This function updates the state by patching it with the new dark mode setting for the specified scope.
1566
+ * It retains the existing CSS variables and theme name for the scope.
1567
+ */
1568
+ setDarkMode(scope, darkMode) {
1569
+ const currentScope = store.scopes()[scope];
1570
+ patchState(store, (state => ({
1571
+ ...state,
1572
+ scopes: {
1573
+ ...store.scopes(),
1574
+ [scope]: {
1575
+ cssVars: currentScope?.cssVars ?? {},
1576
+ darkMode,
1577
+ themeName: currentScope?.themeName
1578
+ }
1579
+ }
1580
+ })));
1581
+ },
1582
+ })));
1583
+ }
1584
+ /**
1585
+ * Processes the CSS variables for a given theme name.
1586
+ *
1587
+ * @param themeName - The name of the theme to process.
1588
+ * @returns An object containing the CSS variables for both light and dark themes.
1589
+ * @throws Will throw an error if the theme with the specified name is not found.
1590
+ */
1591
+ function processCssVars(themeName) {
1592
+ const theme = THEMES.find(theme => theme.name === themeName);
1593
+ if (!theme)
1594
+ throw new Error(`Theme "${themeName}" not found`);
1595
+ const cssVars = {
1596
+ light: themeColorsToCssVariables(theme.colors),
1597
+ dark: themeColorsToCssVariables(theme.colorsDark)
1598
+ };
1599
+ return cssVars;
1600
+ }
1601
+ /**
1602
+ * Converts an object of theme colors to CSS variables.
1603
+ *
1604
+ * @param colors - An object where keys are theme color names and values are the corresponding color values.
1605
+ * @returns An object where keys are CSS variable names and values are the corresponding color values.
1606
+ */
1607
+ function themeColorsToCssVariables(colors) {
1608
+ const cssVars = colors
1609
+ ? Object.fromEntries(Object.entries(colors).map(([name, value]) => {
1610
+ if (value === undefined)
1611
+ return [];
1612
+ const cssName = themeColorNameToCssVariable(name);
1613
+ return [cssName, value];
1614
+ }))
1615
+ : {};
1616
+ return cssVars;
1617
+ }
1618
+ /**
1619
+ * Converts a camelCase theme color name to a CSS variable format.
1620
+ *
1621
+ * This function takes a camelCase string and transforms it into a CSS variable
1622
+ * name by inserting hyphens between lowercase and uppercase letters and converting
1623
+ * the entire string to lowercase.
1624
+ *
1625
+ * @param name - The camelCase theme color name to be converted.
1626
+ * @returns The corresponding CSS variable name in kebab-case.
1627
+ */
1628
+ function themeColorNameToCssVariable(name) {
1629
+ return `--${name.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase()}`;
1630
+ }
1631
+ /**
1632
+ * Applies a set of CSS variables to a given HTML element.
1633
+ *
1634
+ * @param element - The HTML element to which the CSS variables will be applied.
1635
+ * @param cssVars - An optional record of CSS variable names and their corresponding values.
1636
+ */
1637
+ function applyThemeToNativeElement(element, cssVars) {
1638
+ if (!cssVars)
1639
+ return;
1640
+ Object.keys(cssVars).forEach((key) => element.style.setProperty(key, cssVars[key]));
1641
+ }
1642
+
1643
+ const UserSettingsStore = signalStore({ providedIn: 'root' }, withDevtools('UserSettings'), withBookmarkFeatures(), withRecentSearchFeatures(), withSavedSearchFeatures(), withBasketsFeatures(), withAssistantFeatures(), withUserSettingsFeatures());
1644
+ function withUserSettingsFeatures() {
1645
+ return signalStoreFeature(withState({ language: undefined }), withMethods((store) => ({
1646
+ /**
1647
+ * Initializes the user settings store by fetching the user settings from the backend API
1648
+ * and patching the store with the retrieved settings.
1649
+ *
1650
+ * @returns {Promise<void>} A promise that resolves when the initialization is complete.
1651
+ */
1652
+ async initialize() {
1653
+ // Fetch the user settings from the backend API and patch the store with the result
1654
+ const settings = await fetchUserSettings();
1655
+ patchState(store, settings);
1656
+ },
1657
+ /**
1658
+ * Resets the user settings to the initial state.
1659
+ *
1660
+ * This method performs the following actions:
1661
+ * 1. Deletes the current user settings.
1662
+ * 2. Patches the state with the initial state.
1663
+ *
1664
+ * @returns {Promise<void>} A promise that resolves when the reset operation is complete.
1665
+ */
1666
+ async reset() {
1667
+ // Reset the user settings to the initial state
1668
+ await deleteUserSettings();
1669
+ patchState(store, {
1670
+ bookmarks: [],
1671
+ recentSearches: [],
1672
+ savedSearches: [],
1673
+ baskets: [],
1674
+ assistants: {},
1675
+ language: undefined,
1676
+ });
1677
+ },
1678
+ })));
1679
+ }
1680
+ function withBookmarkFeatures() {
1681
+ return signalStoreFeature(withState({ bookmarks: [] }), withMethods((store) => ({
1682
+ /**
1683
+ * Updates the user's bookmarks in the store and optionally logs audit events.
1684
+ *
1685
+ * @param bookmarks - An array of bookmarks to update in the user settings.
1686
+ * @param auditEvents - Optional audit events to log during the update.
1687
+ * @returns A promise that resolves when the update is complete.
1688
+ */
1689
+ async updateBookmarks(bookmarks, auditEvents) {
1690
+ await patchUserSettings({ bookmarks }, auditEvents);
1691
+ patchState(store, (state) => {
1692
+ return { ...state, bookmarks: [...bookmarks] };
1693
+ });
1694
+ },
1695
+ /**
1696
+ * Adds an article to the bookmarks if it is not already bookmarked.
1697
+ *
1698
+ * @param article - The article to be bookmarked.
1699
+ * @param queryName - Optional name of the query associated with the bookmark.
1700
+ * @returns A promise that resolves when the bookmark has been added.
1701
+ */
1702
+ async bookmark(article, queryName) {
1703
+ if (!article.id)
1704
+ return;
1705
+ if (this.isBookmarked(article))
1706
+ return;
1707
+ const bookmark = {
1708
+ id: article.id,
1709
+ label: article.title,
1710
+ source: article.collection[0],
1711
+ author: article.authors?.[0],
1712
+ queryName,
1713
+ };
1714
+ await this.updateBookmarks([...store.bookmarks(), bookmark], {
1715
+ type: "Basket_AddDoc",
1716
+ detail: {
1717
+ docid: article.id
1718
+ }
1719
+ });
1720
+ },
1721
+ /**
1722
+ * Removes a bookmark by its ID.
1723
+ *
1724
+ * @param id - The ID of the bookmark to remove.
1725
+ * @returns A promise that resolves when the bookmark has been removed and the bookmarks have been updated.
1726
+ *
1727
+ * @remarks
1728
+ * - If the provided ID is falsy, the function will return immediately.
1729
+ * - If the bookmark with the given ID is not found, the function will return immediately.
1730
+ * - The function updates the bookmarks and logs the removal action with a type of "Basket_RemoveDoc".
1731
+ */
1732
+ async unbookmark(id) {
1733
+ if (!id)
1734
+ return;
1735
+ const bookmarks = store.bookmarks();
1736
+ const index = bookmarks?.findIndex((bookmark) => bookmark.id === id);
1737
+ if (index === -1)
1738
+ return;
1739
+ bookmarks.splice(index, 1);
1740
+ await this.updateBookmarks([...bookmarks], {
1741
+ type: "Basket_RemoveDoc",
1742
+ detail: {
1743
+ docid: id
1744
+ }
1745
+ });
1746
+ },
1747
+ /**
1748
+ * Checks if the given article is bookmarked.
1749
+ *
1750
+ * @param article - An optional partial article object to check.
1751
+ * @returns `true` if the article is bookmarked, `false` otherwise.
1752
+ */
1753
+ isBookmarked(article) {
1754
+ if (!article || !article.id)
1755
+ return false;
1756
+ const bookmarks = store.bookmarks();
1757
+ return !!bookmarks?.find((bookmark) => bookmark.id === article.id);
1758
+ },
1759
+ /**
1760
+ * Toggles the bookmark status of a given article.
1761
+ *
1762
+ * This method checks if the article is currently bookmarked. If it is,
1763
+ * it removes the bookmark. If it is not, it adds a bookmark.
1764
+ *
1765
+ * @param article - The article to toggle the bookmark status for.
1766
+ * @returns A promise that resolves when the bookmark status has been toggled.
1767
+ */
1768
+ async toggleBookmark(article) {
1769
+ const isBookmarked = await this.isBookmarked(article);
1770
+ if (isBookmarked)
1771
+ this.unbookmark(article.id);
1772
+ else
1773
+ this.bookmark(article);
1774
+ },
1775
+ })));
1776
+ }
1777
+ function withRecentSearchFeatures() {
1778
+ return signalStoreFeature(withState({ recentSearches: [] }), withMethods((store) => ({
1779
+ /**
1780
+ * Deletes a recent search entry from the user's recent searches list.
1781
+ *
1782
+ * @param {number} index - The index of the recent search to delete.
1783
+ * @returns {Promise<void>} A promise that resolves when the recent search has been deleted and the state has been updated.
1784
+ */
1785
+ async deleteRecentSearch(index) {
1786
+ const recentSearches = store.recentSearches();
1787
+ if (recentSearches) {
1788
+ const recentSearchToDelete = recentSearches.splice(index, 1)[0];
1789
+ await patchUserSettings({ recentSearches }, {
1790
+ type: "RecentQuery_Delete",
1791
+ detail: {
1792
+ recentQuery: recentSearchToDelete.queryParams?.text
1793
+ }
1794
+ });
1795
+ patchState(store, (state) => {
1796
+ return { ...state, recentSearches: [...recentSearches] };
1797
+ });
1798
+ }
1799
+ },
1800
+ /**
1801
+ * Updates the recent searches in the user settings.
1802
+ *
1803
+ * @param recentSearches - An array of recent searches to be updated in the user settings.
1804
+ * @param auditEvents - Optional audit events to be logged during the update.
1805
+ * @returns A promise that resolves when the recent searches have been updated.
1806
+ */
1807
+ async updateRecentSearches(recentSearches, auditEvents) {
1808
+ await patchUserSettings({ recentSearches }, auditEvents);
1809
+ patchState(store, (state) => {
1810
+ return { ...state, recentSearches: [...recentSearches] };
1811
+ });
1812
+ },
1813
+ /**
1814
+ * Adds the current search to the recent searches list.
1815
+ *
1816
+ * This method checks if a search with the same label already exists in the recent searches.
1817
+ * If it does, it updates the existing search with the new query parameters and date.
1818
+ * If it does not, it adds a new search to the recent searches list.
1819
+ *
1820
+ * @param queryParams - The parameters of the current search.
1821
+ * @returns A promise that resolves when the recent searches have been updated.
1822
+ */
1823
+ async addCurrentSearch(queryParams) {
1824
+ // fist search if the search is already in the recent searches with the same label
1825
+ const recentSearches = store.recentSearches();
1826
+ const existingSearch = recentSearches.find((search) => search.label === queryParams.text);
1827
+ const url = window.location.hash.substring(1);
1828
+ const path = url.split('?')[0];
1829
+ if (existingSearch) {
1830
+ await this.updateRecentSearches([
1831
+ {
1832
+ ...existingSearch,
1833
+ date: new Date().toISOString(),
1834
+ url,
1835
+ path,
1836
+ queryParams,
1837
+ filterCount: queryParams.filters?.length
1838
+ },
1839
+ ...recentSearches.filter((search) => search.label !== queryParams.text),
1840
+ ], {
1841
+ type: "RecentQuery_Update",
1842
+ detail: {
1843
+ recentQuery: queryParams.text
1844
+ }
1845
+ });
1846
+ }
1847
+ else {
1848
+ const recentSearch = {
1849
+ url,
1850
+ path,
1851
+ display: queryParams.text,
1852
+ label: queryParams.text,
1853
+ filterCount: queryParams.filters?.length,
1854
+ date: new Date().toISOString(),
1855
+ queryParams
1856
+ };
1857
+ await this.updateRecentSearches([recentSearch, ...store.recentSearches()], {
1858
+ type: "RecentQuery_Add",
1859
+ detail: {
1860
+ recentQuery: queryParams.text
1861
+ }
1862
+ });
1863
+ }
1864
+ },
1865
+ })));
1866
+ }
1867
+ function withSavedSearchFeatures() {
1868
+ return signalStoreFeature(withState({ savedSearches: [] }), withMethods((store) => ({
1869
+ async deleteSavedSearch(index) {
1870
+ const savedSearches = store.savedSearches();
1871
+ if (savedSearches) {
1872
+ const savedSearchToDelete = savedSearches.splice(index, 1)[0];
1873
+ await patchUserSettings({ savedSearches }, {
1874
+ type: "SavedQuery_Delete",
1875
+ detail: {
1876
+ savedQuery: savedSearchToDelete.display
1877
+ }
1878
+ });
1879
+ patchState(store, (state) => {
1880
+ return { ...state, savedSearches: [...savedSearches] };
1881
+ });
1882
+ }
1883
+ },
1884
+ async updateSavedSearches(savedSearches) {
1885
+ await patchUserSettings({ savedSearches }, {
1886
+ type: "SavedQuery_Add",
1887
+ detail: {
1888
+ savedQuery: savedSearches[0].display
1889
+ }
1890
+ });
1891
+ patchState(store, (state) => {
1892
+ return { ...state, savedSearches: [...savedSearches] };
1893
+ });
1894
+ },
1895
+ })));
1896
+ }
1897
+ function basketIndex(name, baskets) {
1898
+ if (baskets?.length) {
1899
+ for (let i = 0, ic = baskets.length; i < ic; i++) {
1900
+ const basket = baskets[i];
1901
+ if (basket && basket.name === name) {
1902
+ return i;
1903
+ }
1904
+ }
1905
+ }
1906
+ return -1;
1907
+ }
1908
+ function withBasketsFeatures() {
1909
+ return signalStoreFeature(withState({ baskets: [] }), withMethods((store) => ({
1910
+ async deleteBasket(index) {
1911
+ const baskets = store.baskets();
1912
+ if (baskets) {
1913
+ const basketToDelete = baskets.splice(index, 1)[0];
1914
+ await patchUserSettings({ baskets }, {
1915
+ type: "Basket_Delete",
1916
+ detail: {
1917
+ savedQuery: basketToDelete.name
1918
+ }
1919
+ });
1920
+ patchState(store, (state) => {
1921
+ return { ...state, baskets: [...baskets] };
1922
+ });
1923
+ }
1924
+ },
1925
+ async createBasket(basket) {
1926
+ const baskets = store.baskets();
1927
+ if (basketIndex(basket.name) >= 0)
1928
+ return;
1929
+ baskets.unshift(basket);
1930
+ await patchUserSettings({ baskets }, {
1931
+ type: "Basket_Add",
1932
+ detail: {
1933
+ basket: basket.name
1934
+ }
1935
+ });
1936
+ patchState(store, (state) => {
1937
+ return { ...state, baskets: [...baskets] };
1938
+ });
1939
+ },
1940
+ async updateBaskets(baskets) {
1941
+ await patchUserSettings({ baskets }, {
1942
+ type: "Basket_Update"
1943
+ });
1944
+ patchState(store, (state) => {
1945
+ return { ...state, baskets: [...baskets] };
1946
+ });
1947
+ },
1948
+ async updateBasket(basket, index) {
1949
+ const baskets = store.baskets();
1950
+ const prevIndex = basketIndex(basket.name, baskets);
1951
+ if ((prevIndex !== -1 && index !== prevIndex) // A basket with the same name exists at a different index
1952
+ || index < 0 || index >= baskets.length) // index out of bounds
1953
+ return;
1954
+ baskets.splice(index, 1, basket);
1955
+ await patchUserSettings({ baskets }, {
1956
+ type: "Basket_Update",
1957
+ detail: {
1958
+ basket: basket.name
1959
+ }
1960
+ });
1961
+ patchState(store, (state) => {
1962
+ return { ...state, baskets: [...baskets] };
1963
+ });
1964
+ },
1965
+ async addToBasket(name, ids) {
1966
+ const baskets = store.baskets();
1967
+ const basket = baskets.find(b => b.name === name);
1968
+ if (!ids || !basket)
1969
+ return;
1970
+ if (!basket.ids)
1971
+ basket.ids = [];
1972
+ ids = Array.isArray(ids) ? ids : [ids];
1973
+ for (let id of ids) {
1974
+ if (!basket.ids.some(i => i === id)) {
1975
+ basket.ids.push(id);
1976
+ }
1977
+ }
1978
+ await patchUserSettings({ baskets }, {
1979
+ type: "Basket_AddDoc",
1980
+ detail: {
1981
+ basket: basket.name,
1982
+ docid: ids[0]
1983
+ }
1984
+ });
1985
+ patchState(store, (state) => {
1986
+ return { ...state, baskets: [...baskets] };
1987
+ });
1988
+ },
1989
+ async removeFromBasket(name, ids) {
1990
+ const baskets = store.baskets();
1991
+ const basket = baskets.find(b => b.name === name);
1992
+ if (!ids || !basket)
1993
+ return;
1994
+ if (!basket.ids)
1995
+ basket.ids = [];
1996
+ ids = Array.isArray(ids) ? ids : [ids];
1997
+ for (let id of ids) {
1998
+ if (!basket.ids.some(i => i === id)) {
1999
+ basket.ids.push(id);
2000
+ }
2001
+ }
2002
+ await patchUserSettings({ baskets }, {
2003
+ type: "Basket_AddDoc",
2004
+ detail: {
2005
+ basket: basket.name,
2006
+ docid: ids[0]
2007
+ }
2008
+ });
2009
+ patchState(store, (state) => {
2010
+ return { ...state, baskets: [...baskets] };
2011
+ });
2012
+ },
2013
+ })));
2014
+ }
2015
+ function withAssistantFeatures() {
2016
+ return signalStoreFeature(withState({ assistants: {} }), withMethods((store) => ({
2017
+ async updateAssistantSettings(assistantSettings) {
2018
+ patchState(store, (state) => {
2019
+ return { ...state, assistants: assistantSettings };
2020
+ });
2021
+ },
2022
+ async updateLanguage(language, auditEvents) {
2023
+ await patchUserSettings({ language }, auditEvents);
2024
+ patchState(store, (state) => {
2025
+ return { ...state, language };
2026
+ });
2027
+ },
2028
+ })));
2029
+ }
2030
+
2031
+ class ThemeSelectorComponent {
2032
+ constructor() {
2033
+ this.scope = input.required();
2034
+ this.showPrivate = input(false);
2035
+ this.themes = computed(() => THEMES.filter(theme => this.showPrivate() ? true : !theme.private));
2036
+ this.selectedTheme = model();
2037
+ this.themeStore = inject(ThemeStore);
2038
+ effect(() => {
2039
+ const currentTheme = this.themeStore.scopes()[this.scope()];
2040
+ if (!currentTheme)
2041
+ return;
2042
+ this.selectedTheme.set(this.themeStore.scopes()[this.scope()].themeName);
2043
+ }, { allowSignalWrites: true });
2044
+ }
2045
+ selectTheme(theme) {
2046
+ if (typeof theme !== 'string')
2047
+ theme = theme.name;
2048
+ this.themeStore.setCurrentTheme(this.scope(), theme);
2049
+ }
2050
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: ThemeSelectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2051
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.5", type: ThemeSelectorComponent, isStandalone: true, selector: "theme-selector", inputs: { scope: { classPropertyName: "scope", publicName: "scope", isSignal: true, isRequired: true, transformFunction: null }, showPrivate: { classPropertyName: "showPrivate", publicName: "showPrivate", isSignal: true, isRequired: false, transformFunction: null }, selectedTheme: { classPropertyName: "selectedTheme", publicName: "selectedTheme", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedTheme: "selectedThemeChange" }, ngImport: i0, template: `
2052
+ <sq-menu>
2053
+ <ng-content></ng-content>
2054
+
2055
+ <div menu-content>
2056
+ <ul class="p-2 flex flex-col gap-1 divide-y">
2057
+ @for (theme of themes(); track $index) {
2058
+ <li class="p-2" (click)="selectTheme(theme)">
2059
+ <div class="flex gap-2 items-baseline">
2060
+ <span class="size-3 rounded-full" [ngStyle]="{ 'background-color': 'hsl(' + theme.colors.primary + ')' }" aria-hidden="true"></span>
2061
+ <span>{{ theme.name }}</span>
2062
+ </div>
2063
+ </li>
2064
+ }
2065
+ </ul>
2066
+ </div>
2067
+ </sq-menu>
2068
+ `, isInline: true, styles: [""], dependencies: [{ kind: "component", type: MenuComponent, selector: "sq-menu, Menu", inputs: ["position", "className", "disabled", "autoClose"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] }); }
2069
+ }
2070
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: ThemeSelectorComponent, decorators: [{
2071
+ type: Component,
2072
+ args: [{ selector: 'theme-selector', standalone: true, imports: [MenuComponent, NgStyle], template: `
2073
+ <sq-menu>
2074
+ <ng-content></ng-content>
2075
+
2076
+ <div menu-content>
2077
+ <ul class="p-2 flex flex-col gap-1 divide-y">
2078
+ @for (theme of themes(); track $index) {
2079
+ <li class="p-2" (click)="selectTheme(theme)">
2080
+ <div class="flex gap-2 items-baseline">
2081
+ <span class="size-3 rounded-full" [ngStyle]="{ 'background-color': 'hsl(' + theme.colors.primary + ')' }" aria-hidden="true"></span>
2082
+ <span>{{ theme.name }}</span>
2083
+ </div>
2084
+ </li>
2085
+ }
2086
+ </ul>
2087
+ </div>
2088
+ </sq-menu>
2089
+ ` }]
2090
+ }], ctorParameters: () => [] });
2091
+
2092
+ class ThemeToggleComponent {
2093
+ constructor() {
2094
+ this.scope = input.required();
2095
+ this.darkMode = model();
2096
+ this.themeStore = inject(ThemeStore);
2097
+ effect(() => {
2098
+ const currentTheme = this.themeStore.scopes()[this.scope()];
2099
+ if (!currentTheme)
2100
+ return;
2101
+ this.darkMode.set(currentTheme.darkMode ?? false);
2102
+ }, { allowSignalWrites: true });
2103
+ }
2104
+ toggleDarkMode(status) {
2105
+ const scope = this.themeStore.scopes()?.[this.scope()];
2106
+ if (!scope || !scope.cssVars)
2107
+ return;
2108
+ this.themeStore.setDarkMode(this.scope(), status);
2109
+ }
2110
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: ThemeToggleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2111
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.5", type: ThemeToggleComponent, isStandalone: true, selector: "theme-toggle", inputs: { scope: { classPropertyName: "scope", publicName: "scope", isSignal: true, isRequired: true, transformFunction: null }, darkMode: { classPropertyName: "darkMode", publicName: "darkMode", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { darkMode: "darkModeChange" }, ngImport: i0, template: `
2112
+ <label class="flex gap-2 items-center select-none cursor-pointer">
2113
+ @if (darkMode()) {
2114
+ <i class="fa-fw far fa-toggle-large-on text-primary"></i>
2115
+ }
2116
+ @else {
2117
+ <i class="fa-fw far fa-toggle-large-on fa-flip-horizontal"></i>
2118
+ }
2119
+
2120
+ <span>Dark mode</span>
2121
+
2122
+ <input
2123
+ type="checkbox"
2124
+ class="hidden"
2125
+ [(ngModel)]="darkMode"
2126
+ (ngModelChange)="toggleDarkMode($event)"
2127
+ />
2128
+ </label>
2129
+ `, isInline: true, styles: [""], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] }); }
2130
+ }
2131
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: ThemeToggleComponent, decorators: [{
2132
+ type: Component,
2133
+ args: [{ selector: 'theme-toggle', standalone: true, imports: [FormsModule], template: `
2134
+ <label class="flex gap-2 items-center select-none cursor-pointer">
2135
+ @if (darkMode()) {
2136
+ <i class="fa-fw far fa-toggle-large-on text-primary"></i>
2137
+ }
2138
+ @else {
2139
+ <i class="fa-fw far fa-toggle-large-on fa-flip-horizontal"></i>
2140
+ }
2141
+
2142
+ <span>Dark mode</span>
2143
+
2144
+ <input
2145
+ type="checkbox"
2146
+ class="hidden"
2147
+ [(ngModel)]="darkMode"
2148
+ (ngModelChange)="toggleDarkMode($event)"
2149
+ />
2150
+ </label>
2151
+ ` }]
2152
+ }], ctorParameters: () => [] });
2153
+
2154
+ /**
2155
+ * Represents a directive that enables infinite scrolling behavior.
2156
+ * This directive listens for the intersection of the element with the viewport
2157
+ * and emits a `loadMore` event when the element becomes visible.
2158
+ *
2159
+ * @remarks
2160
+ * This directive requires the `IntersectionObserver` API to be available in the browser.
2161
+ *
2162
+ * @example
2163
+ * ```html
2164
+ * <div infinityScroll (onScroll)="loadMore()">
2165
+ * <!-- Content to be scrolled -->
2166
+ * </div>
2167
+ * ```
2168
+ *
2169
+ */
2170
+ class InfinityScrollDirective {
2171
+ constructor(el) {
2172
+ this.el = el;
2173
+ this.options = input({ root: null });
2174
+ this.onScroll = output();
2175
+ this.observer = new IntersectionObserver(([entry]) => {
2176
+ if (entry.isIntersecting) {
2177
+ this.onScroll.emit();
2178
+ }
2179
+ }, this.options());
2180
+ afterNextRender(() => {
2181
+ this.observer.observe(this.el.nativeElement);
2182
+ });
2183
+ }
2184
+ ngOnDestroy() {
2185
+ this.observer.disconnect();
2186
+ }
2187
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: InfinityScrollDirective, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive }); }
2188
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "18.2.5", type: InfinityScrollDirective, isStandalone: true, selector: "[infinity-scroll]", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onScroll: "onScroll" }, ngImport: i0 }); }
2189
+ }
2190
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: InfinityScrollDirective, decorators: [{
2191
+ type: Directive,
2192
+ args: [{
2193
+ // eslint-disable-next-line @angular-eslint/directive-selector
2194
+ selector: '[infinity-scroll]',
2195
+ standalone: true
2196
+ }]
2197
+ }], ctorParameters: () => [{ type: i0.ElementRef }] });
2198
+
2199
+ class SelectionService {
2200
+ constructor() {
2201
+ this.route = inject(ActivatedRoute);
2202
+ this.router = inject(Router);
2203
+ this.location = inject(Location);
2204
+ this.selectionStore = inject(SelectionStore);
2205
+ this.queryParamsStore = inject(QueryParamsStore);
2206
+ }
2207
+ /**
2208
+ * Sets the current article in the selection store and optionally updates the query text.
2209
+ * If the provided article is undefined, it clears the current article.
2210
+ *
2211
+ * @param article - The article to set as the current article. If undefined, the current article is cleared.
2212
+ * @param withQueryText - A boolean indicating whether to update the query text in the selection store. Defaults to true.
2213
+ *
2214
+ * @returns void
2215
+ */
2216
+ setCurrentArticle(article, withQueryText = true) {
2217
+ if (!article) {
2218
+ this.clearCurrentArticle();
2219
+ return;
2220
+ }
2221
+ // update selection store with article and query text
2222
+ const { text } = getState(this.queryParamsStore);
2223
+ this.selectionStore.update({ article, id: article.id, queryText: withQueryText ? text : undefined });
2224
+ if (article?.id)
2225
+ this.updateArticleIdInQueryParams(article.id);
2226
+ }
2227
+ /**
2228
+ * Clears the current article selection from the selection store and
2229
+ * removes the article ID from the query parameters.
2230
+ *
2231
+ * @remarks
2232
+ * This method performs two main actions:
2233
+ * 1. Clears the current selection from the `selectionStore`.
2234
+ * 2. Removes the article ID from the query parameters to ensure
2235
+ * the URL does not reference the cleared article.
2236
+ *
2237
+ * @public
2238
+ */
2239
+ clearCurrentArticle() {
2240
+ this.selectionStore.clear();
2241
+ this.clearArticleIdFromQueryParams();
2242
+ }
2243
+ /**
2244
+ * Updates the article ID in the query parameters of the current route.
2245
+ * If the provided ID is undefined, it will remove the ID from the query parameters.
2246
+ *
2247
+ * @param id - The article ID to be set in the query parameters. If undefined, the ID will be removed.
2248
+ */
2249
+ updateArticleIdInQueryParams(id) {
2250
+ const url = this.router
2251
+ .createUrlTree([], { relativeTo: this.route, queryParams: { id }, queryParamsHandling: 'merge' })
2252
+ .toString();
2253
+ this.location.replaceState(url);
2254
+ }
2255
+ /**
2256
+ * Clears the 'id' parameter from the current route's query parameters.
2257
+ *
2258
+ * This method creates a copy of the current query parameters, removes the 'id' parameter,
2259
+ * and then updates the browser's URL to reflect the change without reloading the page.
2260
+ *
2261
+ * @private
2262
+ */
2263
+ clearArticleIdFromQueryParams() {
2264
+ const queryParams = Object.assign({}, this.route.snapshot.queryParams);
2265
+ delete queryParams['id'];
2266
+ const url = this.router.createUrlTree([], { relativeTo: this.route, queryParams }).toString();
2267
+ this.location.replaceState(url);
2268
+ }
2269
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: SelectionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2270
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: SelectionService, providedIn: 'root' }); }
2271
+ }
2272
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: SelectionService, decorators: [{
2273
+ type: Injectable,
2274
+ args: [{
2275
+ providedIn: 'root'
2276
+ }]
2277
+ }] });
2278
+
2279
+ /*
2280
+ * The `SelectionHistoryService` class is responsible for managing the selection history.
2281
+ *
2282
+ * It keeps track of the history of selected articles and provides methods to navigate through the history.
2283
+ * The service also emits events when the selection history changes.
2284
+ * This service is used by the Drawer
2285
+ */
2286
+ class SelectionHistoryService {
2287
+ constructor() {
2288
+ this.selectionHistoryEvent = new EventEmitter();
2289
+ this.selectionService = inject(SelectionService);
2290
+ this.selectionStore = inject(SelectionStore);
2291
+ this.history = [];
2292
+ effect(() => {
2293
+ const { article } = getState(this.selectionStore);
2294
+ if (!!article && article !== this.history[this.history.length - 1]) {
2295
+ this.history.push(article);
2296
+ this.selectionHistoryEvent.next('new');
2297
+ }
2298
+ });
2299
+ }
2300
+ /**
2301
+ * Retrieves the index of the current selection.
2302
+ *
2303
+ * @returns {number} The index of the current selection, which is the last element in the history array.
2304
+ */
2305
+ getCurrentSelectionIndex() {
2306
+ return this.history.length - 1;
2307
+ }
2308
+ /**
2309
+ * Retrieves an article from the selection history at the specified index.
2310
+ *
2311
+ * @param index - The index of the article to retrieve.
2312
+ * @returns The article at the specified index, or `undefined` if the index is out of bounds.
2313
+ */
2314
+ getSelection(index) {
2315
+ if (index < 0 || index >= this.history.length)
2316
+ return undefined;
2317
+ return this.history[index];
2318
+ }
2319
+ /**
2320
+ * Retrieves the length of the history array.
2321
+ *
2322
+ * @returns {number} The number of entries in the history.
2323
+ */
2324
+ getHistoryLength() {
2325
+ return this.history.length;
2326
+ }
2327
+ /**
2328
+ * Clears the selection history and resets the current article selection.
2329
+ *
2330
+ * This method performs the following actions:
2331
+ * - Empties the history array.
2332
+ * - Calls the `clearCurrentArticle` method on the `selectionService` to reset the current article selection.
2333
+ */
2334
+ clearHistory() {
2335
+ this.history.length = 0;
2336
+ this.selectionService.clearCurrentArticle();
2337
+ }
2338
+ /**
2339
+ * Navigates back in the selection history.
2340
+ *
2341
+ * Removes the most recent entry from the history. If the history is empty after this operation,
2342
+ * it returns `undefined`. Otherwise, it sets the current article to the last entry in the history,
2343
+ * triggers a 'back' event, and returns the last entry.
2344
+ *
2345
+ * @returns {Article | undefined} The last article in the history, or `undefined` if the history is empty.
2346
+ */
2347
+ back() {
2348
+ this.history.pop();
2349
+ if (this.history.length === 0)
2350
+ return undefined;
2351
+ const last = this.history[this.history.length - 1];
2352
+ this.selectionService.setCurrentArticle(last);
2353
+ this.selectionHistoryEvent.next('back');
2354
+ return last;
2355
+ }
2356
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: SelectionHistoryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2357
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: SelectionHistoryService, providedIn: 'root' }); }
2358
+ }
2359
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: SelectionHistoryService, decorators: [{
2360
+ type: Injectable,
2361
+ args: [{
2362
+ providedIn: 'root'
2363
+ }]
2364
+ }], ctorParameters: () => [] });
2365
+
2366
+ class AuditService {
2367
+ constructor() {
2368
+ this.appStore = inject(AppStore);
2369
+ this.app = computed(() => getState(this.appStore));
2370
+ this.http = inject(HttpClient);
2371
+ this.router = inject(Router);
2372
+ this.API_URL = `${globalConfig.backendUrl}/${globalConfig.apiPath}`;
2373
+ this.lastClickTime = 0;
2374
+ }
2375
+ /**
2376
+ * Notify the Sinequa server of a set of audit events
2377
+ *
2378
+ * @param auditEvents The audit events
2379
+ */
2380
+ notify(auditEvents) {
2381
+ this.http.post(this.API_URL + "/audit.notify", {
2382
+ event: "None", // AuditEventType.None,
2383
+ app: this.app().name,
2384
+ $auditRecord: { auditEvents: [auditEvents] }
2385
+ }).subscribe();
2386
+ }
2387
+ /**
2388
+ * It sends an audit event to the Audit Service.
2389
+ */
2390
+ notifyLogin() {
2391
+ this.notify({
2392
+ type: "Login_Success_Form" // AuditEventType.Search_Login_Success
2393
+ });
2394
+ }
2395
+ /**
2396
+ * Notify logout
2397
+ */
2398
+ notifyLogout() {
2399
+ this.notify({
2400
+ type: "Search_Exit_Logout" // AuditEventType.Search_Exit_Logout
2401
+ });
2402
+ }
2403
+ /**
2404
+ * Notify the Sinequa server of a document event
2405
+ *
2406
+ * @param evt The audit event type
2407
+ * @param doc The document (article) in question
2408
+ * @param resultId The resultid that contain the document
2409
+ * @param parameters Additional parameters
2410
+ */
2411
+ notifyDocument(auditEventType, doc, resultsOrId, parameters, rfmParameters) {
2412
+ let resultId;
2413
+ let result;
2414
+ if (typeof resultsOrId == "string") {
2415
+ resultId = resultsOrId;
2416
+ }
2417
+ else {
2418
+ result = resultsOrId;
2419
+ resultId = result ? result.id : null;
2420
+ }
2421
+ const detail = {
2422
+ docid: doc.id,
2423
+ rank: doc.rank,
2424
+ title: doc.title,
2425
+ source: doc.collection[0].split("/")[0],
2426
+ collection: doc.collection[0],
2427
+ resultId,
2428
+ filename: doc.filename,
2429
+ fileext: doc.fileext,
2430
+ index: doc.databasealias,
2431
+ resultcount: result?.totalRowCount
2432
+ };
2433
+ if (parameters) {
2434
+ Object.keys(parameters).forEach(key => detail[key] = parameters[key]);
2435
+ }
2436
+ const data = {
2437
+ type: auditEventType,
2438
+ detail
2439
+ };
2440
+ if (rfmParameters) {
2441
+ const rfmDetail = {}; // Add index signature
2442
+ Object.keys(rfmParameters).forEach(key => rfmDetail[key] = rfmParameters[key]);
2443
+ data.rfmDetail = rfmDetail;
2444
+ }
2445
+ this.lastClickTime = Date.now();
2446
+ // Listen to the navigation event outside the app
2447
+ document.addEventListener('visibilitychange', () => {
2448
+ // Capture the navigation even triggered just after the click
2449
+ if (document.visibilityState === 'hidden' && (Date.now() - this.lastClickTime) < 1000) {
2450
+ // Second event triggered when we come back
2451
+ document.addEventListener('visibilitychange', () => {
2452
+ if (document.visibilityState === 'visible') {
2453
+ this.notify({ type: "Navigation_Return" /* AuditEventType.Navigation_Return */ });
2454
+ }
2455
+ }, { once: true });
2456
+ }
2457
+ }, { once: true });
2458
+ this.notify(data);
2459
+ }
2460
+ /**
2461
+ * Notify route change
2462
+ */
2463
+ notifyRouteChange(url) {
2464
+ this.notify({
2465
+ type: "Navigation_Route", // AuditEventType.Navigation_Route,
2466
+ detail: {
2467
+ detail: url
2468
+ }
2469
+ });
2470
+ }
2471
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: AuditService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2472
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: AuditService, providedIn: 'root' }); }
2473
+ }
2474
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: AuditService, decorators: [{
2475
+ type: Injectable,
2476
+ args: [{
2477
+ providedIn: 'root'
2478
+ }]
2479
+ }] });
2480
+
2481
+ class NavigationService {
2482
+ constructor() {
2483
+ this.router = inject(Router);
2484
+ this.auditService = inject(AuditService);
2485
+ // The URL after the last navigation event, used to redirect the user after login
2486
+ this.urlAfterNavigation = null;
2487
+ /**
2488
+ * Observable that emits events of type `NavigationEnd` from the Angular Router.
2489
+ *
2490
+ * This observable performs the following operations:
2491
+ * - Maps all router events to `RouterEvent`.
2492
+ * - Filters the events to only include instances of `NavigationEnd`.
2493
+ * - Taps into the event stream to extract the route name from the URL and notify the audit service of route changes,
2494
+ * excluding the "loading" route and duplicate navigations.
2495
+ * - Updates the `urlAfterNavigation` property with the current URL after navigation.
2496
+ * - Shares the replayed value with a buffer size of 1 to ensure subscribers receive the latest emitted value.
2497
+ *
2498
+ * @type Observable<RouterEvent>
2499
+ */
2500
+ this.navigationEnd$ = this.router.events.pipe(map(event => event), filter((event) => event instanceof NavigationEnd), tap(event => {
2501
+ const url = event.url.slice(1).split('?')[0]; // Extract route name
2502
+ if (url && (url !== "loading" && url !== this.urlAfterNavigation)) {
2503
+ this.auditService.notifyRouteChange(url);
2504
+ }
2505
+ }), tap(event => this.urlAfterNavigation = event.url), shareReplay(1));
2506
+ /**
2507
+ * An observable that emits the tab extracted from the URL pathname or the last part of the URL.
2508
+ *
2509
+ * This observable listens to navigation end events and processes the URL to determine the current tab.
2510
+ * It creates a fake URL object to extract the pathname and then uses the `getQueryParamsFromUrl` function
2511
+ * to extract the tab from the URL pathname or defaults to the last part of the URL if no tab is found.
2512
+ *
2513
+ * @type Observable<string> - An observable that emits the current tab as a string.
2514
+ */
2515
+ this.path$ = this.navigationEnd$.pipe(
2516
+ // create a fake URL object to extract the pathname
2517
+ map((event) => {
2518
+ const url = new URL(`http://localhost${event.url}`);
2519
+ // extract the tab from the URL pathname or use the last part of the URL
2520
+ const { tab = url.pathname.split('/').pop() } = getQueryParamsFromUrl(event.url) || {};
2521
+ return tab;
2522
+ }));
2523
+ }
2524
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: NavigationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2525
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: NavigationService, providedIn: 'root' }); }
2526
+ }
2527
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: NavigationService, decorators: [{
2528
+ type: Injectable,
2529
+ args: [{
2530
+ providedIn: 'root'
2531
+ }]
2532
+ }] });
2533
+
2534
+ class BackdropService {
2535
+ constructor() {
2536
+ this.isVisible = new BehaviorSubject(false);
2537
+ }
2538
+ show() {
2539
+ this.isVisible.next(true);
2540
+ }
2541
+ hide() {
2542
+ this.isVisible.next(false);
2543
+ }
2544
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: BackdropService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2545
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: BackdropService, providedIn: 'root' }); }
2546
+ }
2547
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: BackdropService, decorators: [{
2548
+ type: Injectable,
2549
+ args: [{
2550
+ providedIn: 'root'
2551
+ }]
2552
+ }] });
2553
+
2554
+ class DrawerStackService {
2555
+ constructor() {
2556
+ this.isOpened = new BehaviorSubject(false);
2557
+ this.toggleTopDrawerExtension$ = new EventEmitter();
2558
+ this.forceTopDrawerCollapse$ = new EventEmitter();
2559
+ this.closeTopDrawer$ = new EventEmitter();
2560
+ this.closeAllDrawers$ = new EventEmitter();
2561
+ this.isChatOpened = new BehaviorSubject(false);
2562
+ this.openChatDrawer$ = new EventEmitter();
2563
+ this.closeChatDrawer$ = new EventEmitter();
2564
+ this.askAI$ = new EventEmitter();
2565
+ this.selection = inject(SelectionService);
2566
+ this.selectionHistory = inject(SelectionHistoryService);
2567
+ this.selectionStore = inject(SelectionStore);
2568
+ this.navigationService = inject(NavigationService);
2569
+ this.backdropService = inject(BackdropService);
2570
+ this.subscriptions = new Subscription();
2571
+ // on new search, close all drawers including chat drawer
2572
+ this.subscriptions.add(this.navigationService.navigationEnd$.subscribe(() => {
2573
+ this.closeAssistant();
2574
+ this.closeAll();
2575
+ }));
2576
+ }
2577
+ ngOnDestroy() {
2578
+ this.subscriptions.unsubscribe();
2579
+ }
2580
+ /**
2581
+ * Sets current drawer stack status to open
2582
+ */
2583
+ open(componentType) {
2584
+ this.componentType = componentType;
2585
+ this.isOpened.next(true);
2586
+ }
2587
+ /**
2588
+ * Emits event to extend the top drawer
2589
+ */
2590
+ extend() {
2591
+ this.toggleTopDrawerExtension$.next();
2592
+ }
2593
+ /**
2594
+ * Emits event to close the top drawer, checks if history is empty, if so,
2595
+ * sets current drawer stack status to closed and clears history and current
2596
+ * selection
2597
+ */
2598
+ close() {
2599
+ this.closeTopDrawer$.next();
2600
+ if (this.selectionHistory.getCurrentSelectionIndex() === -1) {
2601
+ this.isOpened.next(false);
2602
+ this.selectionHistory.clearHistory();
2603
+ }
2604
+ }
2605
+ /**
2606
+ * Emits event to close the top drawer
2607
+ */
2608
+ closeTop() {
2609
+ this.closeTopDrawer$.next();
2610
+ }
2611
+ /**
2612
+ * Sets current drawer stack status to closed, clears history and emits event
2613
+ * to close all drawers
2614
+ *
2615
+ * @param keepDrawerOpen if true, do not trigger layout animation
2616
+ */
2617
+ closeAll(keepDrawerOpen = false) {
2618
+ if (!keepDrawerOpen)
2619
+ this.isOpened.next(false);
2620
+ this.selectionHistory.clearHistory();
2621
+ this.closeAllDrawers$.next();
2622
+ }
2623
+ /**
2624
+ * Replace the current selection with the given article by closing all
2625
+ * drawers and opening the drawer with the new selection without triggering
2626
+ * layout animation
2627
+ *
2628
+ * @param article the article to replace the current selection with
2629
+ */
2630
+ replace(article) {
2631
+ const { id } = getState(this.selectionStore);
2632
+ if (id && (!article || article.id === id))
2633
+ return;
2634
+ // close everything without trigger layout animation
2635
+ this.closeAll(true);
2636
+ // set selection
2637
+ this.selection.setCurrentArticle(article);
2638
+ // open drawer
2639
+ this.open();
2640
+ }
2641
+ /**
2642
+ * Stack the given article by setting the current selection and opening the
2643
+ * drawer
2644
+ *
2645
+ * @param article the article to stack
2646
+ */
2647
+ stack(article, withQueryText) {
2648
+ const { id } = getState(this.selectionStore);
2649
+ if (id && (!article || article.id === id))
2650
+ return;
2651
+ // force top drawer to collapse
2652
+ this.forceTopDrawerCollapse$.next();
2653
+ // set selection
2654
+ this.selection.setCurrentArticle(article, withQueryText);
2655
+ // open drawer
2656
+ this.open();
2657
+ }
2658
+ toggleAssistant() {
2659
+ if (this.isChatOpened.getValue()) {
2660
+ this.backdropService.hide();
2661
+ this.closeAssistant();
2662
+ }
2663
+ else {
2664
+ this.backdropService.show();
2665
+ this.openAssistant();
2666
+ }
2667
+ }
2668
+ openAssistant() {
2669
+ this.isChatOpened.next(true);
2670
+ this.isOpened.next(true);
2671
+ this.openChatDrawer$.next();
2672
+ this.backdropService.show();
2673
+ }
2674
+ closeAssistant(keepDrawerOpen = false) {
2675
+ this.isChatOpened.next(false);
2676
+ if (!keepDrawerOpen && this.selectionHistory.getCurrentSelectionIndex() === -1) {
2677
+ this.isOpened.next(false);
2678
+ this.closeAllDrawers$.next();
2679
+ }
2680
+ this.closeChatDrawer$.next();
2681
+ this.backdropService.hide();
2682
+ }
2683
+ askAI(text) {
2684
+ this.openAssistant();
2685
+ this.askAI$.next(text);
2686
+ }
2687
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: DrawerStackService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2688
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: DrawerStackService, providedIn: 'root' }); }
2689
+ }
2690
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: DrawerStackService, decorators: [{
2691
+ type: Injectable,
2692
+ args: [{
2693
+ providedIn: 'root'
2694
+ }]
2695
+ }], ctorParameters: () => [] });
2696
+
2697
+ /**
2698
+ * Directive that selects an article on click.
2699
+ */
2700
+ class SelectArticleOnClickDirective {
2701
+ constructor() {
2702
+ this.drawerStack = inject(DrawerStackService);
2703
+ this.article = input.required();
2704
+ this.strategy = input('stack');
2705
+ }
2706
+ onClick() {
2707
+ if (!this.article())
2708
+ return;
2709
+ switch (this.strategy()) {
2710
+ case 'replace':
2711
+ this.drawerStack.replace(this.article());
2712
+ break;
2713
+ case 'stack':
2714
+ default:
2715
+ this.drawerStack.stack(this.article());
2716
+ break;
2717
+ }
2718
+ }
2719
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: SelectArticleOnClickDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
2720
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "18.2.5", type: SelectArticleOnClickDirective, isStandalone: true, selector: "[appSelectArticleOnClick],[selectArticleOnClick]", inputs: { article: { classPropertyName: "article", publicName: "article", isSignal: true, isRequired: true, transformFunction: null }, strategy: { classPropertyName: "strategy", publicName: "strategy", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "click": "onClick()" } }, ngImport: i0 }); }
2721
+ }
2722
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: SelectArticleOnClickDirective, decorators: [{
2723
+ type: Directive,
2724
+ args: [{
2725
+ selector: '[appSelectArticleOnClick],[selectArticleOnClick]',
2726
+ standalone: true,
2727
+ }]
2728
+ }], propDecorators: { onClick: [{
2729
+ type: HostListener,
2730
+ args: ['click']
2731
+ }] } });
2732
+
2733
+ /**
2734
+ * Directive that handles the behavior of showing a bookmark for an article.
2735
+ *
2736
+ * @remarks
2737
+ * This directive listens to mouse enter and mouse leave events to determine when to show the bookmark.
2738
+ * It also checks the user's settings to determine if the article is bookmarked.
2739
+ *
2740
+ * @example
2741
+ * ```html
2742
+ * <div showBookmark [article]="currentArticle" (showBookmark)="onShowBookmark($event)"></div>
2743
+ * ```
2744
+ */
2745
+ class ShowBookmarkDirective {
2746
+ mouseEnter() {
2747
+ this.bookmarkHovered.set(true);
2748
+ }
2749
+ mouseLeave() {
2750
+ this.bookmarkHovered.set(false);
2751
+ }
2752
+ constructor() {
2753
+ this.bookmarkHovered = signal(false);
2754
+ this.userSettingsStore = inject(UserSettingsStore);
2755
+ this.isBookmarked = computed(() => {
2756
+ if (!this.article())
2757
+ return false;
2758
+ return this.userSettingsStore.isBookmarked(this.article());
2759
+ });
2760
+ this.article = input.required();
2761
+ this.showBookmark = output();
2762
+ effect(() => {
2763
+ const bookmarkHovered = this.bookmarkHovered();
2764
+ const isBookmarked = this.isBookmarked();
2765
+ this.showBookmark.emit(bookmarkHovered || isBookmarked);
2766
+ });
2767
+ }
2768
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: ShowBookmarkDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
2769
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "18.2.5", type: ShowBookmarkDirective, isStandalone: true, selector: "[appShowBookmark],[showBookmark]", inputs: { article: { classPropertyName: "article", publicName: "article", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { showBookmark: "showBookmark" }, host: { listeners: { "mouseenter": "mouseEnter()", "mouseleave": "mouseLeave()" } }, ngImport: i0 }); }
2770
+ }
2771
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: ShowBookmarkDirective, decorators: [{
2772
+ type: Directive,
2773
+ args: [{
2774
+ selector: '[appShowBookmark],[showBookmark]',
2775
+ standalone: true
2776
+ }]
2777
+ }], ctorParameters: () => [], propDecorators: { mouseEnter: [{
2778
+ type: HostListener,
2779
+ args: ['mouseenter']
2780
+ }], mouseLeave: [{
2781
+ type: HostListener,
2782
+ args: ['mouseleave']
2783
+ }] } });
2784
+
2785
+ /**
2786
+ * This directive is used to apply a theme to a native element based on the theme scope.
2787
+ */
2788
+ class ThemeProviderDirective {
2789
+ constructor() {
2790
+ this.themeProvider = input.required();
2791
+ this.host = inject(ElementRef);
2792
+ this.themeStore = inject(ThemeStore);
2793
+ effect(() => {
2794
+ const scope = this.themeStore.scopes()?.[this.themeProvider()];
2795
+ if (!scope)
2796
+ return;
2797
+ const vars = scope.darkMode ? scope.cssVars.dark : scope.cssVars.light;
2798
+ if (vars)
2799
+ applyThemeToNativeElement(this.host.nativeElement, vars);
2800
+ });
2801
+ }
2802
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: ThemeProviderDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
2803
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "18.2.5", type: ThemeProviderDirective, isStandalone: true, selector: "[themeProvider]", inputs: { themeProvider: { classPropertyName: "themeProvider", publicName: "themeProvider", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 }); }
2804
+ }
2805
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: ThemeProviderDirective, decorators: [{
2806
+ type: Directive,
2807
+ args: [{
2808
+ selector: '[themeProvider]',
2809
+ standalone: true
2810
+ }]
2811
+ }], ctorParameters: () => [] });
2812
+
2813
+ /**
2814
+ * Returns a guard function that checks if the user is authenticated.
2815
+ * If the user is not authenticated, it navigates to the login page.
2816
+ * @returns The guard function.
2817
+ */
2818
+ function AuthGuard() {
2819
+ return (_, state) => {
2820
+ const router = inject(Router);
2821
+ const { loginPath, useCredentials, useSSO } = globalConfig;
2822
+ if (isAuthenticated())
2823
+ return true;
2824
+ // when using SSO authentication, the application is already authenticated
2825
+ if (useSSO)
2826
+ return true;
2827
+ if (useCredentials) {
2828
+ router.navigate([loginPath], { queryParams: { returnUrl: state.url } });
2829
+ }
2830
+ else {
2831
+ router.navigate(['loading'], { queryParams: { returnUrl: state.url } });
2832
+ }
2833
+ return false;
2834
+ };
2835
+ }
2836
+
2837
+ /**
2838
+ * InitializationGuard is a route guard that ensures the application is ready before allowing navigation to a requested page.
2839
+ *
2840
+ * This guard checks if the application is fully initialized by verifying the readiness of the ApplicationStore.
2841
+ * If the application is not ready, it redirects the user to a loading page and prevents navigation to the requested page.
2842
+ *
2843
+ * @returns {CanActivateFn} A function that returns true if the application is ready, false otherwise.
2844
+ *
2845
+ * @example
2846
+ * // Usage in a routing module
2847
+ * const routes: Routes = [
2848
+ * {
2849
+ * path: 'some-path',
2850
+ * component: SomeComponent,
2851
+ * canActivate: [InitializationGuard]
2852
+ * }
2853
+ * ];
2854
+ */
2855
+ function InitializationGuard() {
2856
+ return (_, state) => {
2857
+ const route = inject(Router);
2858
+ const app = inject(ApplicationStore);
2859
+ /**
2860
+ * If the app is not ready, navigate to the loading page.
2861
+ * This is useful when the app is loading and the user tries to access a page that requires the app to be ready.
2862
+ *
2863
+ * We need to wait appStore, PrincipalStore and UserSettingsStore to be initialized before we can navigate to the requested page.
2864
+ *
2865
+ * @returns True if the app is ready, false otherwise.
2866
+ */
2867
+ if (!app.ready()) {
2868
+ route.navigate(['/loading'], { queryParams: { returnUrl: state.url } });
2869
+ return false;
2870
+ }
2871
+ return true;
2872
+ };
2873
+ }
2874
+
2875
+ /**
2876
+ * Intercepts HTTP requests to add audit information if the request URL includes the API path.
2877
+ *
2878
+ * This interceptor checks if the request URL contains the specified API path from the global configuration.
2879
+ * If the request body is JSON serializable and not an instance of `HttpParams`, it adds additional audit information
2880
+ * to the request body using the `addAuditAdditionalInfo` function.
2881
+ *
2882
+ * @param request - The HTTP request to be intercepted.
2883
+ * @param next - The next handler in the HTTP request chain.
2884
+ * @returns The next handler in the HTTP request chain, potentially with modified request body.
2885
+ */
2886
+ const auditInterceptorFn = (request, next) => {
2887
+ if (!request.url.includes(globalConfig.apiPath))
2888
+ return next(request);
2889
+ if (isJsonable(request.body) && !(request.body instanceof HttpParams)) {
2890
+ // beware addAuditAdditionalInfo use a reference to the original object
2891
+ addAuditAdditionalInfo(request.body);
2892
+ }
2893
+ return next(request);
2894
+ };
2895
+
2896
+ // TODO: Replace with your locale name
2897
+ // const LOCALE_NAME = "fr";
2898
+ /**
2899
+ * Intercepts HTTP requests to add authentication headers and handle CSRF tokens.
2900
+ *
2901
+ * This interceptor checks if the user is logged in and adds necessary headers
2902
+ * to the request, including a CSRF token. If user override is active, it sets
2903
+ * the override user and domain headers instead. It also updates the CSRF token
2904
+ * from the response headers if present.
2905
+ *
2906
+ * @param request - The outgoing HTTP request.
2907
+ * @param next - The next handler in the HTTP request pipeline.
2908
+ * @returns An observable of the HTTP event stream.
2909
+ */
2910
+ const authInterceptorFn = (request, next) => {
2911
+ const { userOverride, userOverrideActive, useSSO } = globalConfig;
2912
+ // add auth header with jwt if user is logged in and request is to the api url
2913
+ const isLoggedIn = isAuthenticated() || useSSO;
2914
+ const csrfToken = getToken();
2915
+ let headers = {
2916
+ 'Sinequa-Force-Camel-Case': 'true',
2917
+ 'accept': 'application/json',
2918
+ 'Sinequa-csrf-token': `${csrfToken}`,
2919
+ // "x-language": LOCALE_NAME
2920
+ };
2921
+ if (userOverride && userOverrideActive) {
2922
+ headers = {
2923
+ ...headers,
2924
+ 'sinequa-override-user': userOverride.username,
2925
+ 'sinequa-override-domain': userOverride.domain,
2926
+ };
2927
+ }
2928
+ request = request.clone({
2929
+ setHeaders: headers
2930
+ });
2931
+ return next(request).pipe(map(event => {
2932
+ if (event instanceof HttpResponse) {
2933
+ const csrfToken = event.headers.get('sinequa-csrf-token');
2934
+ if (csrfToken) {
2935
+ setToken(csrfToken);
2936
+ }
2937
+ }
2938
+ return event; // Add this line to fix the return type
2939
+ }));
2940
+ };
2941
+
2942
+ /**
2943
+ * Interceptor function that modifies the request body by appending a "locale" parameter with the value "fr".
2944
+ * If the request body is of type FormData, the "locale" parameter is appended directly.
2945
+ * If the request body is an object, a new object is created with the "locale" parameter added.
2946
+ *
2947
+ * @param request - The HTTP request object.
2948
+ * @param next - The HTTP handler function.
2949
+ * @returns The modified request object.
2950
+ */
2951
+ const bodyInterceptorFn = (request, next) => {
2952
+ const LOCALE_NAME = "fr";
2953
+ if (request.body instanceof FormData) {
2954
+ request.body.append("ui-language", LOCALE_NAME);
2955
+ request = request.clone();
2956
+ }
2957
+ else {
2958
+ const body = { ...request.body, "ui-language": LOCALE_NAME };
2959
+ request = request.clone({
2960
+ body
2961
+ });
2962
+ }
2963
+ return next(request);
2964
+ };
2965
+
2966
+ const ROUTE_COMPONENTS = new InjectionToken('ROUTE_COMPONENTS', { factory: () => [] });
2967
+ class ApplicationService {
2968
+ constructor() {
2969
+ this.userSettingsStore = inject(UserSettingsStore);
2970
+ this.principalStore = inject(PrincipalStore);
2971
+ this.appService = inject(AppService);
2972
+ this.appStore = inject(AppStore);
2973
+ this.applicationStore = inject(ApplicationStore);
2974
+ this.route = inject(ActivatedRoute);
2975
+ this.router = inject(Router);
2976
+ this.auditService = inject(AuditService);
2977
+ this.components = inject(ROUTE_COMPONENTS);
2978
+ this.defaultComponent = computed(() => this.components.find(c => c.path === 'all')?.component);
2979
+ this.defaultLayoutComponent = computed(() => this.components.find(c => c.isRoot)?.component);
2980
+ }
2981
+ /**
2982
+ * Registers a list of components.
2983
+ *
2984
+ * * For each path, the corresponding component is registered.
2985
+ * * The default component is the component registered with the path 'all'.
2986
+ * * The default layout component is the component registered with the isLayout flag set to true.
2987
+ * * If no layout component is registered, the default layout component is the default component.
2988
+ * * If no default component is registered, the default component is the first component in the list.
2989
+ *
2990
+ * Those components will be used to create the routes.
2991
+ *
2992
+ * @deprecated use the ROUTE_COMPONENTS injection token instead
2993
+ *
2994
+ * @param components - An array of ComponentMapping objects to be registered.
2995
+ */
2996
+ register(components) {
2997
+ this.components = [...components];
2998
+ }
2999
+ /**
3000
+ * Authenticates the user with the provided credentials, initializes and creates routes.&nbsp;&nbsp;
3001
+ * * in case of failure, it rejects with an error message.
3002
+ * * in case of success, it resolves with `true`, shows a toast notification to welcome the user,
3003
+ * and updates the application state to ready.
3004
+ *
3005
+ * @param {Credentials} [credentials] - The user's login credentials.
3006
+ * @returns {Promise<boolean>} - A promise that resolves to `true` if login is successful,
3007
+ * or rejects with an error message if login fails.
3008
+ * @throws {Error} - Throws an error if the login process fails.
3009
+ */
3010
+ async autoLogin({ credentials, withCreateRoutes = true } = {}) {
3011
+ const { useCredentialsOrSSO } = globalConfig;
3012
+ try {
3013
+ const authenticated = await login(credentials);
3014
+ if (authenticated) {
3015
+ await this.initAndCreateRoutes(withCreateRoutes).catch((error) => {
3016
+ console.warn('Error initializing the application (authenticated)', error);
3017
+ return Promise.reject(error);
3018
+ });
3019
+ // Show a toast notification to welcome the user
3020
+ // and the application is ready to use
3021
+ const { fullName, name } = getState(this.principalStore).principal;
3022
+ toast(`Welcome back ${fullName || name}!`, { duration: 2000 });
3023
+ this.applicationStore.updateReadyState();
3024
+ return Promise.resolve(true);
3025
+ }
3026
+ else {
3027
+ return Promise.reject('Login failed');
3028
+ }
3029
+ }
3030
+ catch (error) {
3031
+ console.error('Error logging in', error);
3032
+ if (useCredentialsOrSSO) {
3033
+ await this.initAndCreateRoutes(withCreateRoutes).catch((error) => {
3034
+ console.warn('Error initializing the application (useCredentialsOrSSO)', error);
3035
+ setGlobalConfig({ useCredentials: true });
3036
+ return Promise.reject(error);
3037
+ });
3038
+ setGlobalConfig({ useSSO: true });
3039
+ // Show a toast notification to welcome the user
3040
+ // and the application is ready to use
3041
+ const { fullName, name } = getState(this.principalStore).principal;
3042
+ toast(`Welcome back ${fullName || name}!`, { duration: 2000 });
3043
+ this.applicationStore.updateReadyState();
3044
+ return Promise.resolve(true);
3045
+ }
3046
+ else {
3047
+ return Promise.reject(error);
3048
+ }
3049
+ }
3050
+ }
3051
+ /**
3052
+ * Initializes the application.
3053
+ * - Fetches the application configuration.
3054
+ * - Sets the fetched application configuration in the app store.
3055
+ * - Loads the user settings and logs the state of the user settings store.
3056
+ */
3057
+ async init() {
3058
+ // Fetch the application configuration
3059
+ await this.appStore.initialize();
3060
+ console.log("appStore", getState(this.appStore));
3061
+ // Load the principal (user information)
3062
+ await this.principalStore.initialize();
3063
+ console.log("principalStore", getState(this.principalStore));
3064
+ // Load the user settings
3065
+ await this.userSettingsStore.initialize();
3066
+ console.log("userSettingsStore", getState(this.userSettingsStore));
3067
+ // Labels access
3068
+ const service = (this.appStore.getWebServiceByType('labels'));
3069
+ if (!service) {
3070
+ this.applicationStore.updateHasLabelsAccess(false);
3071
+ return;
3072
+ }
3073
+ try {
3074
+ const rights = await labels.getUserRights();
3075
+ this.applicationStore.updateHasLabelsAccess(rights.canEditPublicLabels || rights.canManagePublicLabels);
3076
+ }
3077
+ catch (error) {
3078
+ console.log("labels.canHandleLabels failure - error: ", error);
3079
+ this.applicationStore.updateHasLabelsAccess(false);
3080
+ }
3081
+ }
3082
+ /**
3083
+ * Initializes the application and creates routes.
3084
+ *
3085
+ * This method performs the following actions:
3086
+ * 1. Calls the `init` method to initialize the application.
3087
+ * 2. Calls the `createRoutes` method to set up the application routes.
3088
+ * 3. Notifies the audit service of a login event.
3089
+ *
3090
+ * @returns {Promise<void>} A promise that resolves when the initialization and route creation are complete.
3091
+ */
3092
+ async initAndCreateRoutes(withCreateRoutes = true) {
3093
+ // throw an error if no components are registered and withCreateRoutes is true
3094
+ // components are required to create the routes
3095
+ if (withCreateRoutes && !this.components.length)
3096
+ throw new Error('No components registered');
3097
+ await this.init();
3098
+ if (withCreateRoutes) {
3099
+ this.createRoutes();
3100
+ }
3101
+ }
3102
+ /**
3103
+ * Creates dynamic routes based on the application's queries and custom JSON configurations.
3104
+ *
3105
+ * This method performs the following steps:
3106
+ * 1. Retrieves the queries and custom JSON data from the application state.
3107
+ * 2. Maps the queries to an array of objects containing query names and tabs.
3108
+ * 3. Throws an error if no queries are found.
3109
+ * 4. Retrieves route data from custom JSONs or falls back to default data.
3110
+ * 5. Creates routes for each tab in each query, or uses the query name if no tabs are found.
3111
+ * 6. Removes the current search route from the router configuration.
3112
+ * 7. Creates child routes based on the provided routes data or the first query's tabs.
3113
+ * 8. Updates the search route with the new child routes.
3114
+ * 9. Resets the router configuration with the new routes.
3115
+ *
3116
+ * @throws {Error} If no queries are found.
3117
+ */
3118
+ createRoutes() {
3119
+ // Now we can create the dynamic routes based on the queries's tabs
3120
+ const { queries, data: custom, customJSONs = [] } = getState(this.appStore);
3121
+ // contains an array of objects with the query name and the tabs
3122
+ const queriesMap = Object.entries(queries).map(([key, value]) => ({ key, ...value }));
3123
+ // ! if no queries are found, throw an error
3124
+ if (!queriesMap.length) {
3125
+ throw new Error('No queries found');
3126
+ }
3127
+ // Retrieves the routes data from the custom JSONs array or fall back to the default data (custom json).
3128
+ let { data: cjRoutes } = Array.isArray(customJSONs) ? customJSONs?.find((c => c.name === 'routes')) || {} : { data: custom?.['routes'] };
3129
+ // check if cjRoutes is an array and not an object to avoid errors
3130
+ if (cjRoutes && !Array.isArray(cjRoutes)) {
3131
+ cjRoutes = undefined;
3132
+ }
3133
+ const routes = (cjRoutes || custom?.['routes']);
3134
+ // take only first query
3135
+ const firstQuery = queriesMap[0];
3136
+ // We need to create a route for each tab in each query
3137
+ // if a query has no tabs, we create a route with the query name as the tab name
3138
+ const firstQueryConfig = { tabs: firstQuery.tabSearch.tabs || [{ name: firstQuery.name }] };
3139
+ // We need to remove the current search route from the router config
3140
+ // the route exists in the router config because it was created in the app-routing.module.ts and we need it
3141
+ // to be able to navigate to the search page. We will recreate it with the new tabs
3142
+ const currentConfig = this.router.config.filter(route => route.path !== 'search');
3143
+ let children = [];
3144
+ // if the routes data is provided, we create the children routes based on the routes data
3145
+ if (routes) {
3146
+ // update routes pathDisplayName with the query display name when not provided
3147
+ // for each path we need to find the corresponding query and tab in the firstQuery object
3148
+ // and create a child route with the query name and tab name
3149
+ const displayNamesMap = firstQuery.tabSearch.tabs.reduce((acc, tab) => {
3150
+ acc[tab.name] = { display: tab.display };
3151
+ return acc;
3152
+ }, {});
3153
+ children = routes.map((route) => {
3154
+ return ({
3155
+ path: route.path,
3156
+ component: this.components.find(c => c.path === route.path)?.component || this.defaultComponent(),
3157
+ data: {
3158
+ queryName: route.wsName || firstQuery.name,
3159
+ display: route.pathDisplayName || displayNamesMap[route.wsQueryTab]?.display || route.path,
3160
+ wsQueryTab: route.wsQueryTab,
3161
+ iconClass: route.icon
3162
+ }
3163
+ });
3164
+ });
3165
+ }
3166
+ else {
3167
+ // if the routes data is not provided, we create the children routes based on the first query's tabs
3168
+ // create the children routes for the search route
3169
+ children = firstQueryConfig.tabs.map(tab => ({
3170
+ path: tab.name,
3171
+ component: this.components.find(c => c.path === tab.name)?.component || this.defaultComponent(),
3172
+ data: { queryName: firstQuery.name, display: tab.display || tab.name }
3173
+ }));
3174
+ }
3175
+ const searchPath = this.router.config.find(route => route.path === 'search')
3176
+ || {
3177
+ path: 'search',
3178
+ component: this.components.find(c => c.path === 'search')?.component || this.defaultLayoutComponent(),
3179
+ canActivate: [AuthGuard(), InitializationGuard()],
3180
+ children: []
3181
+ };
3182
+ searchPath.component = this.components.find(c => c.path === 'search')?.component || this.defaultLayoutComponent();
3183
+ searchPath.children = [...children, { path: '**', redirectTo: 'all', pathMatch: 'full' }];
3184
+ const newConfig = [searchPath, ...currentConfig];
3185
+ // finally we reset the router config with the new routes
3186
+ this.router.resetConfig(newConfig);
3187
+ }
3188
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: ApplicationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3189
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: ApplicationService, providedIn: 'root' }); }
3190
+ }
3191
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: ApplicationService, decorators: [{
3192
+ type: Injectable,
3193
+ args: [{
3194
+ providedIn: 'root'
3195
+ }]
3196
+ }] });
3197
+
3198
+ class AutocompleteService {
3199
+ constructor() {
3200
+ this.opened = signal(false);
3201
+ this.injector = inject(Injector);
3202
+ this.userSettingsStore = inject(UserSettingsStore);
3203
+ this.appStore = inject(AppStore);
3204
+ }
3205
+ /**
3206
+ * Retrieves autocomplete items for the given text, max count for each
3207
+ * category handled by the service can be specified in the admin
3208
+ *
3209
+ * @param text Text to retrieve autocomplete items for
3210
+ * @returns An observable of an array of {@link Suggestion} arrays grouped by
3211
+ * suggestion queries configured in the admin
3212
+ */
3213
+ getFromSuggestQueriesForText(text) {
3214
+ // Do not ask for autocomplete items if the text is empty
3215
+ if (!text)
3216
+ return of([]);
3217
+ const queries = this.appStore.getWebServiceByType('autocomplete')?.suggestQueries?.split(',') ?? [];
3218
+ const obss = queries.reduce((acc, curr) => {
3219
+ acc.push(from(fetchSuggest(curr, text)));
3220
+ return acc;
3221
+ }, []);
3222
+ return forkJoin(obss);
3223
+ }
3224
+ /**
3225
+ * Retrieves autocomplete items for the given text from the user settings
3226
+ *
3227
+ * @param text Text to retrieve autocomplete items for
3228
+ * @param maxCount Maximum number of items to retrieve
3229
+ * @returns An observable of an array of {@link Suggestion} arrays grouped by
3230
+ * `recent-searches`, `saved-searches`, `bookmarks` from the user settings
3231
+ */
3232
+ getFromUserSettingsForText(text, maxCount) {
3233
+ const { bookmarks, recentSearches, savedSearches } = getState(this.userSettingsStore);
3234
+ const items = [];
3235
+ if (typeof maxCount === 'number')
3236
+ maxCount = { recentSearches: maxCount, savedSearches: maxCount, bookmarks: maxCount };
3237
+ if (recentSearches) {
3238
+ // don't filter if the text is empty
3239
+ const matchingRecentSearches = text
3240
+ ? recentSearches.filter(recentSearch => recentSearch.display?.toLocaleLowerCase().includes(text.toLocaleLowerCase()))
3241
+ : recentSearches;
3242
+ const searches = matchingRecentSearches.slice(0, maxCount?.recentSearches);
3243
+ if (searches.length > 0)
3244
+ items.push(...searches.map(search => ({ category: 'recent-search', ...search })));
3245
+ }
3246
+ if (savedSearches) {
3247
+ // don't filter if the text is empty
3248
+ const matchingSavedSearches = text
3249
+ ? savedSearches.filter(savedSearch => savedSearch.display?.toLocaleLowerCase().includes(text.toLocaleLowerCase()))
3250
+ : savedSearches;
3251
+ const searches = matchingSavedSearches.slice(0, maxCount?.savedSearches);
3252
+ if (searches.length > 0)
3253
+ items.push(...searches.map(search => ({ category: 'saved-search', ...search })));
3254
+ }
3255
+ if (bookmarks) {
3256
+ // don't filter if the text is empty
3257
+ const matchingBookmarks = text
3258
+ ? bookmarks.filter(bookmark => bookmark.label?.toLowerCase().includes(text.toLowerCase()))
3259
+ : bookmarks;
3260
+ const searches = matchingBookmarks.slice(0, maxCount?.bookmarks);
3261
+ if (searches.length > 0)
3262
+ items.push(...searches.map(search => ({ category: 'bookmark', display: search.label ?? '', ...search })));
3263
+ }
3264
+ return items;
3265
+ }
3266
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: AutocompleteService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3267
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: AutocompleteService, providedIn: 'root' }); }
3268
+ }
3269
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: AutocompleteService, decorators: [{
3270
+ type: Injectable,
3271
+ args: [{
3272
+ providedIn: 'root'
3273
+ }]
3274
+ }] });
3275
+
3276
+ ;
3277
+ class LabelService {
3278
+ constructor() {
3279
+ this.appStore = inject(AppStore);
3280
+ this.hasAccess = signal(undefined);
3281
+ }
3282
+ /**
3283
+ * Check user rights to verify if they can access labels handling
3284
+ * @returns if has the rights
3285
+ */
3286
+ async canHandleLabels() {
3287
+ if (this.hasAccess() !== undefined)
3288
+ return this.hasAccess();
3289
+ const service = this.appStore.getWebServiceByType('labels');
3290
+ if (!service)
3291
+ return false;
3292
+ try {
3293
+ const rights = await labels.getUserRights();
3294
+ this.hasAccess.set(rights.canEditPublicLabels || rights.canManagePublicLabels);
3295
+ return this.hasAccess();
3296
+ }
3297
+ catch (error) {
3298
+ console.log("labels.canHandleLabels failure - error: ", error);
3299
+ return false;
3300
+ }
3301
+ }
3302
+ /**
3303
+ * Get relevant config info from the labels web service
3304
+ * @returns the LabelsConfig or undefined if no rights
3305
+ */
3306
+ getLabelsConfig() {
3307
+ return from(this.canHandleLabels())
3308
+ .pipe(map((canHandle) => {
3309
+ if (!canHandle)
3310
+ return undefined;
3311
+ const { allowPublicLabelsCreation, allowPublicLabelsModification, privateLabelsField, publicLabelsField, defaultPublicLabels, labelsAutoSuggestMaxCount, labelsAutoSuggestWildcard } = this.appStore.getWebServiceByType('labels');
3312
+ return {
3313
+ allowPublicLabelsCreation,
3314
+ allowPublicLabelsModification,
3315
+ privateLabelsField,
3316
+ publicLabelsField,
3317
+ defaultPublicLabels,
3318
+ labelsAutoSuggestMaxCount,
3319
+ labelsAutoSuggestWildcard
3320
+ };
3321
+ }));
3322
+ }
3323
+ /**
3324
+ * Fetch labels from string
3325
+ * @param prefix the string prefix to filter with
3326
+ * @param publicOnly whether they should be public or not
3327
+ * @param locale optional locale filtering
3328
+ * @returns list of label strings
3329
+ */
3330
+ fetch(prefix, publicOnly = true, locale) {
3331
+ return from(fetchLabels(prefix, publicOnly, locale));
3332
+ }
3333
+ /**
3334
+ * Add some labels to many documents
3335
+ * @param labelsToAdd list of labels to add
3336
+ * @param ids list of document id to add the label
3337
+ * @param publicOnly whether the operation should be public only
3338
+ * @returns a promise that resolves when the operation is complete
3339
+ */
3340
+ add(labelsToAdd, ids, publicOnly) {
3341
+ return from(this.canHandleLabels())
3342
+ .pipe(switchMap((canHandle) => {
3343
+ if (!canHandle)
3344
+ return of(undefined);
3345
+ return from(labels.add(labelsToAdd, ids, publicOnly));
3346
+ }));
3347
+ }
3348
+ /**
3349
+ * Remove some labels from many documents
3350
+ * @param labelsToRemove list of labels to remove
3351
+ * @param ids list of document id to add the label
3352
+ * @param publicOnly whether the operation should be public only
3353
+ * @returns a promise that resolves when the operation is complete
3354
+ */
3355
+ remove(labelsToRemove, ids, publicOnly) {
3356
+ return from(this.canHandleLabels())
3357
+ .pipe(switchMap((canHandle) => {
3358
+ if (!canHandle)
3359
+ return of(undefined);
3360
+ return from(labels.remove(labelsToRemove, ids, publicOnly));
3361
+ }));
3362
+ }
3363
+ /**
3364
+ * Name some labels to a new name
3365
+ * @param labelsToRename list of labels to rename
3366
+ * @param newLabel the new label name
3367
+ * @param publicOnly whether the operation should be public only
3368
+ * @returns a promise that resolves when the operation is complete
3369
+ */
3370
+ rename(labelsToRename, newLabel, publicOnly) {
3371
+ return from(this.canHandleLabels())
3372
+ .pipe(switchMap((canHandle) => {
3373
+ if (!canHandle)
3374
+ return of(undefined);
3375
+ return from(labels.rename(labelsToRename, newLabel, publicOnly));
3376
+ }));
3377
+ }
3378
+ /**
3379
+ * Delete some labels
3380
+ * @param labelsToDelete the labels to delete
3381
+ * @param publicOnly whether the operation should be public only
3382
+ * @returns a promise that resolves when the operation is complete
3383
+ */
3384
+ delete(labelsToDelete, publicOnly) {
3385
+ return from(this.canHandleLabels())
3386
+ .pipe(switchMap((canHandle) => {
3387
+ if (!canHandle)
3388
+ return of(undefined);
3389
+ return from(labels.delete(labelsToDelete, publicOnly));
3390
+ }));
3391
+ }
3392
+ /**
3393
+ * Create some labels for a query
3394
+ * @param labelstoAdd the labels to create
3395
+ * @param query the query object to which the labels will be added
3396
+ * @param publicOnly whether the operation should be public only
3397
+ * @returns a promise that resolves when the operation is complete
3398
+ */
3399
+ bulkAdd(labelstoAdd, query, publicOnly) {
3400
+ return from(this.canHandleLabels())
3401
+ .pipe(switchMap((canHandle) => {
3402
+ if (!canHandle)
3403
+ return of(undefined);
3404
+ return from(labels.bulkAdd(labelstoAdd, query, publicOnly));
3405
+ }));
3406
+ }
3407
+ /**
3408
+ * Delete some labels from a query
3409
+ * @param labelsToRemove the labels to remove
3410
+ * @param query the query object to which the labels will be removed
3411
+ * @param publicOnly whether the operation should be public only
3412
+ * @returns a promise that resolves when the operation is complete
3413
+ */
3414
+ bulkRemove(labelsToRemove, query, publicOnly) {
3415
+ return from(this.canHandleLabels())
3416
+ .pipe(switchMap((canHandle) => {
3417
+ if (!canHandle)
3418
+ return of(undefined);
3419
+ return from(labels.bulkRemove(labelsToRemove, query, publicOnly));
3420
+ }));
3421
+ }
3422
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: LabelService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3423
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: LabelService, providedIn: 'root' }); }
3424
+ }
3425
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: LabelService, decorators: [{
3426
+ type: Injectable,
3427
+ args: [{
3428
+ providedIn: 'root'
3429
+ }]
3430
+ }] });
3431
+
3432
+ const SAVED_SEARCHES_MAX_STORAGE = 100;
3433
+ class SavedSearchesService {
3434
+ constructor() {
3435
+ this.userSettingsStore = inject(UserSettingsStore);
3436
+ this.queryParamsStore = inject(QueryParamsStore);
3437
+ }
3438
+ /**
3439
+ * Retrieves the list of saved searches from the user settings store.
3440
+ *
3441
+ * @returns {SavedSearch[]} An array of saved searches.
3442
+ */
3443
+ getSavedSearches() {
3444
+ return this.userSettingsStore.savedSearches();
3445
+ }
3446
+ /**
3447
+ * Saves the current search query to the user's saved searches.
3448
+ *
3449
+ * This method retrieves the current search text from the query parameters store.
3450
+ * If the search text is empty, it logs an error and exits.
3451
+ * Otherwise, it creates a new saved search object with the current URL, date, and search text.
3452
+ *
3453
+ * The new saved search is added to the beginning of the saved searches array.
3454
+ * If the array exceeds the maximum allowed storage, the oldest search is removed.
3455
+ *
3456
+ * Finally, the updated saved searches array is saved back to the user settings store,
3457
+ * and a success message is displayed to the user.
3458
+ *
3459
+ * @throws {Error} If saving an empty search is attempted.
3460
+ */
3461
+ saveSearch() {
3462
+ const { text } = getState(this.queryParamsStore);
3463
+ if (!text) {
3464
+ console.error('Saving empty search is not allowed');
3465
+ return;
3466
+ }
3467
+ const savedSearch = { url: window.location.hash.substring(1), date: new Date().toISOString(), display: text };
3468
+ const savedSearches = this.userSettingsStore.savedSearches();
3469
+ if (savedSearches.length >= SAVED_SEARCHES_MAX_STORAGE) {
3470
+ savedSearches.pop();
3471
+ }
3472
+ savedSearches.unshift(savedSearch);
3473
+ this.userSettingsStore.updateSavedSearches(savedSearches);
3474
+ toast.success('Search successfully saved');
3475
+ }
3476
+ /**
3477
+ * Updates the saved searches in the user settings store.
3478
+ *
3479
+ * @param savedSearches - An array of SavedSearch objects to update.
3480
+ */
3481
+ updateSavedSearches(savedSearches) {
3482
+ this.userSettingsStore.updateSavedSearches(savedSearches);
3483
+ }
3484
+ /**
3485
+ * Deletes a saved search from the user settings store.
3486
+ *
3487
+ * @param index - The index of the saved search to delete.
3488
+ */
3489
+ deleteSavedSearch(index) {
3490
+ this.userSettingsStore.deleteSavedSearch(index);
3491
+ }
3492
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: SavedSearchesService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3493
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: SavedSearchesService, providedIn: 'root' }); }
3494
+ }
3495
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: SavedSearchesService, decorators: [{
3496
+ type: Injectable,
3497
+ args: [{
3498
+ providedIn: 'root'
3499
+ }]
3500
+ }] });
3501
+
3502
+ const AGGREGATION_MAX_COUNT = 11;
3503
+ class QueryService {
3504
+ constructor() {
3505
+ this.http = inject(HttpClient);
3506
+ this.appStore = inject(AppStore);
3507
+ this.queryParamsStore = inject(QueryParamsStore);
3508
+ this.API_URL = `${globalConfig.backendUrl}/${globalConfig.apiPath}`;
3509
+ }
3510
+ /**
3511
+ * Performs a search query.
3512
+ *
3513
+ * @param q - The partial query object.
3514
+ * @param includeQueryParams - Indicates whether to include query parameters automatically in the request.
3515
+ * @param audit - The audit events object.
3516
+ * @returns An observable that emits the search results.
3517
+ */
3518
+ search(q, includeQueryParams = true, audit) {
3519
+ const $auditRecord = audit ? { auditEvents: [audit] } : undefined;
3520
+ const { app } = globalConfig;
3521
+ const currentQuery = q ?? this.queryParamsStore.getQuery();
3522
+ const query = includeQueryParams ? { ...this.queryParamsStore.getQuery(), ...currentQuery } : currentQuery;
3523
+ // Check if the search query is empty and if empty searches are allowed
3524
+ // If not allowed, return an empty result
3525
+ const allowEmptySearch = this.appStore.allowEmptySearch(query?.name || '');
3526
+ if (allowEmptySearch === false && query?.text === '') {
3527
+ return of({ records: [] });
3528
+ }
3529
+ const body = {
3530
+ app,
3531
+ query,
3532
+ $auditRecord
3533
+ };
3534
+ return this.http.post(this.API_URL + "/query", body)
3535
+ .pipe(catchError(error => {
3536
+ console.error("queryService.getResults failure - error: ", error);
3537
+ return of({});
3538
+ }), map((result) => {
3539
+ // update $hasMore flag
3540
+ result.aggregations.forEach((agg) => {
3541
+ agg.$hasMore = false;
3542
+ if (!agg.isDistribution && !agg.isTree && agg.items) {
3543
+ agg.$hasMore = agg.items.length === AGGREGATION_MAX_COUNT;
3544
+ }
3545
+ if (agg.isTree) {
3546
+ buildPathsAndLevels(agg);
3547
+ }
3548
+ });
3549
+ return result;
3550
+ }), map(result => {
3551
+ result.records?.map((article) => (Object.assign(article, { value: article.title, type: 'default' })));
3552
+ return result;
3553
+ }), map(result => {
3554
+ const r = ({ ...result, nextPage: result.page < Math.ceil(result.rowCount / result.pageSize) ? result.page + 1 : undefined, previousPage: result.page > 1 ? result.page - 1 : undefined });
3555
+ return r;
3556
+ }), tap(response => console.log("queryService.getResults success - data: ", response)));
3557
+ }
3558
+ /**
3559
+ * Performs a bulk search operation.
3560
+ *
3561
+ * @param q An array of Query objects representing the search queries.
3562
+ * @param audit An optional AuditEvents object for auditing purposes.
3563
+ * @returns An Observable that emits an array of Result objects.
3564
+ */
3565
+ bulkSearch(q, audit) {
3566
+ const { app = '' } = globalConfig;
3567
+ const body = {
3568
+ methods: [],
3569
+ propagateErrors: true,
3570
+ $auditRecord: { auditEvents: [audit] }
3571
+ };
3572
+ for (const query of q) {
3573
+ body.methods.push({
3574
+ method: "query",
3575
+ app,
3576
+ query
3577
+ });
3578
+ }
3579
+ return this.http.post(this.API_URL + "/multi", body)
3580
+ .pipe(catchError(error => {
3581
+ console.error("queryService.bulkSearch failure - error: ", error);
3582
+ return EMPTY;
3583
+ }), map(response => {
3584
+ // update $hasMore flag
3585
+ response.results.forEach((result) => {
3586
+ result.aggregations.forEach((agg) => {
3587
+ agg.$hasMore = false;
3588
+ if (!agg.isDistribution && !agg.isTree && agg.items) {
3589
+ agg.$hasMore = agg.items.length === AGGREGATION_MAX_COUNT;
3590
+ }
3591
+ if (agg.isTree) {
3592
+ buildPathsAndLevels(agg);
3593
+ }
3594
+ });
3595
+ });
3596
+ return response.results;
3597
+ }), map(results => {
3598
+ results.forEach(result => result.records?.map((article) => (Object.assign(article, { value: article.title, type: 'default' }))));
3599
+ return results;
3600
+ }), map(results => {
3601
+ return results.map(result => {
3602
+ const r = ({ ...result, nextPage: result.page < Math.ceil(result.rowCount / result.pageSize) ? result.page + 1 : undefined, previousPage: result.page > 1 ? result.page - 1 : undefined });
3603
+ return r;
3604
+ });
3605
+ }),
3606
+ // map(response => ResultsSchema.parse(response) as T),
3607
+ tap(response => console.log("queryService.bulkSearch success - data: ", response)));
3608
+ }
3609
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: QueryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3610
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: QueryService, providedIn: 'root' }); }
3611
+ }
3612
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: QueryService, decorators: [{
3613
+ type: Injectable,
3614
+ args: [{
3615
+ providedIn: 'root'
3616
+ }]
3617
+ }] });
3618
+
3619
+ class SearchService {
3620
+ constructor() {
3621
+ this.router = inject(Router);
3622
+ this.route = inject(ActivatedRoute);
3623
+ this.queryService = inject(QueryService);
3624
+ // Represents the last result of a search operation with getResult().
3625
+ this.result = {};
3626
+ this.queryParamsStore = inject(QueryParamsStore);
3627
+ this.userSettingsStore = inject(UserSettingsStore);
3628
+ this.injector = inject(Injector);
3629
+ }
3630
+ /**
3631
+ * Executes a search operation with the given commands and options.
3632
+ *
3633
+ * @param commands - An array of strings representing the search commands.
3634
+ * @param options - An optional object containing search options.
3635
+ * @param options.appendFilters - A boolean indicating whether to append existing filters to the search query. Defaults to true.
3636
+ * @param options.audit - An optional audit trail object to be stored in the navigation state.
3637
+ *
3638
+ * The method constructs query parameters based on the current state and the provided options,
3639
+ * then navigates to the specified commands with the constructed query parameters and audit trail.
3640
+ */
3641
+ search(commands, options = { appendFilters: true }) {
3642
+ const queryParams = {};
3643
+ const { text, filters = [], page, sort, tab } = getState(this.queryParamsStore);
3644
+ const { audit, appendFilters } = options;
3645
+ this.audit = audit;
3646
+ if (appendFilters) {
3647
+ queryParams.f = filters.length > 0 ? JSON.stringify(filters) : undefined;
3648
+ queryParams.p = page;
3649
+ queryParams.s = sort;
3650
+ queryParams.t = tab;
3651
+ queryParams.q = text;
3652
+ }
3653
+ // navigation state store the audit trail
3654
+ this.router.navigate(commands, { relativeTo: this.route, queryParamsHandling: 'merge', queryParams, state: { audit } });
3655
+ }
3656
+ /**
3657
+ * Retrieves the search result based on the provided query.
3658
+ *
3659
+ * @param q - A partial query object containing search parameters.
3660
+ * @returns An Observable of the search result.
3661
+ *
3662
+ * This method performs the following actions:
3663
+ * - Creates an audit event with the type "Search_Text" and the query text.
3664
+ * - Resets the audit property to undefined.
3665
+ * - Calls the search method of the queryService with the query, a flag to include the query name in records, and the audit event.
3666
+ * - Handles any errors by returning an empty Result object.
3667
+ * - Maps the search result to the service's result property.
3668
+ */
3669
+ getResult(q) {
3670
+ const audit = { type: "Search_Text", detail: { querytext: q?.text }, ...this.audit };
3671
+ this.audit = undefined;
3672
+ // add the query name to records, to have it available if we bookmark one
3673
+ return this.queryService.search(q, true, audit)
3674
+ .pipe(catchError(() => of({})), map((result) => this.result = result));
3675
+ }
3676
+ /**
3677
+ * Navigates to the specified page and returns the search result.
3678
+ * @param page - The page number to navigate to.
3679
+ * @returns A promise that resolves to the search result.
3680
+ */
3681
+ gotoPage(page) {
3682
+ this.queryParamsStore.patch({ page });
3683
+ this.search([], { audit: {
3684
+ type: "Search_GotoPage",
3685
+ detail: {
3686
+ page: page,
3687
+ fromresultid: this.result ? this.result.id : null
3688
+ }
3689
+ } });
3690
+ }
3691
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: SearchService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3692
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: SearchService, providedIn: 'root' }); }
3693
+ }
3694
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: SearchService, decorators: [{
3695
+ type: Injectable,
3696
+ args: [{
3697
+ providedIn: 'root'
3698
+ }]
3699
+ }] });
3700
+
3701
+ class DrawerService {
3702
+ constructor() {
3703
+ this.isOpened = new BehaviorSubject(false);
3704
+ this.isExtended = new BehaviorSubject(false);
3705
+ this.backdrop = inject(BackdropService);
3706
+ }
3707
+ open() {
3708
+ this.isOpened.next(true);
3709
+ }
3710
+ close() {
3711
+ this.collapse();
3712
+ this.isOpened.next(false);
3713
+ }
3714
+ toggle() {
3715
+ this.isOpened.getValue() ? this.close() : this.open();
3716
+ }
3717
+ extend() {
3718
+ this.isExtended.next(true);
3719
+ this.backdrop.show();
3720
+ }
3721
+ collapse() {
3722
+ this.backdrop.hide();
3723
+ this.isExtended.next(false);
3724
+ }
3725
+ toggleExtension() {
3726
+ this.isExtended.getValue() ? this.collapse() : this.extend();
3727
+ }
3728
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: DrawerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3729
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: DrawerService }); }
3730
+ }
3731
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: DrawerService, decorators: [{
3732
+ type: Injectable
3733
+ }] });
3734
+
3735
+ /**
3736
+ * Interceptor function that handles HTTP errors by logging out the user and redirecting to the login page.
3737
+ *
3738
+ * @param request - The HTTP request object.
3739
+ * @param next - The HTTP handler function.
3740
+ * @returns The modified request object.
3741
+ */
3742
+ const errorInterceptorFn = (request, next) => {
3743
+ const { useCredentials, loginPath, userOverrideActive } = globalConfig;
3744
+ const router = inject(Router);
3745
+ const lastUrlAfterNavigation = inject(NavigationService).urlAfterNavigation;
3746
+ if (request.url.includes('api/v1/audit.notify')) {
3747
+ return next(request);
3748
+ }
3749
+ return next(request).pipe(catchError$1((err) => {
3750
+ if ([401, 403].includes(err.status)) {
3751
+ // auto logout if 401 or 403 response returned from api
3752
+ if (useCredentials && !userOverrideActive) {
3753
+ logout();
3754
+ router.navigate([loginPath], { queryParams: { returnUrl: lastUrlAfterNavigation } });
3755
+ }
3756
+ else if (!userOverrideActive) {
3757
+ // logout the user and navigate to login page
3758
+ logout().then(() => login());
3759
+ }
3760
+ }
3761
+ return Promise.reject(err);
3762
+ }));
3763
+ };
3764
+
3765
+ /**
3766
+ * Intercepts HTTP requests and handles errors by displaying toast notifications.
3767
+ *
3768
+ * This interceptor checks if the request URL includes 'api/v1/audit.notify'. If it does,
3769
+ * the request is passed through without any modifications. For other requests, it catches
3770
+ * errors and displays a toast notification for specific HTTP status codes (400, 403, 500, 503).
3771
+ *
3772
+ * @param request - The outgoing HTTP request.
3773
+ * @param next - The next handler in the HTTP request pipeline.
3774
+ * @returns An observable that either passes the request through or handles errors with toast notifications.
3775
+ */
3776
+ const toastInterceptorFn = (request, next) => {
3777
+ if (request.url.includes('api/v1/audit.notify')) {
3778
+ return next(request);
3779
+ }
3780
+ return next(request).pipe(catchError$1((err) => {
3781
+ const { status, statusText, error } = err;
3782
+ if ([400, 403, 500, 503].includes(status)) {
3783
+ const { errorMessage = err.statusText, errorCodeText = `Error ${status}` } = error;
3784
+ toast.error(statusText, { description: `${errorCodeText}: ${errorMessage}`, closeButton: true, duration: 5000 });
3785
+ }
3786
+ return Promise.reject(err);
3787
+ }));
3788
+ };
3789
+
3790
+ // export * from "./audit.interceptor";
3791
+
3792
+ /**
3793
+ * The HighlightWordPipe class is a custom pipe in the Atomic Angular library.
3794
+ * It is used to highlight a specific word within a given text.
3795
+ *
3796
+ * @remarks
3797
+ * This pipe takes in a `value` string, a `word` string to highlight, and an optional `clipBy` number to limit the length of the highlighted text.
3798
+ * It returns an array of `HighlightWords.Chunk` objects representing the highlighted portions of the text.
3799
+ *
3800
+ * @example
3801
+ * ```html
3802
+ * <div [innerHTML]="text | highlightWord:'search':10"></div>
3803
+ * ```
3804
+ *
3805
+ */
3806
+ class HighlightWordPipe {
3807
+ transform(value, word, clipBy) {
3808
+ return highlightWords({ text: value, query: word, clipBy });
3809
+ }
3810
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: HighlightWordPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
3811
+ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "18.2.5", ngImport: i0, type: HighlightWordPipe, isStandalone: true, name: "highlightWord" }); }
3812
+ }
3813
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: HighlightWordPipe, decorators: [{
3814
+ type: Pipe,
3815
+ args: [{
3816
+ name: 'highlightWord',
3817
+ standalone: true,
3818
+ pure: true
3819
+ }]
3820
+ }] });
3821
+
3822
+ /**
3823
+ * Pipe that transforms a collection of strings into a corresponding icon class.
3824
+ *
3825
+ * This pipe uses the first element of the collection to determine the icon class
3826
+ * based on the source name. If the collection is empty or undefined, it returns
3827
+ * a default icon class.
3828
+ *
3829
+ * @deprecated This pipe is deprecated and will be removed in the future.
3830
+ *
3831
+ * @param collection - An array of strings representing the collection.
3832
+ * @returns A string representing the icon class.
3833
+ */
3834
+ class SourceIconPipe {
3835
+ constructor() {
3836
+ this.appStore = inject(AppStore);
3837
+ }
3838
+ transform(collection) {
3839
+ if (collection === undefined || collection.length === 0) {
3840
+ return 'far fa-file';
3841
+ }
3842
+ const name = collection[0].split("/")[1];
3843
+ const sources = this.appStore.sources();
3844
+ // workplace search uses a different format for sources
3845
+ if (Array.isArray(sources)) {
3846
+ return sources.find((source) => source.name === name)?.icon || 'far fa-file';
3847
+ }
3848
+ return 'far fa-file';
3849
+ }
3850
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: SourceIconPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
3851
+ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "18.2.5", ngImport: i0, type: SourceIconPipe, isStandalone: true, name: "sourceIcon" }); }
3852
+ }
3853
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: SourceIconPipe, decorators: [{
3854
+ type: Pipe,
3855
+ args: [{
3856
+ name: 'sourceIcon',
3857
+ standalone: true,
3858
+ pure: true
3859
+ }]
3860
+ }] });
3861
+
3862
+ /**
3863
+ * Resolves the name of the default query from the application store.
3864
+ *
3865
+ * @param route - The current route snapshot.
3866
+ * @param state - The current router state snapshot.
3867
+ * @returns The name of the default query or an empty string if not available.
3868
+ */
3869
+ const queryNameResolver = (route, state) => {
3870
+ const appStore = inject(AppStore);
3871
+ return appStore.getDefaultQuery()?.name || '';
3872
+ };
3873
+
3874
+ // pre-configured highlights's colors
3875
+ const HIGHLIGHTS = new InjectionToken('highlights', {
3876
+ factory: () => [
3877
+ {
3878
+ name: 'company',
3879
+ color: 'white',
3880
+ bgColor: '#FF7675'
3881
+ },
3882
+ {
3883
+ name: 'geo',
3884
+ color: 'white',
3885
+ bgColor: '#74B9FF'
3886
+ },
3887
+ {
3888
+ name: 'person',
3889
+ color: 'white',
3890
+ bgColor: '#00ABB5'
3891
+ },
3892
+ {
3893
+ name: 'extractslocations',
3894
+ color: 'black',
3895
+ bgColor: '#fffacd'
3896
+ },
3897
+ {
3898
+ name: 'matchlocations',
3899
+ color: 'black',
3900
+ bgColor: '#ff0'
3901
+ }
3902
+ ],
3903
+ });
3904
+
3905
+ class AggregationsService {
3906
+ constructor() {
3907
+ this.http = inject(HttpClient);
3908
+ this.queryService = inject(QueryService);
3909
+ this.appStore = inject(AppStore);
3910
+ this.aggregationsStore = inject(AggregationsStore);
3911
+ this.API_URL = `${globalConfig.backendUrl}/${globalConfig.apiPath}`;
3912
+ }
3913
+ loadMore(query, aggregation, audit) {
3914
+ const { app } = globalConfig;
3915
+ // skip cached items for computation
3916
+ const skip = aggregation.items?.filter(i => !(i.$cached))?.length || 0;
3917
+ const count = 10;
3918
+ const aggregations = { [aggregation.name]: { skip, count } };
3919
+ const q = { ...query, action: "aggregate", aggregations };
3920
+ // add audit record if available
3921
+ const $auditRecord = audit ? { auditEvents: [audit] } : undefined;
3922
+ const body = {
3923
+ app,
3924
+ query: q,
3925
+ $auditRecord
3926
+ };
3927
+ return this.http.post(this.API_URL + "/query", body).pipe(map(result => {
3928
+ const { aggregations } = result;
3929
+ const agg = aggregations[0];
3930
+ if (agg.items) {
3931
+ agg.$hasMore = agg.items.length > count;
3932
+ agg.items = [...aggregation.items, ...agg.items];
3933
+ }
3934
+ else {
3935
+ aggregation.$hasMore = false;
3936
+ return aggregation;
3937
+ }
3938
+ return agg;
3939
+ }), catchError(error => {
3940
+ console.log("aggregation.loadMore failure - error: ", error);
3941
+ return throwError(() => new Error(error));
3942
+ }), tap(response => console.log("aggregation.loadMore success - data: ", response)));
3943
+ }
3944
+ open(query, aggregation, item) {
3945
+ const value = `/${item.$path}/*`;
3946
+ const expression = `${aggregation.column}: ${escapeExpr(value)}`;
3947
+ const q = {
3948
+ ...query,
3949
+ action: "open",
3950
+ open: [{ expression, aggregation: aggregation.name }]
3951
+ };
3952
+ return this.queryService.search(q).pipe(map(results => {
3953
+ // find the node in the tree and replace it with the new one
3954
+ const items = results.aggregations[0].items;
3955
+ const newNode = this.findNode(items, item.$path);
3956
+ if (!newNode) {
3957
+ return aggregation;
3958
+ }
3959
+ // mark the node as opened
3960
+ newNode.$opened = true;
3961
+ return this.replaceInTree(aggregation, item.$path, newNode);
3962
+ }));
3963
+ }
3964
+ findNode(items, path = "") {
3965
+ if (!items || items.length === 0) {
3966
+ return undefined;
3967
+ }
3968
+ for (const item of items) {
3969
+ if (item.$path === path) {
3970
+ return item;
3971
+ }
3972
+ if (item.items) {
3973
+ const found = this.findNode(item.items, path);
3974
+ if (found) {
3975
+ return found;
3976
+ }
3977
+ }
3978
+ }
3979
+ return undefined;
3980
+ }
3981
+ replaceInTree(tree, valueToFind = "", newValue) {
3982
+ if (tree.$path === valueToFind) {
3983
+ return newValue;
3984
+ }
3985
+ if (tree.hasChildren && tree.items) {
3986
+ for (let i = 0; i < tree.items.length; i++) {
3987
+ tree.items[i] = this.replaceInTree(tree.items[i], valueToFind, newValue);
3988
+ }
3989
+ }
3990
+ return tree;
3991
+ }
3992
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: AggregationsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3993
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: AggregationsService, providedIn: 'root' }); }
3994
+ }
3995
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: AggregationsService, decorators: [{
3996
+ type: Injectable,
3997
+ args: [{
3998
+ providedIn: 'root'
3999
+ }]
4000
+ }] });
4001
+
4002
+ class JsonMethodPluginService {
4003
+ constructor() {
4004
+ this.http = inject(HttpClient);
4005
+ this.API_URL = `${globalConfig.backendUrl}/${globalConfig.apiPath}`;
4006
+ }
4007
+ /**
4008
+ * Call a JsonMethod plugin using an HTTP POST
4009
+ *
4010
+ * @param method The name of the JsonMethod plugin
4011
+ * @param query Parameters to pass to the plugin
4012
+ * @param options HTTP options for the request
4013
+ * @returns An observable of the plugin's return value
4014
+ */
4015
+ post(method, query, options) {
4016
+ if (!isObject(query)) {
4017
+ return throwError(() => ({ error: "invalid query object" }));
4018
+ }
4019
+ return this.http.post(`${this.API_URL}/plugin/${method}`, query, options);
4020
+ }
4021
+ /**
4022
+ * Call a JsonMethod plugin using an HTTP GET
4023
+ *
4024
+ * @param method The name of the JsonMethod plugin
4025
+ * @param query Parameters to pass to the plugin
4026
+ * @param options HTTP options for the request
4027
+ * @returns An observable of the plugin's return value
4028
+ */
4029
+ get(method, query, options) {
4030
+ const params = new HttpParams();
4031
+ for (const key in query) {
4032
+ if (query[key] !== undefined) {
4033
+ params.set(key, query[key]);
4034
+ }
4035
+ }
4036
+ return this.http.get(`${this.API_URL}/plugin/${method}`, {
4037
+ params: params,
4038
+ ...options
4039
+ });
4040
+ }
4041
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: JsonMethodPluginService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
4042
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: JsonMethodPluginService, providedIn: 'root' }); }
4043
+ }
4044
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: JsonMethodPluginService, decorators: [{
4045
+ type: Injectable,
4046
+ args: [{
4047
+ providedIn: 'root'
4048
+ }]
4049
+ }] });
4050
+
4051
+ const PREVIEW_CONFIG = new InjectionToken("Preview configuration", {
4052
+ factory: () => ({
4053
+ allowWorker: false
4054
+ })
4055
+ });
4056
+ class PreviewService {
4057
+ constructor(highlights) {
4058
+ this.highlights = highlights;
4059
+ this.http = inject(HttpClient);
4060
+ this.injector = inject(Injector);
4061
+ this.API_URL = `${globalConfig.backendUrl}/${globalConfig.apiPath}`;
4062
+ this.applicationStore = inject(ApplicationStore);
4063
+ this.selectionStore = inject(SelectionStore);
4064
+ this.queryParamsStore = inject(QueryParamsStore);
4065
+ this.appStore = inject(AppStore);
4066
+ this.sanitizer = inject(DomSanitizer);
4067
+ this.queryService = inject(QueryService);
4068
+ this.searchService = inject(SearchService);
4069
+ this.auditService = inject(AuditService);
4070
+ this.events = new EventEmitter();
4071
+ // ! worker
4072
+ this.onMessage = new Subject();
4073
+ this.allowWoker = inject(PREVIEW_CONFIG).allowWorker;
4074
+ this.highlightCategory = "extractslocations";
4075
+ this.extracts = ["matchlocations", "extractslocations", "matchingpassages"];
4076
+ this.entities = ["company", "geo", "person"];
4077
+ window.addEventListener('message', this.receiveMessage.bind(this), false);
4078
+ }
4079
+ ngOnDestroy() {
4080
+ window.removeEventListener('message', this.receiveMessage.bind(this), false);
4081
+ }
4082
+ /**
4083
+ * Handles incoming messages from a MessageEvent.
4084
+ *
4085
+ * @param event - The MessageEvent containing the message data.
4086
+ *
4087
+ * The function processes messages of type 'ready' and 'get-html-results'.
4088
+ *
4089
+ * - For 'ready' messages:
4090
+ * - Initializes the preview iframe with the app name and highlights.
4091
+ * - If preview data is available, retrieves HTML content based on the current selection.
4092
+ *
4093
+ * - For 'get-html-results' messages:
4094
+ * - Updates the application store with the extracted HTML results.
4095
+ * - If no extracts are found, updates the application store with an empty array.
4096
+ */
4097
+ receiveMessage(event) {
4098
+ const message = event.data;
4099
+ // the webworker needs to be initialized with the app name to be able to load the correct webworker file (worker.js)
4100
+ const { app } = globalConfig;
4101
+ // when the iframe is ready, we can send the init message
4102
+ if (message.type === 'ready') {
4103
+ // Initialize the preview iframe with the app name and highlights
4104
+ // app name is used to intialize the webworker
4105
+ this.sendMessage({ action: "init", highlights: this.highlights, appname: app });
4106
+ if (this.previewData) {
4107
+ const { id } = getState(this.selectionStore);
4108
+ this.events.emit('fetching');
4109
+ this.retrieveHtmlContent(id, this.highlightCategory, this.previewData);
4110
+ }
4111
+ }
4112
+ // when the iframe sends back the html content of the extracts (no worker)
4113
+ if (message.type === 'get-html-results') {
4114
+ const { id } = getState(this.selectionStore);
4115
+ if (id) {
4116
+ console.info("no worker");
4117
+ const extracts = this.fetchExtracts(id, message.data, this.previewData);
4118
+ this.applicationStore.updateExtracts(this.previewData?.record.id || '', extracts);
4119
+ this.events.emit('fetched');
4120
+ }
4121
+ }
4122
+ // when the iframe sends back the html content of the extracts (with worker)
4123
+ // we update the extracts in the application store
4124
+ if (message.type === 'get-html-results-webworker') {
4125
+ console.info("with worker");
4126
+ if (message.data.extracts.length === 0) {
4127
+ this.events.emit('fetched');
4128
+ this.applicationStore.updateExtracts(this.previewData?.record.id || '', []);
4129
+ return;
4130
+ }
4131
+ const extracts = message.data.extracts.map((item) => ({
4132
+ ...item,
4133
+ text: this.sanitizer.bypassSecurityTrustHtml(item.text)
4134
+ }));
4135
+ this.applicationStore.updateExtracts(this.previewData?.record.id || '', extracts);
4136
+ this.events.emit('fetched');
4137
+ }
4138
+ }
4139
+ /**
4140
+ * Generates a preview of the document with the given ID, applying the specified query parameters,
4141
+ * custom highlights, and audit events.
4142
+ *
4143
+ * @param id - The unique identifier of the document to preview.
4144
+ * @param q - Partial query parameters to customize the preview request.
4145
+ * @param customHighlights - Optional array of custom highlights to apply to the preview.
4146
+ * @param audit - Optional audit events to merge with the default audit record.
4147
+ * @returns An Observable that emits the preview data.
4148
+ */
4149
+ preview(id, q, customHighlights, audit) {
4150
+ const detail = this.getAuditPreviewDetail(id, q);
4151
+ const defaultAudit = {
4152
+ type: "Doc_Preview",
4153
+ detail
4154
+ };
4155
+ // merge the default audit record with the provided audit record
4156
+ const $auditRecord = audit ? { auditEvents: [{ ...defaultAudit, ...audit }] } : { auditEvents: [defaultAudit] };
4157
+ const { app } = globalConfig;
4158
+ const query = { ...this.queryParamsStore.getQuery(), ...q };
4159
+ const browserUrl = `${window.location.origin}${window.location.pathname}`;
4160
+ const body = {
4161
+ app,
4162
+ action: "get",
4163
+ id,
4164
+ query: { name: query.name, text: query.text },
4165
+ browserUrl,
4166
+ customHighlights,
4167
+ $auditRecord
4168
+ };
4169
+ return this.http.post(this.API_URL + "/preview", body)
4170
+ .pipe(tap((data) => this.setPreviewData(data)), catchError(error => {
4171
+ console.error("queryService.preview failure - error: ", error);
4172
+ return EMPTY;
4173
+ }));
4174
+ }
4175
+ /**
4176
+ * Closes the preview with the specified ID and updates the audit log.
4177
+ *
4178
+ * @param id - The ID of the preview to close.
4179
+ * @param query - The partial query object used to retrieve the preview detail.
4180
+ */
4181
+ close(id, query) {
4182
+ const detail = this.getAuditPreviewDetail(id, query);
4183
+ const auditEvent = {
4184
+ type: "Preview_Close" /* AuditEventType.Preview_Close */,
4185
+ detail
4186
+ };
4187
+ Audit.notify(auditEvent);
4188
+ }
4189
+ /**
4190
+ * Previews an article in a new browser's tab.
4191
+ *
4192
+ * @param article - The article to preview.
4193
+ */
4194
+ openExternal(article) {
4195
+ const query = this.queryParamsStore.getQuery();
4196
+ const detail = this.getAuditPreviewDetail(article.id, query);
4197
+ const result = this.searchService.result;
4198
+ this.auditService.notifyDocument("Click_ResultLink", article, result || detail.resultid || "", {
4199
+ querytext: detail.querytext,
4200
+ querylang: detail.querylang,
4201
+ score: detail.score
4202
+ }, {
4203
+ queryhash: result ? result.rfmQueryHash : undefined,
4204
+ querytext: detail.querytext,
4205
+ querylang: detail.querylang
4206
+ });
4207
+ window.open(article.url1, '_blank', 'noopener noreferrer');
4208
+ }
4209
+ /**
4210
+ * Sets the iframe window object.
4211
+ *
4212
+ * @param iframe - The window object of the iframe or null to unset.
4213
+ */
4214
+ setIframe(iframe) {
4215
+ this.iframe = iframe;
4216
+ }
4217
+ /**
4218
+ * Sets the preview data and updates the highlight category based on the provided data.
4219
+ *
4220
+ * @param data - The preview data to be set.
4221
+ *
4222
+ * If the provided data contains highlights per category with 'matchingpassages' having values,
4223
+ * the highlight category is set to "matchingpassages". Otherwise, it is set to "extractslocations".
4224
+ */
4225
+ setPreviewData(data) {
4226
+ this.previewData = data;
4227
+ if (data) {
4228
+ this.highlightCategory = data.highlightsPerCategory?.['matchingpassages']?.values.length ? "matchingpassages" : "extractslocations";
4229
+ }
4230
+ }
4231
+ /**
4232
+ * Sends a message to the iframe if it exists.
4233
+ *
4234
+ * @param message - The message to be sent. It can be of any type.
4235
+ */
4236
+ sendMessage(message) {
4237
+ if (this.iframe) {
4238
+ this.iframe.postMessage(message, '*');
4239
+ }
4240
+ }
4241
+ /**
4242
+ * Get preview id for an entity value for an index
4243
+ * @param entity the entity type (i.e. "geo")
4244
+ * @param value the entity value (i.e. "AUSTRALIA")
4245
+ * @param index the entity index from its list
4246
+ * @returns the html id
4247
+ */
4248
+ getEntityId(entity, value, index) {
4249
+ if (!this.previewData)
4250
+ return undefined;
4251
+ return this.previewData.highlightsPerLocation
4252
+ .filter(h => h.values.find(v => v === value))[index].positionInCategories[entity];
4253
+ }
4254
+ /**
4255
+ * Send a message to the prewiew iFrame with the required data to retrieve HTML content for a specific highlight category
4256
+ *
4257
+ * @param id - The unique identifier for the request.
4258
+ * @param highlightCategory - The category of highlights to retrieve.
4259
+ * @param previewData - The data containing highlights and their locations.
4260
+ */
4261
+ retrieveHtmlContent(id, highlightCategory, previewData) {
4262
+ // Generate the list of items we want to retrieve
4263
+ const ids = previewData.highlightsPerCategory[highlightCategory]?.values[0]?.locations.map((_, i) => `${highlightCategory}_${i}`);
4264
+ if (this.allowWoker) {
4265
+ // if the worker is allowed, we send the message to the worker
4266
+ this.sendMessage({ action: 'get-html-webworker', id, ids, previewData });
4267
+ }
4268
+ else {
4269
+ this.sendMessage({ action: 'get-html', id, ids, previewData });
4270
+ }
4271
+ }
4272
+ /**
4273
+ * Sends a message to zoom in.
4274
+ *
4275
+ * This method triggers a "zoom-in" action by sending a message
4276
+ * with the specified action type.
4277
+ */
4278
+ zoomIn() {
4279
+ this.sendMessage({ action: "zoom-in" });
4280
+ }
4281
+ /**
4282
+ * Sends a message to zoom out the preview.
4283
+ *
4284
+ * This method triggers a "zoom-out" action by sending a message
4285
+ * to the relevant service or component.
4286
+ */
4287
+ zoomOut() {
4288
+ this.sendMessage({ action: "zoom-out" });
4289
+ }
4290
+ /**
4291
+ * Toggles the highlights based on the provided flags for extracts and entities.
4292
+ *
4293
+ * @param extracts - A boolean flag indicating whether to include extracts highlights.
4294
+ * @param entities - A boolean flag indicating whether to include entities highlights.
4295
+ */
4296
+ toggle(extracts, entities) {
4297
+ const extractsHighlights = extracts ? this.highlights.filter(h => this.extracts.includes(h.name)) : [];
4298
+ const entitiesHighlights = entities ? this.highlights.filter(h => this.entities.includes(h.name)) : [];
4299
+ const highlights = [...extractsHighlights, ...entitiesHighlights];
4300
+ this.sendMessage({ action: "highlight", highlights });
4301
+ }
4302
+ getAuditPreviewDetail(id, q) {
4303
+ const results = this.searchService.result;
4304
+ const queryLanguage = results?.queryAnalysis?.queryLanguage
4305
+ || q?.questionLanguage
4306
+ || this.appStore.getQueryByName(q.name)?.questionLanguage;
4307
+ const record = this.previewData?.record;
4308
+ const collectionColumn = record?.collection;
4309
+ const collection = !!collectionColumn ? collectionColumn[0] : id.split("|")[0];
4310
+ const rank = !!record ? record.rank : 0;
4311
+ const passages = record?.matchingpassages?.passages;
4312
+ const score = passages && passages.length ? passages[0].score : undefined;
4313
+ return {
4314
+ docid: id,
4315
+ rank,
4316
+ collection,
4317
+ source: collection.split("|")[0],
4318
+ resultid: results.id,
4319
+ querylang: queryLanguage,
4320
+ querytext: q?.text,
4321
+ filename: record?.filename,
4322
+ fileext: record?.fileext,
4323
+ score
4324
+ };
4325
+ }
4326
+ fetchExtracts(id, extracts, data) {
4327
+ // extracts contains the html of extracts in chronological order
4328
+ // locations contains the list of start positions sorted by score
4329
+ const locations = data.highlightsPerCategory[this.highlightCategory]?.values[0]?.locations || [];
4330
+ // first extract all the extracts locations
4331
+ let extractslocations = locations.map((l, relevanceIndex) => ({
4332
+ startIndex: l.start,
4333
+ relevanceIndex
4334
+ }));
4335
+ // sort them by start index
4336
+ extractslocations.sort((a, b) => a.startIndex - b.startIndex);
4337
+ // then extract the text of each extract
4338
+ extractslocations = extractslocations.map((ex, textIndex) => ({
4339
+ ...ex,
4340
+ textIndex: textIndex,
4341
+ text: extracts[textIndex] || "",
4342
+ id: `${this.highlightCategory}_${textIndex}`,
4343
+ }));
4344
+ // then sanitize the text and remove empty extracts
4345
+ const _extracts = extractslocations.filter(item => item.text.trim().length > 0).map(item => ({
4346
+ ...item,
4347
+ text: this.sanitizer.bypassSecurityTrustHtml(item.text)
4348
+ }));
4349
+ // finally sort them by relevance index
4350
+ _extracts.sort((a, b) => a.relevanceIndex - b.relevanceIndex);
4351
+ return _extracts;
4352
+ }
4353
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: PreviewService, deps: [{ token: HIGHLIGHTS }], target: i0.ɵɵFactoryTarget.Injectable }); }
4354
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: PreviewService, providedIn: 'root' }); }
4355
+ }
4356
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: PreviewService, decorators: [{
4357
+ type: Injectable,
4358
+ args: [{
4359
+ providedIn: 'root'
4360
+ }]
4361
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
4362
+ type: Inject,
4363
+ args: [HIGHLIGHTS]
4364
+ }] }] });
4365
+
4366
+ class TextChunkService {
4367
+ constructor() {
4368
+ this.http = inject(HttpClient);
4369
+ this.API_URL = `${globalConfig.backendUrl}/${globalConfig.apiPath}`;
4370
+ }
4371
+ /**
4372
+ * Retrieves text chunks based on the provided parameters.
4373
+ *
4374
+ * @param id - The ID of the record.
4375
+ * @param textChunks - An array of TextLocation objects representing the location of the text chunks.
4376
+ * @param highlights - An array of strings representing the highlights to be applied to the text chunks.
4377
+ * @param query - The query used to retrieve the text chunks.
4378
+ * @param leftSentencesCount - The number of sentences to include before the text chunks.
4379
+ * @param rightSentencesCount - The number of sentences to include after the text chunks.
4380
+ * @returns An Observable that emits an array of TextChunk objects.
4381
+ */
4382
+ getTextChunks(id, textChunks, highlights, query, leftSentencesCount, rightSentencesCount) {
4383
+ const body = {
4384
+ id,
4385
+ textChunks,
4386
+ highlights,
4387
+ query,
4388
+ leftSentencesCount,
4389
+ rightSentencesCount
4390
+ };
4391
+ return this.http.post(this.API_URL + "/doc.textchunks", body)
4392
+ .pipe(map((response) => response.chunks), catchError(error => {
4393
+ console.error("TextChunkService.getTextChunks failure - error: ", error);
4394
+ return [];
4395
+ }));
4396
+ }
4397
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: TextChunkService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
4398
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: TextChunkService, providedIn: 'root' }); }
4399
+ }
4400
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: TextChunkService, decorators: [{
4401
+ type: Injectable,
4402
+ args: [{
4403
+ providedIn: 'root'
4404
+ }]
4405
+ }] });
4406
+
4407
+ /*
4408
+ * Public API Surface of atomic
4409
+ */
4410
+
4411
+ /*
4412
+ * Public API Surface of atomic
4413
+ */
4414
+
4415
+ /**
4416
+ * Generated bundle index. Do not edit.
4417
+ */
4418
+
4419
+ export { AGGREGATIONS_NAMES, AGGREGATIONS_NAMES_PRESET_DEFAULT, AggregationsService, AggregationsStore, AppService, AppStore, ApplicationService, ApplicationStore, AuditService, AuthGuard, AutocompleteService, BackdropService, DrawerService, DrawerStackService, DropdownComponent, DropdownInputComponent, DropdownListComponent, HIGHLIGHTS, HighlightWordPipe, InfinityScrollDirective, InitializationGuard, InlineWorker, JsonMethodPluginService, LabelService, MenuComponent, MenuItemComponent, MetadataComponent, NavigationService, PreviewService, PrincipalService, PrincipalStore, QueryParamsStore, QueryService, ROUTE_COMPONENTS, SIGNAL_R_LOG_LEVEL, SIGNAL_R_TRANSPORTS, SavedSearchesService, SearchService, SelectArticleOnClickDirective, SelectionHistoryService, SelectionService, SelectionStore, ShowBookmarkDirective, SignalRWebService, SourceIconPipe, THEMES, TextChunkService, ThemeProviderDirective, ThemeSelectorComponent, ThemeStore, ThemeToggleComponent, UserSettingsStore, applyThemeToNativeElement, auditInterceptorFn, authInterceptorFn, bodyInterceptorFn, buildQuery, cn, debouncedSignal, errorInterceptorFn, getCurrentPath, getCurrentQueryName, getQueryNameFromRoute, processCssVars, queryNameResolver, themeColorNameToCssVariable, themeColorsToCssVariables, toastInterceptorFn, withAggregationsFeatures, withAppCustomizationFeatures, withAppFeatures, withApplicationFeatures, withAssistantFeatures, withBasketsFeatures, withBookmarkFeatures, withExtractsFeatures, withPrincipalFeatures, withQueryParamsFeatures, withRecentSearchFeatures, withSavedSearchFeatures, withSelectionFeatures, withThemeBodyHook, withThemes, withThemesFeatures, withUserSettingsFeatures };
4420
+ //# sourceMappingURL=sinequa-atomic-angular.mjs.map