@sinequa/atomic-angular 1.2.5 → 1.5.0

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.
@@ -1,26 +1,26 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, inject, HostBinding, Component, Pipe, InjectionToken, computed, ChangeDetectorRef, DestroyRef, LOCALE_ID, Inject, Optional, input, output, signal, effect, assertInInjectionContext, runInInjectionContext, Injector, EventEmitter, Directive, viewChild, ElementRef, afterNextRender, untracked, linkedSignal, model, TemplateRef, HostListener, Renderer2, contentChildren, contentChild, booleanAttribute, ChangeDetectionStrategy, resource, ViewContainerRef, viewChildren, numberAttribute, afterEveryRender } from '@angular/core';
3
- import { BehaviorSubject, Subscription, firstValueFrom, catchError, map, Subject, of, tap, EMPTY, throwError, filter, shareReplay, fromEvent, debounceTime, from, switchMap } from 'rxjs';
2
+ import { Injectable, inject, HostBinding, Component, Pipe, InjectionToken, computed, ChangeDetectorRef, DestroyRef, LOCALE_ID, Inject, Optional, input, output, signal, effect, assertInInjectionContext, runInInjectionContext, Injector, EventEmitter, Directive, viewChild, ElementRef, afterNextRender, untracked, linkedSignal, model, TemplateRef, HostListener, Renderer2, contentChildren, contentChild, booleanAttribute, resource, ChangeDetectionStrategy, ViewContainerRef, numberAttribute, viewChildren, afterRenderEffect, afterEveryRender } from '@angular/core';
3
+ import { BehaviorSubject, Subscription, firstValueFrom, catchError, map, Subject, of, tap, EMPTY, throwError, filter, shareReplay, switchMap, from, fromEvent, debounceTime } from 'rxjs';
4
4
  import { TranslocoService, TranslocoPipe, provideTranslocoScope } from '@jsverse/transloco';
5
- import { DropdownComponent, DropdownContentComponent, InputComponent, ButtonComponent, cn, FaIconComponent, EllipsisIcon, ChevronRightIcon, MenuComponent, MenuContentComponent, MenuItemComponent, LinkComponent, BadgeComponent, DialogComponent, DialogHeaderComponent, DialogTitleComponent, DialogContentComponent, DialogFooterComponent, ListItemComponent, SquareCheckIcon, SquareMinusIcon, SquareIcon, SwitchComponent, SelectOptionDirective, DialogService, XMarkIcon, InboxIcon, SparklesIcon, FileOutputIcon, TabsComponent, TabsListComponent, TabComponent, FrownIcon, ChevronLeftIcon, ChevronsLeftIcon, ChevronsRightIcon, ArrowUpAzIcon, ArrowDownZaIcon, ArrowUpRightFromSquareIcon, ToggleRightIcon, ToggleLeftIcon, Separator, SheetCloseDirective, ArrowLeftIcon, SheetService, DateRangePickerDirective, DatepickerDirective, InputGroupInput, InputGroupComponent, InputGroupAddonComponent, SearchIcon, FilterIcon, TriangleAlertIcon, FilterXIcon, IconButtonComponent, HighlighterIcon, TagsIcon, SpinnerIcon, MagnifyingGlassIcon, LoadingCircleIcon, CircleCheckIcon, PopoverComponent, BellIcon, TrashIcon, BarsIcon, CardComponent, CardHeaderComponent, CardContentComponent, CardFooterComponent, EyeSlashIcon, EyeIcon, BookmarkIcon, PopoverContentComponent, UserIcon, FolderIcon, VerticalDividerComponent, CommentIcon, ThumbsUpIcon, ThumbsDownIcon, ListFilterIcon, BreakpointObserverService, TrashCanIcon, CircleXIcon, InfoCircleIcon, HorizontalDividerComponent, HistoryIcon, StarIcon, FlagEnglishIcon, FlagFrenchIcon, EditIcon, UndoIcon, SaveIcon, AvatarComponent, AvatarFallbackComponent, AvatarImageComponent } from '@sinequa/ui';
5
+ import { DropdownComponent, DropdownContentComponent, InputComponent, ButtonComponent, cn, FaIconComponent, EllipsisIcon, ChevronRightIcon, MenuComponent, MenuContentComponent, MenuItemComponent, LinkComponent, BadgeComponent, DialogComponent, DialogHeaderComponent, DialogTitleComponent, DialogContentComponent, DialogFooterComponent, ListItemComponent, SquareCheckIcon, SquareMinusIcon, SquareIcon, SwitchComponent, SelectOptionDirective, DialogService, XMarkIcon, InboxIcon, SparklesIcon, FileOutputIcon, TabsComponent, TabsListComponent, TabComponent, SidebarMenuComponent, SidebarMenuItemComponent, SidebarMenuButtonComponent, TooltipDirective, FrownIcon, ChevronLeftIcon, ChevronsLeftIcon, ChevronsRightIcon, ArrowUpAzIcon, ArrowDownZaIcon, ArrowUpRightFromSquareIcon, ToggleRightIcon, ToggleLeftIcon, Separator, SheetCloseDirective, ArrowLeftIcon, SheetService, DateRangePickerDirective, DatepickerDirective, FilterIcon, FilterXIcon, IconButtonComponent, HighlighterIcon, TagsIcon, SpinnerIcon, MagnifyingGlassIcon, LoadingCircleIcon, CircleCheckIcon, PopoverComponent, BellIcon, TrashIcon, BarsIcon, CardComponent, CardHeaderComponent, CardContentComponent, CardFooterComponent, InputGroupInput, InputGroupComponent, InputGroupAddonComponent, EyeSlashIcon, EyeIcon, BookmarkIcon, PopoverContentComponent, UserIcon, FolderIcon, VerticalDividerComponent, CommentIcon, ThumbsUpIcon, ThumbsDownIcon, DropdownDirective, SearchIcon, TriangleAlertIcon, ListFilterIcon, BreakpointObserverService, TrashCanIcon, CircleXIcon, InfoCircleIcon, HorizontalDividerComponent, HistoryIcon, StarIcon, FlagEnglishIcon, FlagFrenchIcon, EditIcon, UndoIcon, SaveIcon, AvatarComponent, AvatarFallbackComponent, AvatarImageComponent } from '@sinequa/ui';
6
6
  import highlightWords from 'highlight-words';
7
- import { ActivatedRoute, Router, NavigationEnd, RouterLink, RouterModule } from '@angular/router';
7
+ import { ActivatedRoute, Router, NavigationEnd, RouterLink, RouterLinkActive, RouterModule } from '@angular/router';
8
8
  import { withDevtools } from '@angular-architects/ngrx-toolkit';
9
9
  import { signalStore, signalStoreFeature, withState, withMethods, patchState, getState, withComputed } from '@ngrx/signals';
10
10
  import { EngineType, fetchApp, extraColumns, sysLang, globalConfig, getQueryParamsFromUrl, clearSessionTokens, login, info, warn, error, setGlobalConfig, notify, addConcepts, queryParamsFromUrl, patchUserSettings, deleteUserSettings, fetchUserSettings, buildPathsAndLevels, escapeExpr, isAuthenticated, isExpired, debug, AuditEventType, fetchSuggest, isObject, Audit, getMetadata, bisect, isNotInputEvent, fetchSponsoredLinks, fetchQuery, translateAggregationToDateOptions, aggItemRegex, parseValueAndOperatorFromItem, fetchSuggestField, fetchSimilarDocuments, logout, fetchChangePassword, fetchSendPasswordResetEmail, expiresSoon, suggestionsToTreeAggregationNodes, labels, fetchLabels, guid, getRelativeDate, createUserProfile, deleteUserProfileProperty, patchUserProfile, isJsonable, addAuditAdditionalInfo, getToken, setToken, createHeaders } from '@sinequa/atomic';
11
- import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
11
+ import { takeUntilDestroyed, toObservable, toSignal } from '@angular/core/rxjs-interop';
12
12
  import { DatePipe, DATE_PIPE_DEFAULT_TIMEZONE, DATE_PIPE_DEFAULT_OPTIONS, Location, NgTemplateOutlet, NgStyle, NgClass, NgComponentOutlet } from '@angular/common';
13
13
  import { HttpClient, HttpParams, httpResource, HttpResponse, HttpHeaders, HttpContextToken } from '@angular/common/http';
14
14
  import { Title, DomSanitizer } from '@angular/platform-browser';
15
15
  import { cva } from 'class-variance-authority';
16
16
  import * as i1 from '@angular/forms';
17
17
  import { FormsModule, NonNullableFormBuilder, ReactiveFormsModule, FormGroup, FormControl } from '@angular/forms';
18
- import { injectVirtualizer } from '@tanstack/angular-virtual';
19
18
  import * as i1$1 from '@angular/cdk/drag-drop';
20
19
  import { DragDropModule } from '@angular/cdk/drag-drop';
21
20
  import * as i2 from '@angular/cdk/a11y';
22
21
  import { A11yModule } from '@angular/cdk/a11y';
23
22
  import { Overlay } from '@angular/cdk/overlay';
23
+ import { injectVirtualizer } from '@tanstack/angular-virtual';
24
24
  import { catchError as catchError$1, switchMap as switchMap$1 } from 'rxjs/operators';
25
25
 
26
26
  class BackdropService {
@@ -450,9 +450,13 @@ function withAppFeatures() {
450
450
  * @returns The alias of the column if it exists, otherwise the original column name.
451
451
  */
452
452
  getColumnAlias(column) {
453
+ if (typeof column !== "string" || column.length === 0) {
454
+ return column;
455
+ }
453
456
  const state = getState(store);
454
457
  const schema = state.indexes._.columns;
455
- const col = schema[column.toLocaleLowerCase()];
458
+ const key = column.toLocaleLowerCase();
459
+ const col = Object.hasOwn(schema, key) ? schema[key] : undefined;
456
460
  if (col) {
457
461
  return col.aliases?.[0] ? `${col.aliases[0].charAt(0).toLowerCase()}${col.aliases[0].slice(1)}` : column;
458
462
  }
@@ -465,8 +469,15 @@ function withAppFeatures() {
465
469
  * @returns The column definition as a `CCColumn` object if found, or `undefined` if the column does not exist.
466
470
  */
467
471
  getColumn(columnOrAlias) {
472
+ if (typeof columnOrAlias !== "string" || columnOrAlias.length === 0) {
473
+ return undefined;
474
+ }
468
475
  const state = getState(store);
469
- return state.columnMap?.[columnOrAlias.toLocaleLowerCase()];
476
+ const columnMap = state.columnMap;
477
+ if (!columnMap)
478
+ return undefined;
479
+ const key = columnOrAlias.toLocaleLowerCase();
480
+ return Object.hasOwn(columnMap, key) ? columnMap[key] : undefined;
470
481
  },
471
482
  /**
472
483
  * Determines if the specified column is of a date-related type.
@@ -1513,6 +1524,16 @@ function withThemes(app, themes) {
1513
1524
  return app;
1514
1525
  }
1515
1526
 
1527
+ /**
1528
+ * Signs in the user by checking the global configuration for authentication method and acting accordingly.
1529
+ *
1530
+ * This function first clears any existing session tokens to ensure a clean authentication state. It then checks the
1531
+ * global configuration to determine whether to use credentials-based authentication or Single Sign-On (SSO). If
1532
+ * credentials are used, it redirects the user to the login page. If SSO is enabled, it reloads the page to trigger
1533
+ * the SSO login process. If neither method is specified, it attempts a standard login and handles any errors that
1534
+ * may occur during the process.
1535
+ * @returns A promise resolving to a boolean indicating the success of the sign-in operation.
1536
+ */
1516
1537
  async function signIn() {
1517
1538
  assertInInjectionContext(signIn);
1518
1539
  const router = inject(Router);
@@ -1523,23 +1544,24 @@ async function signIn() {
1523
1544
  // If credentials are used, redirect to the login page
1524
1545
  if (useCredentials) {
1525
1546
  router.navigate([loginPath], { queryParams: { returnUrl: lastUrlAfterNavigation } });
1526
- return; // prevent further execution
1547
+ return false; // prevent further execution
1527
1548
  }
1528
1549
  // SSO is set to true when the browser handles authentication automatically
1529
1550
  // If SSO is used, reload the page to trigger SSO login
1530
1551
  if (useSSO) {
1531
1552
  // reload the page to trigger SSO login
1532
1553
  window.location.reload();
1533
- return; // prevent further execution
1554
+ return false; // prevent further execution
1534
1555
  }
1535
1556
  // Otherwise, perform a standard login
1536
1557
  try {
1537
1558
  const response = await login();
1538
1559
  if (response) {
1539
1560
  info("Response from login", response);
1561
+ return true;
1540
1562
  }
1541
1563
  else {
1542
- warn("No response from login", response);
1564
+ warn("Response from login", response);
1543
1565
  }
1544
1566
  }
1545
1567
  catch (err) {
@@ -1550,7 +1572,7 @@ async function signIn() {
1550
1572
  }
1551
1573
  throw err;
1552
1574
  }
1553
- return; // prevent further execution
1575
+ return false; // prevent further execution
1554
1576
  }
1555
1577
 
1556
1578
  /**
@@ -1571,24 +1593,31 @@ async function withBootstrapApp(applicationService, { createRoutes = true }) {
1571
1593
  return new Promise(resolve => {
1572
1594
  // Check if the user is authenticated
1573
1595
  signIn()
1574
- .then(() => {
1575
- info('User authenticated, initializing application...');
1576
- // Initialize the application
1577
- applicationService
1578
- .initialize(createRoutes)
1579
- .then(() => {
1580
- info(`Application initialized with routes: ${createRoutes} successfully.`);
1581
- resolve();
1582
- })
1583
- .catch(err => {
1584
- error(`Error initializing application with routes: ${createRoutes}:`, err);
1585
- resolve();
1586
- });
1596
+ .then(async (response) => {
1597
+ if (response) {
1598
+ info('User authenticated, initializing application...');
1599
+ // Initialize the application.
1600
+ // Awaited so the APP_INITIALIZER does not resolve (and bootstrap does not
1601
+ // complete) until initialization is done. Otherwise routed components render
1602
+ // and set their page title before `initialize()` runs `setGeneralApp()`, which
1603
+ // would then overwrite the page title with the bare application name.
1604
+ try {
1605
+ await applicationService.initialize(createRoutes);
1606
+ info(`Application initialized with routes: ${createRoutes} successfully.`);
1607
+ }
1608
+ catch (err) {
1609
+ error(`Error initializing application with routes: ${createRoutes}:`, err);
1610
+ }
1611
+ }
1612
+ else {
1613
+ info('User not authenticated, skipping application initialization.');
1614
+ }
1615
+ ;
1587
1616
  })
1588
1617
  .catch(err => {
1589
1618
  error('Error while signing in:', err);
1590
- resolve();
1591
- });
1619
+ })
1620
+ .finally(() => resolve());
1592
1621
  });
1593
1622
  }
1594
1623
 
@@ -2745,19 +2774,45 @@ function withSavedSearchesFeatures() {
2745
2774
  })));
2746
2775
  }
2747
2776
 
2777
+ /**
2778
+ * Canonical default shape of the user settings state.
2779
+ *
2780
+ * Shared between `initialize()` (overlaid under the fetched settings) and `reset()`
2781
+ * (used as the replacement state) so the two cannot drift apart. Keep in sync with
2782
+ * the `UserSettingsState` type.
2783
+ */
2784
+ const USER_SETTINGS_DEFAULTS = {
2785
+ bookmarks: [],
2786
+ recentSearches: [],
2787
+ savedSearches: [],
2788
+ baskets: [],
2789
+ alerts: [],
2790
+ assistants: {},
2791
+ language: undefined,
2792
+ collapseAssistant: undefined,
2793
+ userTheme: undefined,
2794
+ agents: { isDebugMode: false }
2795
+ };
2748
2796
  function withUserSettingsFeatures() {
2749
2797
  return signalStoreFeature(withState({ language: undefined, collapseAssistant: undefined }), withMethods(store => ({
2750
2798
  /**
2751
2799
  * Initializes the user settings store by fetching the user settings from the backend API
2752
2800
  * and patching the store with the retrieved settings.
2753
2801
  *
2802
+ * The fetched settings are overlaid on top of {@link USER_SETTINGS_DEFAULTS} so that any
2803
+ * key absent from the new user's settings resets to its default instead of retaining the
2804
+ * previous user's in-memory value. This is required because `initialize()` runs both on
2805
+ * first login and on every user override (impersonation): a plain merge would leak the
2806
+ * previous user's data (e.g. `recentSearches`) into the new user's session and backend.
2807
+ *
2754
2808
  * @returns {Promise<void>} A promise that resolves when the initialization is complete.
2755
2809
  */
2756
2810
  async initialize() {
2757
- // Fetch the user settings from the backend API and patch the store with the result
2811
+ // Fetch the user settings from the backend API and overlay them on the defaults,
2812
+ // so missing keys reset rather than keep the previous user's values.
2758
2813
  try {
2759
2814
  const settings = await fetchUserSettings();
2760
- patchState(store, settings);
2815
+ patchState(store, { ...USER_SETTINGS_DEFAULTS, ...settings });
2761
2816
  }
2762
2817
  catch (err) {
2763
2818
  error('Error fetching user settings:', err);
@@ -2775,17 +2830,7 @@ function withUserSettingsFeatures() {
2775
2830
  async reset() {
2776
2831
  // Reset the user settings to the initial state
2777
2832
  await deleteUserSettings();
2778
- patchState(store, {
2779
- bookmarks: [],
2780
- recentSearches: [],
2781
- savedSearches: [],
2782
- baskets: [],
2783
- alerts: [],
2784
- assistants: {},
2785
- language: undefined,
2786
- collapseAssistant: undefined,
2787
- agents: { isDebugMode: false }
2788
- });
2833
+ patchState(store, USER_SETTINGS_DEFAULTS);
2789
2834
  }
2790
2835
  })));
2791
2836
  }
@@ -3265,39 +3310,53 @@ class AggregationsService {
3265
3310
  return node;
3266
3311
  }
3267
3312
  /* aggregations helpers fot the filters components */
3268
- getAuthorizedFilters(aggregations, includedFilters = [], excludedFilters = []) {
3313
+ getAuthorizedFilters(aggregations, includedFilters = [], excludedFilters = [], homepageOnly = false) {
3269
3314
  const agg = aggregations || this.appStore.getAuthorizedFilters();
3270
3315
  const authorizedFilters = agg
3271
3316
  .filter((f) => this.getFilterCriteria()(f)) // filter only the filters present
3317
+ .filter((f) => !homepageOnly || this.getHomepageFilterCriteria()(f)) // when requested, keep only filters flagged `homepage: true`
3272
3318
  .filter((f) => !excludedFilters.includes(f.name))
3273
3319
  .filter((f) => !includedFilters.length || includedFilters.includes(f.name))
3274
3320
  .map((f) => ({ field: f.column, column: f.column, name: f.name })); // field is needed for filters constructions
3275
3321
  return authorizedFilters;
3276
3322
  }
3323
+ /**
3324
+ * Determines whether a custom JSON filter refers to the given aggregation.
3325
+ *
3326
+ * Matches by `name` when it is defined, otherwise falls back to `column`,
3327
+ * resolving column/alias ambiguity through the app store's column map.
3328
+ *
3329
+ * @param filter - The filter object coming from the custom JSON.
3330
+ * @param agg - The aggregation returned by the backend.
3331
+ * @returns `true` if the filter refers to the aggregation.
3332
+ */
3333
+ matchesAggregation(filter, agg) {
3334
+ // if filter.name is defined, use it to compare
3335
+ if (filter.name) {
3336
+ return filter.name.toLocaleLowerCase() === agg.name.toLocaleLowerCase();
3337
+ }
3338
+ // fallback to column comparison
3339
+ // column can be a column's name or an alias
3340
+ // resolve ambiguity between column and alias
3341
+ const { columnMap } = getState(this.appStore);
3342
+ // get the actual column for both filter and f
3343
+ const aggColumn = columnMap?.[agg.column.toLocaleLowerCase()];
3344
+ const filterColumn = columnMap?.[filter?.column?.toLocaleLowerCase() || filter.name.toLocaleLowerCase()];
3345
+ // if either column is not found, fallback to comparing the raw values
3346
+ if (!aggColumn || !filterColumn) {
3347
+ return filter?.column?.toLocaleLowerCase() === agg?.column?.toLocaleLowerCase();
3348
+ }
3349
+ // compare the actual column names coming from the column map
3350
+ return aggColumn?.name === filterColumn?.name;
3351
+ }
3277
3352
  getFilterCriteria = () => {
3278
3353
  // filter: object filter from the custom JSON
3279
3354
  // agg: object aggregation returned by the backend
3280
- return (agg) => {
3281
- return this.appStore.filters().some((filter) => {
3282
- // if filter.name is defined, use it to compare
3283
- if (filter.name) {
3284
- return filter.name.toLocaleLowerCase() === agg.name.toLocaleLowerCase();
3285
- }
3286
- // fallback to column comparison
3287
- // column can be a column's name or an alias
3288
- // resolve ambiguity between column and alias
3289
- const { columnMap } = getState(this.appStore);
3290
- // get the actual column for both filter and f
3291
- const aggColumn = columnMap?.[agg.column.toLocaleLowerCase()];
3292
- const filterColumn = columnMap?.[filter?.column?.toLocaleLowerCase() || filter.name.toLocaleLowerCase()];
3293
- // if either column is not found, fallback to comparing the raw values
3294
- if (!aggColumn || !filterColumn) {
3295
- return filter?.column?.toLocaleLowerCase() === agg?.column?.toLocaleLowerCase();
3296
- }
3297
- // compare the actual column names coming from the column map
3298
- return aggColumn?.name === filterColumn?.name;
3299
- });
3300
- };
3355
+ return (agg) => this.appStore.filters().some((filter) => this.matchesAggregation(filter, agg));
3356
+ };
3357
+ getHomepageFilterCriteria = () => {
3358
+ // only consider filters explicitly flagged with `homepage: true` in the custom JSON
3359
+ return (agg) => this.appStore.filters().some((filter) => filter.homepage && this.matchesAggregation(filter, agg));
3301
3360
  };
3302
3361
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AggregationsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
3303
3362
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AggregationsService, providedIn: "root" });
@@ -3365,14 +3424,10 @@ function AuthGuard() {
3365
3424
  const { loginPath, useCredentials, useSSO } = globalConfig;
3366
3425
  if (state.url.startsWith("/login"))
3367
3426
  return true;
3368
- // If the user is not authenticated, navigate to the login page or loading page based on the authentication method
3427
+ // If the user is not authenticated, navigate to the login page.
3428
+ // The login page handles every authentication method (credentials, OAuth, SAML).
3369
3429
  if (!isAuthenticated() && !useSSO) {
3370
- if (useCredentials) {
3371
- router.navigate([loginPath], { queryParams: { returnUrl: state.url } });
3372
- }
3373
- else {
3374
- router.navigate(["loading"], { queryParams: { returnUrl: state.url } });
3375
- }
3430
+ router.navigate([loginPath], { queryParams: { returnUrl: state.url } });
3376
3431
  return false;
3377
3432
  }
3378
3433
  // If the user is authenticated, initialize the principal store if it's in the initial state
@@ -3424,6 +3479,7 @@ class ApplicationService {
3424
3479
  components = inject(ROUTE_COMPONENTS);
3425
3480
  defaultComponent = computed(() => this.components.find((c) => c.path === "all")?.component, ...(ngDevMode ? [{ debugName: "defaultComponent" }] : []));
3426
3481
  defaultLayoutComponent = computed(() => this.components.find((c) => c.isRoot)?.component, ...(ngDevMode ? [{ debugName: "defaultLayoutComponent" }] : []));
3482
+ routerConfig = signal(undefined, ...(ngDevMode ? [{ debugName: "routerConfig" }] : []));
3427
3483
  /**
3428
3484
  * Initializes the application and creates routes if needed.
3429
3485
  *
@@ -3610,6 +3666,7 @@ class ApplicationService {
3610
3666
  searchPath.children = [...children, ...existingChildren, wildcardPath];
3611
3667
  const newConfig = [searchPath, ...currentConfig];
3612
3668
  info("ApplicationService: createRoutes -> newConfig", newConfig);
3669
+ this.routerConfig.set(newConfig);
3613
3670
  // finally we reset the router config with the new routes
3614
3671
  this.router.resetConfig(newConfig);
3615
3672
  }
@@ -3684,10 +3741,10 @@ class ApplicationService {
3684
3741
  // If general is not defined or is an empty object, do nothing
3685
3742
  if (!general || (typeof general === "object" && Object.keys(general).length === 0))
3686
3743
  return;
3687
- if (general.name) {
3688
- info("Setting document title to:", general.name);
3689
- this.titleService.setTitle(general.name);
3690
- }
3744
+ // NB: the document title is intentionally NOT set here. The page-specific title is
3745
+ // owned by the routed components/layouts via `setTitle()`. Setting it here would
3746
+ // overwrite the page title with the bare application name during bootstrap.
3747
+ // The initial title falls back to the static <title> defined in index.html.
3691
3748
  const { light, dark, alt } = general.logo || {};
3692
3749
  document.documentElement.style.setProperty("--logo-alt-text", `'${alt || general.name}'`);
3693
3750
  // light mode logo configuration
@@ -3981,7 +4038,7 @@ class NavigationService {
3981
4038
  * - Maps all router events to `RouterEvent`.
3982
4039
  * - Filters the events to only include instances of `NavigationEnd`.
3983
4040
  * - Taps into the event stream to extract the route name from the URL and notify the audit service of route changes,
3984
- * excluding the "loading" route and duplicate navigations.
4041
+ * excluding duplicate navigations.
3985
4042
  * - Updates the `urlAfterNavigation` property with the current URL after navigation.
3986
4043
  * - Shares the replayed value with a buffer size of 1 to ensure subscribers receive the latest emitted value.
3987
4044
  *
@@ -3989,8 +4046,7 @@ class NavigationService {
3989
4046
  */
3990
4047
  navigationEnd$ = this.router.events.pipe(map((event) => event), filter((event) => event instanceof NavigationEnd), tap((event) => {
3991
4048
  const url = event.url.slice(1).split("?")[0]; // Extract route name
3992
- // TODO: use a "loading" configuration from globalConfig
3993
- if (url && url !== "loading" && url !== this.urlAfterNavigation) {
4049
+ if (url && url !== this.urlAfterNavigation) {
3994
4050
  this.auditService.notifyRouteChange(url);
3995
4051
  }
3996
4052
  }), tap((event) => {
@@ -5321,6 +5377,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
5321
5377
  }]
5322
5378
  }] });
5323
5379
 
5380
+ /**
5381
+ * @deprecated This component and its `/loading` route are no longer used.
5382
+ * Authentication is handled at bootstrap by `withBootstrapApp`/`signIn`, and the
5383
+ * `AuthGuard` now redirects unauthenticated users to the login page directly.
5384
+ * Will be removed in a future major release. Do not use in new code.
5385
+ */
5324
5386
  class LoadingComponent {
5325
5387
  state = computed(() => getState(this.application), ...(ngDevMode ? [{ debugName: "state" }] : []));
5326
5388
  application = inject(ApplicationStore);
@@ -6174,6 +6236,47 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
6174
6236
  }]
6175
6237
  }], propDecorators: { class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], updatedCollections: [{ type: i0.Output, args: ["updatedCollections"] }] } });
6176
6238
 
6239
+ function injectRouteNavigation(path, showCount) {
6240
+ const router = inject(Router);
6241
+ const route = inject(ActivatedRoute);
6242
+ const queryParamsStore = inject(QueryParamsStore);
6243
+ const queryService = inject(QueryService);
6244
+ const routerConfig = inject(ApplicationService).routerConfig;
6245
+ const searchText = computed(() => getState(queryParamsStore).text || "", ...(ngDevMode ? [{ debugName: "searchText" }] : []));
6246
+ const currentPath = computed(() => {
6247
+ const { tab: currentTab } = getState(queryParamsStore);
6248
+ let current = route.snapshot;
6249
+ while (current.firstChild)
6250
+ current = current.firstChild;
6251
+ const childPath = current.url.map((s) => s.path).join("/");
6252
+ return childPath || currentTab || "all";
6253
+ }, ...(ngDevMode ? [{ debugName: "currentPath" }] : []));
6254
+ const tabs = computed(() => {
6255
+ const result = queryService.result();
6256
+ const config = routerConfig() ?? router.config;
6257
+ return (config
6258
+ .find((item) => item.path === path())
6259
+ ?.children?.filter((c) => c.path !== "**" && !c.redirectTo)
6260
+ .map((child) => {
6261
+ const data = child.data;
6262
+ return {
6263
+ display: data?.display || child.path || "",
6264
+ wsQueryTab: data?.wsQueryTab || child.path || "",
6265
+ path: child.path || "",
6266
+ routerLink: `/${path()}/${child.path}`,
6267
+ icon: data?.icon || "",
6268
+ queryName: data?.queryName,
6269
+ count: showCount()
6270
+ ? result.queryName === data?.queryName
6271
+ ? result.tabs?.find((t) => t.name === (data?.wsQueryTab || child.path))?.count
6272
+ : undefined
6273
+ : undefined
6274
+ };
6275
+ }) ?? []);
6276
+ }, ...(ngDevMode ? [{ debugName: "tabs" }] : []));
6277
+ return { tabs, currentPath, searchText };
6278
+ }
6279
+
6177
6280
  /**
6178
6281
  * Directive to mark a child element for special processing.
6179
6282
  * This can be used to apply specific styles or behaviors to child elements.
@@ -6877,8 +6980,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
6877
6980
  }]
6878
6981
  }] });
6879
6982
  /**
6880
- * Directive to be used on the element that should be considered as the limit
6881
- * for the overflow manager.
6983
+ * Directive to be used on the "more" trigger element. The overflow manager
6984
+ * reserves this element's size when not all items fit, so the last visible
6985
+ * item never overlaps it. Its position is not used for measurement.
6882
6986
  */
6883
6987
  class OverflowStopDirective {
6884
6988
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: OverflowStopDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
@@ -6892,10 +6996,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
6892
6996
  }]
6893
6997
  }] });
6894
6998
  /**
6895
- * Directive that takes items and a stop element to count the number of items
6896
- * that can fit before the stop element.
6897
- * The directive will apply a `visibility: hidden` on the items that are not
6898
- * visible.
6999
+ * Directive that counts how many items fit inside the container and hides the
7000
+ * overflowing ones. The boundary is the container's own content edge, which
7001
+ * keeps the measurement correct even when the host lives inside a flex layout
7002
+ * (we never depend on a sibling's position). The stop element is used only to
7003
+ * reserve space for the "more" trigger when not all items fit.
7004
+ *
7005
+ * The directive applies `display: none` on the items that do not fit.
6899
7006
  *
6900
7007
  * You can specify a target element to observe for resize events, otherwise the
6901
7008
  * directive will observe the element itself.
@@ -6934,10 +7041,26 @@ class OverflowManagerDirective {
6934
7041
  target = input(...(ngDevMode ? [undefined, { debugName: "target" }] : []));
6935
7042
  margin = input(4, ...(ngDevMode ? [{ debugName: "margin" }] : []));
6936
7043
  direction = input("horizontal", ...(ngDevMode ? [{ debugName: "direction" }] : []));
7044
+ /**
7045
+ * Always reserve the stop element's size, even when every item fits inside
7046
+ * the container. Use it when the stop trigger is permanently visible (e.g.
7047
+ * the "more" button also gives access to items that are never rendered in
7048
+ * the container), so the last item never overlaps it.
7049
+ */
7050
+ reserveStop = input(false, ...(ngDevMode ? [{ debugName: "reserveStop", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
6937
7051
  count = output();
6938
7052
  el = inject(ElementRef).nativeElement;
6939
7053
  destroyRef = inject(DestroyRef);
6940
7054
  resizeObserver = new ResizeObserver(() => this.countItems());
7055
+ // Recompute when an individual item's size changes (label update, async
7056
+ // translation, font load…), not only when the container resizes. Items we
7057
+ // hid with `display: none` report a 0×0 size; ignore those notifications so
7058
+ // our own show/hide toggling doesn't trigger redundant recounts.
7059
+ itemsResizeObserver = new ResizeObserver((entries) => {
7060
+ const visibleItemResized = entries.some((entry) => entry.target.style.display !== "none");
7061
+ if (visibleItemResized)
7062
+ this.countItems();
7063
+ });
6941
7064
  countSub;
6942
7065
  _lastCount;
6943
7066
  constructor() {
@@ -6949,21 +7072,37 @@ class OverflowManagerDirective {
6949
7072
  this.countItems();
6950
7073
  }
6951
7074
  });
7075
+ // (re)observe every item whenever the projected list changes, so that
7076
+ // added/removed items and per-item size changes both trigger a recount
7077
+ effect(() => {
7078
+ const items = this.items();
7079
+ this.itemsResizeObserver.disconnect();
7080
+ items.forEach((item) => this.itemsResizeObserver.observe(item.nativeElement));
7081
+ });
6952
7082
  // listens to the count output and toggles the visibility of the items
6953
7083
  this.countSub = this.count.subscribe((count) => this.toggleToCount(count));
6954
7084
  this.destroyRef.onDestroy(() => {
6955
7085
  this.resizeObserver.disconnect();
7086
+ this.itemsResizeObserver.disconnect();
6956
7087
  this.countSub?.unsubscribe();
6957
7088
  this.countSub = undefined;
6958
7089
  });
6959
7090
  }
6960
7091
  /**
6961
- * Counts the number of items that can fit before the stop element.
7092
+ * Counts the number of items that can fit inside the container.
7093
+ *
7094
+ * The boundary is the container's own content edge (not the position of the
7095
+ * stop element). This is what makes the measurement robust when the host is
7096
+ * placed inside a flex layout: we never rely on a sibling staying where we
7097
+ * expect it. The stop element is only used for its *size*, to reserve space
7098
+ * for the "more" trigger when not all items fit.
7099
+ *
6962
7100
  * Emits the count if it has changed.
6963
7101
  */
6964
7102
  countItems() {
6965
7103
  if (!this.items() || this.items().length === 0 || !this.stop())
6966
7104
  return;
7105
+ const horizontal = this.direction() === "horizontal";
6967
7106
  // Reset all items to their natural size before measuring so that previously
6968
7107
  // hidden items (display: none) don't corrupt the layout and position of
6969
7108
  // their siblings.
@@ -6971,19 +7110,33 @@ class OverflowManagerDirective {
6971
7110
  item.nativeElement.style.display = "";
6972
7111
  });
6973
7112
  // getBoundingClientRect() forces a synchronous reflow, so positions are
6974
- // accurate after the reset above.
7113
+ // accurate after the reset above. Using rects (not offsetWidth) means the
7114
+ // inter-item gap is accounted for automatically.
7115
+ const containerRect = this.el.getBoundingClientRect();
6975
7116
  const stopRect = this.stop().nativeElement.getBoundingClientRect();
6976
7117
  const itemsRects = this.items().map((item) => item.nativeElement.getBoundingClientRect());
6977
- let count = 0;
6978
- for (const rect of itemsRects) {
6979
- // if the direction is horizontal, we check the right side of the item
6980
- if (this.direction() === "horizontal" && rect.right + this.margin() <= stopRect.left)
6981
- count++;
6982
- // if the direction is vertical, we check the bottom side of the item
6983
- else if (this.direction() === "vertical" && rect.bottom + this.margin() <= stopRect.top)
6984
- count++;
6985
- else
6986
- break;
7118
+ // The container's content edge, with a small slack margin.
7119
+ const containerEnd = (horizontal ? containerRect.right : containerRect.bottom) - this.margin();
7120
+ const itemEnd = (rect) => (horizontal ? rect.right : rect.bottom);
7121
+ let count;
7122
+ // If every item fits within the container, no "more" trigger is needed and
7123
+ // we don't reserve any space for it unless the trigger is permanently
7124
+ // visible (reserveStop), in which case its space is always reserved.
7125
+ if (!this.reserveStop() && itemEnd(itemsRects[itemsRects.length - 1]) <= containerEnd) {
7126
+ count = itemsRects.length;
7127
+ }
7128
+ else {
7129
+ // Otherwise reserve the stop element's own size so the last visible item
7130
+ // never overlaps the "more" trigger.
7131
+ const reserve = horizontal ? stopRect.width : stopRect.height;
7132
+ const limit = containerEnd - reserve;
7133
+ count = 0;
7134
+ for (const rect of itemsRects) {
7135
+ if (itemEnd(rect) <= limit)
7136
+ count++;
7137
+ else
7138
+ break;
7139
+ }
6987
7140
  }
6988
7141
  if (this._lastCount !== count) {
6989
7142
  this._lastCount = count;
@@ -7006,7 +7159,7 @@ class OverflowManagerDirective {
7006
7159
  });
7007
7160
  }
7008
7161
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: OverflowManagerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
7009
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "20.3.18", type: OverflowManagerDirective, isStandalone: true, selector: "[overflowManager]", inputs: { target: { classPropertyName: "target", publicName: "target", isSignal: true, isRequired: false, transformFunction: null }, margin: { classPropertyName: "margin", publicName: "margin", isSignal: true, isRequired: false, transformFunction: null }, direction: { classPropertyName: "direction", publicName: "direction", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { count: "count" }, queries: [{ propertyName: "items", predicate: OverflowItemDirective, descendants: true, read: ElementRef, isSignal: true }, { propertyName: "stop", first: true, predicate: OverflowStopDirective, descendants: true, read: ElementRef, isSignal: true }], ngImport: i0 });
7162
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "20.3.18", type: OverflowManagerDirective, isStandalone: true, selector: "[overflowManager]", inputs: { target: { classPropertyName: "target", publicName: "target", isSignal: true, isRequired: false, transformFunction: null }, margin: { classPropertyName: "margin", publicName: "margin", isSignal: true, isRequired: false, transformFunction: null }, direction: { classPropertyName: "direction", publicName: "direction", isSignal: true, isRequired: false, transformFunction: null }, reserveStop: { classPropertyName: "reserveStop", publicName: "reserveStop", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { count: "count" }, queries: [{ propertyName: "items", predicate: OverflowItemDirective, descendants: true, read: ElementRef, isSignal: true }, { propertyName: "stop", first: true, predicate: OverflowStopDirective, descendants: true, read: ElementRef, isSignal: true }], ngImport: i0 });
7010
7163
  }
7011
7164
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: OverflowManagerDirective, decorators: [{
7012
7165
  type: Directive,
@@ -7014,7 +7167,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
7014
7167
  selector: "[overflowManager]",
7015
7168
  standalone: true
7016
7169
  }]
7017
- }], ctorParameters: () => [], propDecorators: { items: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => OverflowItemDirective), { ...{ descendants: true, read: ElementRef }, isSignal: true }] }], stop: [{ type: i0.ContentChild, args: [i0.forwardRef(() => OverflowStopDirective), { ...{ descendants: true, read: ElementRef }, isSignal: true }] }], target: [{ type: i0.Input, args: [{ isSignal: true, alias: "target", required: false }] }], margin: [{ type: i0.Input, args: [{ isSignal: true, alias: "margin", required: false }] }], direction: [{ type: i0.Input, args: [{ isSignal: true, alias: "direction", required: false }] }], count: [{ type: i0.Output, args: ["count"] }] } });
7170
+ }], ctorParameters: () => [], propDecorators: { items: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => OverflowItemDirective), { ...{ descendants: true, read: ElementRef }, isSignal: true }] }], stop: [{ type: i0.ContentChild, args: [i0.forwardRef(() => OverflowStopDirective), { ...{ descendants: true, read: ElementRef }, isSignal: true }] }], target: [{ type: i0.Input, args: [{ isSignal: true, alias: "target", required: false }] }], margin: [{ type: i0.Input, args: [{ isSignal: true, alias: "margin", required: false }] }], direction: [{ type: i0.Input, args: [{ isSignal: true, alias: "direction", required: false }] }], reserveStop: [{ type: i0.Input, args: [{ isSignal: true, alias: "reserveStop", required: false }] }], count: [{ type: i0.Output, args: ["count"] }] } });
7018
7171
 
7019
7172
  /**
7020
7173
  * Directive that selects an article on click.
@@ -7113,102 +7266,69 @@ class NavbarTabsComponent {
7113
7266
  cn = cn;
7114
7267
  class = input(...(ngDevMode ? [undefined, { debugName: "class" }] : []));
7115
7268
  router = inject(Router);
7116
- route = inject(ActivatedRoute);
7117
- queryParamsStore = inject(QueryParamsStore);
7118
- // Injecting the QueryService to access last search results
7119
- queryService = inject(QueryService);
7269
+ showCount = input(true, ...(ngDevMode ? [{ debugName: "showCount", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
7120
7270
  /**
7121
- * Determines whether the count should be displayed in the navbar tabs component.
7122
- * This value is provided as an input property.
7271
+ * When enabled (default), tab labels are never clipped: tabs that do not fit
7272
+ * collapse into the ellipsis dropdown instead. Set to `false` to truncate
7273
+ * overflowing labels with an ellipsis inside their slot.
7123
7274
  *
7124
- * @remarks
7125
- * This works only if tabSearch is enabled in the administration panel.
7275
+ * @default true
7126
7276
  */
7127
- showCount = input(true, ...(ngDevMode ? [{ debugName: "showCount", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
7128
- noTruncate = input(false, ...(ngDevMode ? [{ debugName: "noTruncate", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
7277
+ noTruncate = input(true, ...(ngDevMode ? [{ debugName: "noTruncate", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
7129
7278
  minTabWidth = input(...(ngDevMode ? [undefined, { debugName: "minTabWidth" }] : []));
7279
+ path = input("search", ...(ngDevMode ? [{ debugName: "path" }] : []));
7280
+ nav = injectRouteNavigation(this.path, this.showCount);
7281
+ visibleTabCount = signal(undefined, ...(ngDevMode ? [{ debugName: "visibleTabCount" }] : []));
7282
+ moreTabs = computed(() => this.nav.tabs().slice(this.visibleTabCount()), ...(ngDevMode ? [{ debugName: "moreTabs" }] : []));
7130
7283
  /**
7131
- * The base path for the search routes.
7132
- * This value is provided as an input property and defaults to "search".
7284
+ * Minimum width applied to a tab (through the `--tab-min-width` CSS variable)
7285
+ * when labels are truncated (`noTruncate` is `false`), so a tab never shrinks
7286
+ * into an unusable sliver. An explicit `minTabWidth` input always wins.
7133
7287
  *
7134
- * @remarks
7135
- * This path is used to find the corresponding route configuration and its children
7136
- * to populate the tabs in the navbar.
7288
+ * - tab with an icon: the icon (16px) plus the tab's horizontal padding (1.5rem)
7289
+ * - text-only tab: enough room for a few characters and the ellipsis
7137
7290
  */
7138
- path = input("search", ...(ngDevMode ? [{ debugName: "path" }] : []));
7139
- searchText = computed(() => {
7140
- const state = getState(this.queryParamsStore);
7141
- return state.text || "";
7142
- }, ...(ngDevMode ? [{ debugName: "searchText" }] : []));
7143
- visibleTabCount = signal(undefined, ...(ngDevMode ? [{ debugName: "visibleTabCount" }] : []));
7144
- currentPath = computed(() => {
7145
- // get current tab from the query params store
7146
- const { tab: currentTab } = getState(this.queryParamsStore);
7147
- // navigate to the deepest child route
7148
- let current = this.route.snapshot;
7149
- while (current.firstChild) {
7150
- current = current.firstChild;
7151
- }
7152
- // childPath exists when we click on a tab, otherwise it's an empty string
7153
- const childPath = current.url.map((segment) => segment.path).join("/");
7154
- // return the child path if present, otherwise the current tab from the query params, or 'all' as a fallback
7155
- return childPath || currentTab || "all";
7156
- }, ...(ngDevMode ? [{ debugName: "currentPath" }] : []));
7157
- // create tabs from the search routes
7158
- tabs = computed(() => {
7159
- const result = this.queryService.result();
7160
- const r = this.router.config
7161
- .find((item) => item.path === this.path())
7162
- ?.children?.filter((c) => c.path !== "**")
7163
- .map((child) => {
7164
- const data = child.data;
7165
- return {
7166
- display: data?.display || child.path || "",
7167
- wsQueryTab: data?.wsQueryTab || child.path || "",
7168
- path: child.path || "",
7169
- routerLink: `/${this.path()}/${child.path}`,
7170
- icon: data?.icon || "",
7171
- queryName: data?.queryName,
7172
- // get the count from the last search result if showCount is true and queryName matches
7173
- count: this.showCount()
7174
- ? result.queryName === data?.queryName
7175
- ? result.tabs?.find((tab) => tab.name === (data?.wsQueryTab || child.path))?.count
7176
- : undefined
7177
- : undefined
7178
- };
7179
- }) ?? [];
7180
- return r;
7181
- }, ...(ngDevMode ? [{ debugName: "tabs" }] : []));
7182
- moreTabs = computed(() => this.tabs().slice(this.visibleTabCount()), ...(ngDevMode ? [{ debugName: "moreTabs" }] : []));
7291
+ tabMinWidth(hasIcon) {
7292
+ if (this.minTabWidth())
7293
+ return this.minTabWidth();
7294
+ if (this.noTruncate())
7295
+ return undefined;
7296
+ return hasIcon ? "calc(1.5rem + 16px)" : "calc(1.5rem + 4ch)";
7297
+ }
7183
7298
  changeTab() { }
7184
7299
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: NavbarTabsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
7185
7300
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: NavbarTabsComponent, isStandalone: true, selector: "navbar-tabs", inputs: { class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, showCount: { classPropertyName: "showCount", publicName: "showCount", isSignal: true, isRequired: false, transformFunction: null }, noTruncate: { classPropertyName: "noTruncate", publicName: "noTruncate", isSignal: true, isRequired: false, transformFunction: null }, minTabWidth: { classPropertyName: "minTabWidth", publicName: "minTabWidth", isSignal: true, isRequired: false, transformFunction: null }, path: { classPropertyName: "path", publicName: "path", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "cn('block', class())" } }, ngImport: i0, template: `
7186
7301
  <!-- do not display the tabs if there are no tabs -->
7187
- @if (tabs().length > 0) {
7302
+ @if (nav.tabs().length > 0) {
7188
7303
  <div overflowManager (count)="visibleTabCount.set($event)" class="flex items-end rounded-[inherit] bg-inherit">
7189
7304
  <Tabs class="flex-1 min-w-0 overflow-hidden">
7190
7305
  <TabsList class="w-full">
7191
- @for (tab of tabs(); track $index) {
7306
+ @for (tab of nav.tabs(); track $index) {
7192
7307
  <Tab
7193
- class="group"
7308
+ [class]="cn('group', !noTruncate() && 'basis-[12.5rem]')"
7194
7309
  title="{{ tab.display | syslang | transloco }}"
7195
7310
  value="{{ tab.display }}"
7196
7311
  overflowItem
7197
7312
  [noTruncate]="noTruncate()"
7198
- [style.--tab-min-width]="minTabWidth()"
7199
- [attr.aria-selected]="this.currentPath() === tab.path"
7313
+ [style.--tab-min-width]="tabMinWidth(!!tab.icon)"
7314
+ [attr.aria-selected]="nav.currentPath() === tab.path"
7200
7315
  [attr.disabled]="showCount() && tab.count === 0 ? '' : null"
7201
- [active]="this.currentPath() === tab.path"
7316
+ [active]="nav.currentPath() === tab.path"
7202
7317
  [routerLink]="[tab.routerLink]"
7203
- [queryParams]="{ n: tab.queryName, q: searchText(), t: tab.wsQueryTab, f: undefined, sort: undefined, id: undefined, page: undefined }"
7318
+ [queryParams]="{ n: tab.queryName, q: nav.searchText(), t: tab.wsQueryTab, f: undefined, sort: undefined, id: undefined, page: undefined }"
7204
7319
  (click)="changeTab()"
7205
- (keydown.enter)="router.navigate([tab.routerLink], { queryParams: { n: tab.queryName, q: searchText(), t: tab.wsQueryTab, f: undefined, sort: undefined, id: undefined, page: undefined } })"
7320
+ (keydown.enter)="router.navigate([tab.routerLink], { queryParams: { n: tab.queryName, q: nav.searchText(), t: tab.wsQueryTab, f: undefined, sort: undefined, id: undefined, page: undefined } })"
7206
7321
  >
7207
- <div [class]="cn('flex items-center content-start w-full gap-1', !noTruncate() && 'overflow-hidden min-w-0')">
7322
+ <div [class]="cn('flex items-center content-start w-full gap-1', !noTruncate() && '@container overflow-hidden min-w-0')">
7208
7323
  @if (tab.icon) {
7209
- <FaIcon [faClass]="tab.icon" aria-hidden="true" />
7324
+ <!-- the icon never shrinks: the label truncates first -->
7325
+ <FaIcon [faClass]="tab.icon" class="shrink-0" aria-hidden="true" />
7210
7326
  }
7211
- <span [class]="noTruncate() ? '' : 'truncate'">{{ tab.display | syslang | transloco }}</span>
7327
+ <!-- when the tab gets too narrow to show anything meaningful next to
7328
+ the icon, hide the label entirely instead of a clipped sliver.
7329
+ w-0 + invisible (not display: none) keeps the label's line box,
7330
+ so the tab height doesn't collapse to the icon's height -->
7331
+ <span [class]="cn(!noTruncate() && 'truncate', !noTruncate() && tab.icon && '@max-[3rem]:w-0 @max-[3rem]:invisible')">{{ tab.display | syslang | transloco }}</span>
7212
7332
  <!-- Show count badge only if count is > 0 -->
7213
7333
  @if((tab.count ?? 0) > 0) {
7214
7334
  <Badge size="sm">{{ tab.count }}</Badge>
@@ -7235,8 +7355,8 @@ class NavbarTabsComponent {
7235
7355
  <MenuItem>
7236
7356
  <a class="inline-flex items-center gap-1 whitespace-nowrap first-letter:capitalize"
7237
7357
  [routerLink]="[tab.routerLink]"
7238
- [queryParams]="{ n: tab.queryName, q: searchText(), t: tab.wsQueryTab, f: undefined, sort: undefined, id: undefined, page: undefined }"
7239
- [attr.aria-selected]="this.currentPath() === tab.path"
7358
+ [queryParams]="{ n: tab.queryName, q: nav.searchText(), t: tab.wsQueryTab, f: undefined, sort: undefined, id: undefined, page: undefined }"
7359
+ [attr.aria-selected]="nav.currentPath() === tab.path"
7240
7360
  [attr.aria-label]="tab.display | syslang | transloco"
7241
7361
  (click)="changeTab()">
7242
7362
  @if (tab.icon) {
@@ -7250,7 +7370,7 @@ class NavbarTabsComponent {
7250
7370
  </Menu>
7251
7371
  </div>
7252
7372
  }
7253
- `, isInline: true, dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: ButtonComponent, selector: "button", inputs: ["class", "variant", "decoration", "scheme", "iconOnly", "size", "solid"] }, { kind: "component", type: MenuComponent, selector: "menu, Menu", inputs: ["disabled"] }, { kind: "directive", type: MenuItemComponent, selector: "menu-item, menuitem, MenuItem", inputs: ["class", "variant", "decoration"] }, { kind: "directive", type: MenuContentComponent, selector: "MenuContent, menucontent, menu-content", inputs: ["class", "position"] }, { kind: "directive", type: TabsComponent, selector: "tabs, Tabs", inputs: ["class"] }, { kind: "directive", type: TabsListComponent, selector: "tabs-list, TabsList", inputs: ["class", "variant", "size"] }, { kind: "directive", type: TabComponent, selector: "tab, Tab", inputs: ["class", "variant", "size", "noTruncate", "value", "active"], outputs: ["clicked"] }, { kind: "directive", type: OverflowManagerDirective, selector: "[overflowManager]", inputs: ["target", "margin", "direction"], outputs: ["count"] }, { kind: "directive", type: OverflowItemDirective, selector: "[overflowItem]" }, { kind: "directive", type: OverflowStopDirective, selector: "[overflowStop]" }, { kind: "directive", type: BadgeComponent, selector: "badge, Badge", inputs: ["class", "variant", "scheme", "size"] }, { kind: "component", type: EllipsisIcon, selector: "ellipsis-icon, EllipsisIcon, ellipsisicon", inputs: ["class", "orientation"] }, { kind: "component", type: FaIconComponent, selector: "fa-icon, FaIcon", inputs: ["faClass", "class"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }, { kind: "pipe", type: SyslangPipe, name: "syslang" }] });
7373
+ `, isInline: true, dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: ButtonComponent, selector: "button", inputs: ["class", "variant", "decoration", "scheme", "iconOnly", "size", "solid"] }, { kind: "component", type: MenuComponent, selector: "menu, Menu", inputs: ["disabled"] }, { kind: "directive", type: MenuItemComponent, selector: "menu-item, menuitem, MenuItem", inputs: ["class", "variant", "decoration"] }, { kind: "directive", type: MenuContentComponent, selector: "MenuContent, menucontent, menu-content", inputs: ["class", "position"] }, { kind: "directive", type: TabsComponent, selector: "tabs, Tabs", inputs: ["class"] }, { kind: "directive", type: TabsListComponent, selector: "tabs-list, TabsList", inputs: ["class", "variant", "size"] }, { kind: "directive", type: TabComponent, selector: "tab, Tab", inputs: ["class", "variant", "size", "noTruncate", "value", "active"], outputs: ["clicked"] }, { kind: "directive", type: OverflowManagerDirective, selector: "[overflowManager]", inputs: ["target", "margin", "direction", "reserveStop"], outputs: ["count"] }, { kind: "directive", type: OverflowItemDirective, selector: "[overflowItem]" }, { kind: "directive", type: OverflowStopDirective, selector: "[overflowStop]" }, { kind: "directive", type: BadgeComponent, selector: "badge, Badge", inputs: ["class", "variant", "scheme", "size"] }, { kind: "component", type: EllipsisIcon, selector: "ellipsis-icon, EllipsisIcon, ellipsisicon", inputs: ["class", "orientation"] }, { kind: "component", type: FaIconComponent, selector: "fa-icon, FaIcon", inputs: ["faClass", "class"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }, { kind: "pipe", type: SyslangPipe, name: "syslang" }] });
7254
7374
  }
7255
7375
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: NavbarTabsComponent, decorators: [{
7256
7376
  type: Component,
@@ -7277,31 +7397,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
7277
7397
  ],
7278
7398
  template: `
7279
7399
  <!-- do not display the tabs if there are no tabs -->
7280
- @if (tabs().length > 0) {
7400
+ @if (nav.tabs().length > 0) {
7281
7401
  <div overflowManager (count)="visibleTabCount.set($event)" class="flex items-end rounded-[inherit] bg-inherit">
7282
7402
  <Tabs class="flex-1 min-w-0 overflow-hidden">
7283
7403
  <TabsList class="w-full">
7284
- @for (tab of tabs(); track $index) {
7404
+ @for (tab of nav.tabs(); track $index) {
7285
7405
  <Tab
7286
- class="group"
7406
+ [class]="cn('group', !noTruncate() && 'basis-[12.5rem]')"
7287
7407
  title="{{ tab.display | syslang | transloco }}"
7288
7408
  value="{{ tab.display }}"
7289
7409
  overflowItem
7290
7410
  [noTruncate]="noTruncate()"
7291
- [style.--tab-min-width]="minTabWidth()"
7292
- [attr.aria-selected]="this.currentPath() === tab.path"
7411
+ [style.--tab-min-width]="tabMinWidth(!!tab.icon)"
7412
+ [attr.aria-selected]="nav.currentPath() === tab.path"
7293
7413
  [attr.disabled]="showCount() && tab.count === 0 ? '' : null"
7294
- [active]="this.currentPath() === tab.path"
7414
+ [active]="nav.currentPath() === tab.path"
7295
7415
  [routerLink]="[tab.routerLink]"
7296
- [queryParams]="{ n: tab.queryName, q: searchText(), t: tab.wsQueryTab, f: undefined, sort: undefined, id: undefined, page: undefined }"
7416
+ [queryParams]="{ n: tab.queryName, q: nav.searchText(), t: tab.wsQueryTab, f: undefined, sort: undefined, id: undefined, page: undefined }"
7297
7417
  (click)="changeTab()"
7298
- (keydown.enter)="router.navigate([tab.routerLink], { queryParams: { n: tab.queryName, q: searchText(), t: tab.wsQueryTab, f: undefined, sort: undefined, id: undefined, page: undefined } })"
7418
+ (keydown.enter)="router.navigate([tab.routerLink], { queryParams: { n: tab.queryName, q: nav.searchText(), t: tab.wsQueryTab, f: undefined, sort: undefined, id: undefined, page: undefined } })"
7299
7419
  >
7300
- <div [class]="cn('flex items-center content-start w-full gap-1', !noTruncate() && 'overflow-hidden min-w-0')">
7420
+ <div [class]="cn('flex items-center content-start w-full gap-1', !noTruncate() && '@container overflow-hidden min-w-0')">
7301
7421
  @if (tab.icon) {
7302
- <FaIcon [faClass]="tab.icon" aria-hidden="true" />
7422
+ <!-- the icon never shrinks: the label truncates first -->
7423
+ <FaIcon [faClass]="tab.icon" class="shrink-0" aria-hidden="true" />
7303
7424
  }
7304
- <span [class]="noTruncate() ? '' : 'truncate'">{{ tab.display | syslang | transloco }}</span>
7425
+ <!-- when the tab gets too narrow to show anything meaningful next to
7426
+ the icon, hide the label entirely instead of a clipped sliver.
7427
+ w-0 + invisible (not display: none) keeps the label's line box,
7428
+ so the tab height doesn't collapse to the icon's height -->
7429
+ <span [class]="cn(!noTruncate() && 'truncate', !noTruncate() && tab.icon && '@max-[3rem]:w-0 @max-[3rem]:invisible')">{{ tab.display | syslang | transloco }}</span>
7305
7430
  <!-- Show count badge only if count is > 0 -->
7306
7431
  @if((tab.count ?? 0) > 0) {
7307
7432
  <Badge size="sm">{{ tab.count }}</Badge>
@@ -7328,8 +7453,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
7328
7453
  <MenuItem>
7329
7454
  <a class="inline-flex items-center gap-1 whitespace-nowrap first-letter:capitalize"
7330
7455
  [routerLink]="[tab.routerLink]"
7331
- [queryParams]="{ n: tab.queryName, q: searchText(), t: tab.wsQueryTab, f: undefined, sort: undefined, id: undefined, page: undefined }"
7332
- [attr.aria-selected]="this.currentPath() === tab.path"
7456
+ [queryParams]="{ n: tab.queryName, q: nav.searchText(), t: tab.wsQueryTab, f: undefined, sort: undefined, id: undefined, page: undefined }"
7457
+ [attr.aria-selected]="nav.currentPath() === tab.path"
7333
7458
  [attr.aria-label]="tab.display | syslang | transloco"
7334
7459
  (click)="changeTab()">
7335
7460
  @if (tab.icon) {
@@ -7350,6 +7475,73 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
7350
7475
  }]
7351
7476
  }], propDecorators: { class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], showCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCount", required: false }] }], noTruncate: [{ type: i0.Input, args: [{ isSignal: true, alias: "noTruncate", required: false }] }], minTabWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "minTabWidth", required: false }] }], path: [{ type: i0.Input, args: [{ isSignal: true, alias: "path", required: false }] }] } });
7352
7477
 
7478
+ class SidebarNavComponent {
7479
+ path = input("search", ...(ngDevMode ? [{ debugName: "path" }] : []));
7480
+ showCount = input(false, ...(ngDevMode ? [{ debugName: "showCount", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
7481
+ nav = injectRouteNavigation(this.path, this.showCount);
7482
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: SidebarNavComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
7483
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: SidebarNavComponent, isStandalone: true, selector: "sidebar-nav", inputs: { path: { classPropertyName: "path", publicName: "path", isSignal: true, isRequired: false, transformFunction: null }, showCount: { classPropertyName: "showCount", publicName: "showCount", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
7484
+ <sidebar-menu>
7485
+ @for (tab of nav.tabs(); track tab.path) {
7486
+ @let label = tab.display | syslang | transloco;
7487
+ <sidebar-menu-item>
7488
+ <sidebar-menu-button
7489
+ [tooltip]="label" tooltip-position="right"
7490
+ [routerLink]="tab.routerLink"
7491
+ routerLinkActive="active"
7492
+ #rla="routerLinkActive"
7493
+ [attr.data-active]="rla.isActive || null"
7494
+ [queryParams]="{ n: tab.queryName, q: nav.searchText(), t: tab.wsQueryTab, f: undefined, sort: undefined, id: undefined, page: undefined }">
7495
+ @if (tab.icon) {
7496
+ <FaIcon [faClass]="tab.icon" aria-hidden="true" />
7497
+ }
7498
+ <span>{{ label }}</span>
7499
+ </sidebar-menu-button>
7500
+ </sidebar-menu-item>
7501
+ }
7502
+ </sidebar-menu>
7503
+ `, isInline: true, dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: FaIconComponent, selector: "fa-icon, FaIcon", inputs: ["faClass", "class"] }, { kind: "directive", type: SidebarMenuComponent, selector: "sidebar-menu", inputs: ["class"] }, { kind: "directive", type: SidebarMenuItemComponent, selector: "sidebar-menu-item", inputs: ["class"] }, { kind: "directive", type: SidebarMenuButtonComponent, selector: "sidebar-menu-button", inputs: ["isActive", "variant", "size", "class"] }, { kind: "directive", type: RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "directive", type: TooltipDirective, selector: "[tooltip]", inputs: ["tooltip", "strategy", "tooltip-position", "tooltip-delay", "tooltip-duration", "tooltip-offset"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }, { kind: "pipe", type: SyslangPipe, name: "syslang" }] });
7504
+ }
7505
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: SidebarNavComponent, decorators: [{
7506
+ type: Component,
7507
+ args: [{
7508
+ selector: "sidebar-nav",
7509
+ standalone: true,
7510
+ imports: [
7511
+ RouterLink,
7512
+ TranslocoPipe,
7513
+ SyslangPipe,
7514
+ FaIconComponent,
7515
+ SidebarMenuComponent,
7516
+ SidebarMenuItemComponent,
7517
+ SidebarMenuButtonComponent,
7518
+ RouterLinkActive,
7519
+ TooltipDirective
7520
+ ],
7521
+ template: `
7522
+ <sidebar-menu>
7523
+ @for (tab of nav.tabs(); track tab.path) {
7524
+ @let label = tab.display | syslang | transloco;
7525
+ <sidebar-menu-item>
7526
+ <sidebar-menu-button
7527
+ [tooltip]="label" tooltip-position="right"
7528
+ [routerLink]="tab.routerLink"
7529
+ routerLinkActive="active"
7530
+ #rla="routerLinkActive"
7531
+ [attr.data-active]="rla.isActive || null"
7532
+ [queryParams]="{ n: tab.queryName, q: nav.searchText(), t: tab.wsQueryTab, f: undefined, sort: undefined, id: undefined, page: undefined }">
7533
+ @if (tab.icon) {
7534
+ <FaIcon [faClass]="tab.icon" aria-hidden="true" />
7535
+ }
7536
+ <span>{{ label }}</span>
7537
+ </sidebar-menu-button>
7538
+ </sidebar-menu-item>
7539
+ }
7540
+ </sidebar-menu>
7541
+ `
7542
+ }]
7543
+ }], propDecorators: { path: [{ type: i0.Input, args: [{ isSignal: true, alias: "path", required: false }] }], showCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCount", required: false }] }] } });
7544
+
7353
7545
  class NoResultComponent {
7354
7546
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: NoResultComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
7355
7547
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.18", type: NoResultComponent, isStandalone: true, selector: "NoResult", host: { classAttribute: "p-4 flex flex-col gap-2 rounded-md" }, providers: [provideTranslocoScope("no-result")], ngImport: i0, template: `
@@ -7608,21 +7800,21 @@ class SponsoredResultsComponent {
7608
7800
  const { sponsoredLinks } = getState(this.appStore);
7609
7801
  return sponsoredLinks;
7610
7802
  }, ...(ngDevMode ? [{ debugName: "sponsoredLinks" }] : []));
7611
- sponsoredResults = signal(undefined, ...(ngDevMode ? [{ debugName: "sponsoredResults" }] : []));
7612
- constructor() {
7613
- afterNextRender(async () => {
7614
- if (this.sponsoredLinks()) {
7615
- const query = this.queryParamStore.getQuery();
7616
- const links = await withFetch(() => fetchSponsoredLinks(this.sponsoredLinks(), query), this.injector);
7617
- if (links) {
7618
- this.sponsoredResults.set(links?.slice(0, this.slice()));
7619
- }
7620
- }
7621
- });
7622
- }
7803
+ sponsoredResults = resource({
7804
+ params: () => ({
7805
+ query: this.queryParamStore.getQuery(),
7806
+ links: this.sponsoredLinks(),
7807
+ slice: this.slice(),
7808
+ }),
7809
+ loader: async ({ params: { query, links, slice } }) => {
7810
+ const results = await withFetch(() => fetchSponsoredLinks(links, query), this.injector);
7811
+ return results?.slice(0, slice) ?? [];
7812
+ },
7813
+ defaultValue: [],
7814
+ });
7623
7815
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: SponsoredResultsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
7624
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: SponsoredResultsComponent, isStandalone: true, selector: "sponsored-results, SponsoredResults, sponsoredresults", inputs: { slice: { classPropertyName: "slice", publicName: "slice", isSignal: true, isRequired: false, transformFunction: null }, displayPromoted: { classPropertyName: "displayPromoted", publicName: "displayPromoted", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "list" } }, queries: [{ propertyName: "childElement", first: true, predicate: ChildMarkerDirective, descendants: true, isSignal: true }], ngImport: i0, template: ` @if (sponsoredResults()?.length) {
7625
- @for (link of sponsoredResults(); track $index) {
7816
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: SponsoredResultsComponent, isStandalone: true, selector: "sponsored-results, SponsoredResults, sponsoredresults", inputs: { slice: { classPropertyName: "slice", publicName: "slice", isSignal: true, isRequired: false, transformFunction: null }, displayPromoted: { classPropertyName: "displayPromoted", publicName: "displayPromoted", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "list" } }, queries: [{ propertyName: "childElement", first: true, predicate: ChildMarkerDirective, descendants: true, isSignal: true }], ngImport: i0, template: ` @if (sponsoredResults.value().length) {
7817
+ @for (link of sponsoredResults.value(); track $index) {
7626
7818
  <li role="listitem" class="text-primary flex items-center gap-2 rounded px-3 py-2 font-bold">
7627
7819
  <a href="{{ link.url }}" target="_blank" rel="noopener" title="{{ link.tooltip }}" class="result-link peer flex items-center gap-2 hover:underline">
7628
7820
  <arrow-up-right-from-square-icon />
@@ -7645,8 +7837,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
7645
7837
  selector: 'sponsored-results, SponsoredResults, sponsoredresults',
7646
7838
  imports: [NgTemplateOutlet, ArrowUpRightFromSquareIcon],
7647
7839
  standalone: true,
7648
- template: ` @if (sponsoredResults()?.length) {
7649
- @for (link of sponsoredResults(); track $index) {
7840
+ template: ` @if (sponsoredResults.value().length) {
7841
+ @for (link of sponsoredResults.value(); track $index) {
7650
7842
  <li role="listitem" class="text-primary flex items-center gap-2 rounded px-3 py-2 font-bold">
7651
7843
  <a href="{{ link.url }}" target="_blank" rel="noopener" title="{{ link.tooltip }}" class="result-link peer flex items-center gap-2 hover:underline">
7652
7844
  <arrow-up-right-from-square-icon />
@@ -7666,7 +7858,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
7666
7858
  role: 'list'
7667
7859
  }
7668
7860
  }]
7669
- }], ctorParameters: () => [], propDecorators: { childElement: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ChildMarkerDirective), { isSignal: true }] }], slice: [{ type: i0.Input, args: [{ isSignal: true, alias: "slice", required: false }] }], displayPromoted: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayPromoted", required: false }] }] } });
7861
+ }], propDecorators: { childElement: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ChildMarkerDirective), { isSignal: true }] }], slice: [{ type: i0.Input, args: [{ isSignal: true, alias: "slice", required: false }] }], displayPromoted: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayPromoted", required: false }] }] } });
7670
7862
 
7671
7863
  class ThemeSelectorComponent {
7672
7864
  scope = input.required(...(ngDevMode ? [{ debugName: "scope" }] : []));
@@ -7905,11 +8097,16 @@ class AdvancedFiltersComponent {
7905
8097
  }, ...(ngDevMode ? [{ debugName: "allowEmptySearch" }] : []));
7906
8098
  text = "";
7907
8099
  constructor() {
7908
- this.getFirstPageQuery();
8100
+ effect(() => {
8101
+ getState(this.appStore);
8102
+ const query = this.appStore.getDefaultQuery();
8103
+ if (query?.name) {
8104
+ this.getFirstPageQuery(query?.name);
8105
+ }
8106
+ });
7909
8107
  }
7910
- async getFirstPageQuery() {
7911
- const query = this.appStore.getDefaultQuery() || { name: "_default" };
7912
- const response = await fetchQuery({ isFirstPage: true, name: query.name });
8108
+ async getFirstPageQuery(queryName) {
8109
+ const response = await fetchQuery({ isFirstPage: true, name: queryName });
7913
8110
  this.aggregations.set(response.aggregations);
7914
8111
  }
7915
8112
  onTabChange(tab) {
@@ -8447,6 +8644,139 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
8447
8644
  }]
8448
8645
  }], propDecorators: { dialog: [{ type: i0.ViewChild, args: [i0.forwardRef(() => DialogComponent), { isSignal: true }] }], customRange: [{ type: i0.ViewChild, args: ['customRange', { isSignal: true }] }], dualPickers: [{ type: i0.ViewChild, args: ['dualPickers', { isSignal: true }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], lang: [{ type: i0.Input, args: [{ isSignal: true, alias: "lang", required: false }] }], useDateRange: [{ type: i0.Input, args: [{ isSignal: true, alias: "useDateRange", required: false }] }], rangeSelected: [{ type: i0.Output, args: ["rangeSelected"] }] } });
8449
8646
 
8647
+ function injectAggregationBase(refs) {
8648
+ const aggregationsStore = inject(AggregationsStore);
8649
+ const queryParamsStore = inject(QueryParamsStore);
8650
+ const appStore = inject(AppStore);
8651
+ const aggregationsService = inject(AggregationsService);
8652
+ const injector = inject(Injector);
8653
+ const destroyRef = inject(DestroyRef);
8654
+ const debouncedSearchText = debouncedSignal(refs.searchText, 300);
8655
+ const normalizedSearchText = computed(() => debouncedSearchText()
8656
+ .normalize("NFD")
8657
+ .replace(/[̀-ͯ]/g, ""), ...(ngDevMode ? [{ debugName: "normalizedSearchText" }] : []));
8658
+ const suggests = signal([], ...(ngDevMode ? [{ debugName: "suggests" }] : []));
8659
+ const _filterCount = computed(() => {
8660
+ getState(aggregationsStore);
8661
+ const { count = 0 } = queryParamsStore.getFilter({ field: refs.column() ?? undefined, name: refs.name() ?? undefined }) || {};
8662
+ return count;
8663
+ }, ...(ngDevMode ? [{ debugName: "_filterCount" }] : []));
8664
+ const hasFilters = computed(() => _filterCount() > 0, ...(ngDevMode ? [{ debugName: "hasFilters" }] : []));
8665
+ const filtersCount = _filterCount;
8666
+ const query = buildQuery();
8667
+ const filters = signal([], ...(ngDevMode ? [{ debugName: "filters" }] : []));
8668
+ if (refs.searchInput && refs.expanded) {
8669
+ effect(() => {
8670
+ if (refs.searchInput()?.nativeElement && refs.expanded() !== null) {
8671
+ setTimeout(() => refs.searchInput()?.nativeElement.focus(), 0);
8672
+ }
8673
+ });
8674
+ }
8675
+ // Track both debounced text and column so a column change also resets suggestions.
8676
+ // switchMap cancels the in-flight fetch when a new value arrives, avoiding race conditions.
8677
+ const searchTrigger = computed(() => ({ text: debouncedSearchText(), column: refs.column() }), ...(ngDevMode ? [{ debugName: "searchTrigger" }] : []));
8678
+ toObservable(searchTrigger)
8679
+ .pipe(takeUntilDestroyed(destroyRef), switchMap(({ text, column }) => {
8680
+ if (text === "" || column === null)
8681
+ return of([]);
8682
+ const q = queryParamsStore.getQuery();
8683
+ return from(withFetch(() => fetchSuggestField(normalizedSearchText(), [column || ""], q), injector));
8684
+ }))
8685
+ .subscribe((result) => suggests.set(result || []));
8686
+ function clearSearch(e) {
8687
+ e.stopImmediatePropagation();
8688
+ refs.searchText.set("");
8689
+ }
8690
+ function selectItems(items, selected, recursive = false) {
8691
+ items.forEach((item) => {
8692
+ if (item.count > 0)
8693
+ item.$selected = selected;
8694
+ if (recursive && item.items?.length)
8695
+ selectItems(item.items, selected, true);
8696
+ });
8697
+ }
8698
+ function addCurrentFiltersToItems(aggregation) {
8699
+ const currentItems = aggregation?.items || [];
8700
+ if (!aggregation?.isTree && !aggregation?.isDistribution) {
8701
+ const currentFilters = queryParamsStore.getFilter({ field: aggregation?.column, name: aggregation?.name });
8702
+ if (currentFilters) {
8703
+ if (Array.isArray(currentFilters.filters) && currentFilters.filters.length) {
8704
+ currentFilters.filters.forEach((filter) => {
8705
+ const found = currentItems.find((item) => item.value?.toString().toLocaleLowerCase() === filter.value?.toLocaleLowerCase());
8706
+ if (!found) {
8707
+ currentItems.unshift({ value: filter.value, display: filter.display, $selected: true });
8708
+ }
8709
+ else {
8710
+ found.$selected = true;
8711
+ }
8712
+ });
8713
+ }
8714
+ else if (currentFilters.value) {
8715
+ const found = currentItems.find((item) => item.value?.toString().toLocaleLowerCase() === currentFilters.value?.toLocaleLowerCase());
8716
+ if (!found) {
8717
+ currentItems.push({ value: currentFilters.value, display: currentFilters.display, $selected: true });
8718
+ }
8719
+ else {
8720
+ found.$selected = true;
8721
+ }
8722
+ }
8723
+ }
8724
+ }
8725
+ return currentItems;
8726
+ }
8727
+ function applyFilters(appliedFilters, agg, clearFn) {
8728
+ const { name: aggName, column: field } = agg;
8729
+ if (appliedFilters.length > 1) {
8730
+ const display = appliedFilters[0].display;
8731
+ if (agg.isDistribution) {
8732
+ queryParamsStore.updateFilter({
8733
+ operator: "or",
8734
+ filters: appliedFilters,
8735
+ name: aggName,
8736
+ field,
8737
+ display,
8738
+ });
8739
+ }
8740
+ else {
8741
+ const values = appliedFilters.map((f) => f.value);
8742
+ queryParamsStore.updateFilter({
8743
+ operator: "in",
8744
+ name: aggName,
8745
+ field,
8746
+ values,
8747
+ display,
8748
+ filters: appliedFilters,
8749
+ });
8750
+ }
8751
+ }
8752
+ else if (appliedFilters.length === 1) {
8753
+ queryParamsStore.updateFilter(appliedFilters[0]);
8754
+ }
8755
+ else {
8756
+ clearFn();
8757
+ }
8758
+ }
8759
+ return {
8760
+ aggregationsStore,
8761
+ queryParamsStore,
8762
+ appStore,
8763
+ aggregationsService,
8764
+ injector,
8765
+ destroyRef,
8766
+ debouncedSearchText,
8767
+ normalizedSearchText,
8768
+ suggests,
8769
+ hasFilters,
8770
+ filtersCount,
8771
+ query,
8772
+ filters,
8773
+ clearSearch,
8774
+ selectItems,
8775
+ addCurrentFiltersToItems,
8776
+ applyFilters,
8777
+ };
8778
+ }
8779
+
8450
8780
  /**
8451
8781
  * Injection token that indicates whether custom date ranges are allowed.
8452
8782
  *
@@ -8466,507 +8796,67 @@ const FILTER_DATE_ALLOW_CUSTOM_RANGE = new InjectionToken("date allow custom ran
8466
8796
  factory: () => true
8467
8797
  });
8468
8798
 
8469
- class AggregationListItemComponent {
8799
+ const options = {
8800
+ year: 'numeric',
8801
+ month: '2-digit',
8802
+ day: '2-digit'
8803
+ };
8804
+ class AggregationDateComponent {
8470
8805
  cn = cn;
8471
- get disabled() {
8472
- return this.node().count === 0 ? "disabled" : null;
8473
- }
8806
+ /* view queries */
8807
+ dateRangeDialog = viewChild(AggregationDateRangeDialogComponent, ...(ngDevMode ? [{ debugName: "dateRangeDialog" }] : []));
8808
+ /* inputs */
8809
+ name = input(null, ...(ngDevMode ? [{ debugName: "name" }] : []));
8810
+ column = input.required(...(ngDevMode ? [{ debugName: "column" }] : []));
8811
+ id = input(null, ...(ngDevMode ? [{ debugName: "id" }] : []));
8812
+ collapsible = input(false, ...(ngDevMode ? [{ debugName: "collapsible" }] : []));
8813
+ collapsed = input(false, ...(ngDevMode ? [{ debugName: "collapsed" }] : []));
8814
+ searchable = input(undefined, ...(ngDevMode ? [{ debugName: "searchable" }] : []));
8815
+ showFiltersCount = input(false, ...(ngDevMode ? [{ debugName: "showFiltersCount", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
8816
+ title = input({ label: "Date", icon: "far fa-calendar-day" }, ...(ngDevMode ? [{ debugName: "title" }] : []));
8817
+ displayEmptyDistributionIntervals = input(false, ...(ngDevMode ? [{ debugName: "displayEmptyDistributionIntervals" }] : []));
8818
+ /* outputs */
8474
8819
  onSelect = output();
8475
- onOpen = output();
8476
- onFilter = output();
8477
- node = input.required(...(ngDevMode ? [{ debugName: "node" }] : []));
8478
- field = input(...(ngDevMode ? [undefined, { debugName: "field" }] : []));
8479
- appStore = inject(AppStore);
8480
- queryParamsStore = inject(QueryParamsStore);
8481
- searchText = inject(AggregationListComponent).searchText;
8482
- // is the count of items displayed, default to false
8483
- showCount = computed(() => this.appStore.general()?.features?.showAggregationItemCount ?? false, ...(ngDevMode ? [{ debugName: "showCount" }] : []));
8484
- quickFilter = computed(() => this.appStore.general()?.features?.quickFilter, ...(ngDevMode ? [{ debugName: "quickFilter" }] : []));
8485
- linkChildren = computed(() => this.appStore.general()?.features?.filterLinkChildren, ...(ngDevMode ? [{ debugName: "linkChildren" }] : []));
8486
- isFiltered = computed(() => {
8487
- const filters = this.queryParamsStore.getFilter({ field: this.field(), name: this.name() });
8488
- if (!filters)
8489
- return false;
8490
- const values = [this.node().value]; // to also consider the treepath value
8491
- return (values.some((v) => v === filters.value) ||
8492
- (filters.values?.length && filters.values.some((value) => values.some((v) => v === value))));
8493
- }, ...(ngDevMode ? [{ debugName: "isFiltered" }] : []));
8494
- name = computed(() => {
8495
- const value = this.node().display || this.node().value;
8496
- return typeof value === "string" ? value : `${value}`;
8497
- }, ...(ngDevMode ? [{ debugName: "name" }] : []));
8498
- select(item, e) {
8499
- e?.stopImmediatePropagation();
8500
- const selected = !item.$selected;
8501
- item.$selected = selected;
8502
- this.onSelect.emit(item);
8503
- }
8504
- onTextClick(event) {
8505
- if (this.quickFilter()) {
8506
- this.select(this.node(), event);
8507
- this.onFilter.emit();
8508
- }
8509
- }
8510
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AggregationListItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
8511
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: AggregationListItemComponent, isStandalone: true, selector: "aggregation-list-item, AggregationListItem, aggregationlistitem", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null }, field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSelect: "onSelect", onOpen: "onOpen", onFilter: "onFilter" }, host: { properties: { "attr.disabled": "this.disabled" } }, ngImport: i0, template: "<a\r\n role=\"listitem\"\r\n [attr.aria-selected]=\"node().$selected\"\r\n [attr.aria-label]=\"name() | syslang\"\r\n [class]=\"\r\n cn(\r\n 'flex grow items-center gap-2 p-1 leading-7',\r\n node().count === 0 && 'disabled pointer-events-none',\r\n node().$selected && ''\r\n )\r\n \"\r\n (click)=\"select(node(), $event)\">\r\n <input\r\n type=\"checkbox\"\r\n role=\"checkbox\"\r\n value=\"{{ node().value }}\"\r\n [attr.disabled]=\"node().count === 0 ? true : null\"\r\n [attr.aria-disabled]=\"node().count === 0\"\r\n (keydown.enter)=\"select(node(), $event)\"\r\n [checked]=\"node().$selected\" />\r\n\r\n @let icon = node().icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"self-center justify-self-center\" />\r\n }\r\n <span\r\n [class]=\"\r\n cn(\r\n 'line-clamp-1 break-all text-ellipsis',\r\n quickFilter() && 'hover:underline'\r\n )\r\n \"\r\n [title]=\"\r\n quickFilter()\r\n ? ((isFiltered() ? 'filters.removeFilter' : 'filters.addFilter')\r\n | transloco) +\r\n ': ' +\r\n (name() | syslang)\r\n : (name() | syslang)\r\n \"\r\n (click)=\"onTextClick($event)\">\r\n @for (\r\n chunk of (name() | syslang) ?? \"\" | highlightWord: searchText() : 10;\r\n track $index\r\n ) {\r\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\r\n chunk.text\r\n }}</span>\r\n }\r\n </span>\r\n @if (showCount() && node().count > 0) {\r\n <span class=\"ml-auto px-1 text-xs empty:hidden\" aria-hidden=\"true\">{{\r\n node().count\r\n }}</span>\r\n }\r\n</a>\r\n", styles: [":host{display:block;-webkit-user-select:none;user-select:none}:host a{padding-left:var(--agg-tree-indent, .5rem)}a{line-height:var(--agg-item-height, inherit)}\n"], dependencies: [{ kind: "directive", type: ListItemComponent, selector: "[role=\"listitem\"], [role=\"option\"]", inputs: ["class", "variant", "decoration"] }, { kind: "component", type: FaIconComponent, selector: "fa-icon, FaIcon", inputs: ["faClass", "class"] }, { kind: "pipe", type: HighlightWordPipe, name: "highlightWord" }, { kind: "pipe", type: SyslangPipe, name: "syslang" }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
8512
- }
8513
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AggregationListItemComponent, decorators: [{
8514
- type: Component,
8515
- args: [{ selector: "aggregation-list-item, AggregationListItem, aggregationlistitem", standalone: true, imports: [HighlightWordPipe, ListItemComponent, SyslangPipe, TranslocoPipe, FaIconComponent], template: "<a\r\n role=\"listitem\"\r\n [attr.aria-selected]=\"node().$selected\"\r\n [attr.aria-label]=\"name() | syslang\"\r\n [class]=\"\r\n cn(\r\n 'flex grow items-center gap-2 p-1 leading-7',\r\n node().count === 0 && 'disabled pointer-events-none',\r\n node().$selected && ''\r\n )\r\n \"\r\n (click)=\"select(node(), $event)\">\r\n <input\r\n type=\"checkbox\"\r\n role=\"checkbox\"\r\n value=\"{{ node().value }}\"\r\n [attr.disabled]=\"node().count === 0 ? true : null\"\r\n [attr.aria-disabled]=\"node().count === 0\"\r\n (keydown.enter)=\"select(node(), $event)\"\r\n [checked]=\"node().$selected\" />\r\n\r\n @let icon = node().icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"self-center justify-self-center\" />\r\n }\r\n <span\r\n [class]=\"\r\n cn(\r\n 'line-clamp-1 break-all text-ellipsis',\r\n quickFilter() && 'hover:underline'\r\n )\r\n \"\r\n [title]=\"\r\n quickFilter()\r\n ? ((isFiltered() ? 'filters.removeFilter' : 'filters.addFilter')\r\n | transloco) +\r\n ': ' +\r\n (name() | syslang)\r\n : (name() | syslang)\r\n \"\r\n (click)=\"onTextClick($event)\">\r\n @for (\r\n chunk of (name() | syslang) ?? \"\" | highlightWord: searchText() : 10;\r\n track $index\r\n ) {\r\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\r\n chunk.text\r\n }}</span>\r\n }\r\n </span>\r\n @if (showCount() && node().count > 0) {\r\n <span class=\"ml-auto px-1 text-xs empty:hidden\" aria-hidden=\"true\">{{\r\n node().count\r\n }}</span>\r\n }\r\n</a>\r\n", styles: [":host{display:block;-webkit-user-select:none;user-select:none}:host a{padding-left:var(--agg-tree-indent, .5rem)}a{line-height:var(--agg-item-height, inherit)}\n"] }]
8516
- }], propDecorators: { disabled: [{
8517
- type: HostBinding,
8518
- args: ["attr.disabled"]
8519
- }], onSelect: [{ type: i0.Output, args: ["onSelect"] }], onOpen: [{ type: i0.Output, args: ["onOpen"] }], onFilter: [{ type: i0.Output, args: ["onFilter"] }], node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: true }] }], field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: false }] }] } });
8520
-
8521
- class AggregationListComponent {
8522
- cn = cn;
8523
- cdr = inject(ChangeDetectorRef);
8524
- /* virtualizer */
8525
- scrollElement = viewChild("scrollElement", ...(ngDevMode ? [{ debugName: "scrollElement" }] : []));
8526
- virtualizer = injectVirtualizer(() => ({
8527
- count: this.items().length,
8528
- estimateSize: () => 32,
8529
- scrollElement: this.scrollElement()
8530
- }));
8531
- searchInput = viewChild("searchInput", ...(ngDevMode ? [{ debugName: "searchInput" }] : []));
8532
- /* stores */
8533
- aggregationsStore = inject(AggregationsStore);
8534
- queryParamsStore = inject(QueryParamsStore);
8535
- appStore = inject(AppStore);
8536
- /* services */
8537
- aggregationsService = inject(AggregationsService);
8538
- el = inject(ElementRef);
8539
- injector = inject(Injector);
8540
- destroyRef = inject(DestroyRef);
8541
- class = input("", ...(ngDevMode ? [{ debugName: "class" }] : []));
8542
- /**
8543
- * The name of the <details> element. When you provide the same id, the component work as an accordion
8544
- * @defaultValue null
8545
- */
8546
- id = input(null, ...(ngDevMode ? [{ debugName: "id" }] : []));
8547
- name = input.required(...(ngDevMode ? [{ debugName: "name" }] : []));
8548
- column = input.required(...(ngDevMode ? [{ debugName: "column" }] : []));
8549
- onSelect = output();
8550
- onApply = output();
8551
- onClear = output();
8552
- /**
8553
- * Determines whether the aggregation component can be collapsed or expanded.
8554
- * When true, the component will display collapse/expand controls allowing users
8555
- * to show or hide the aggregation content.
8556
- *
8557
- * @default false
8558
- */
8559
- collapsible = input(false, ...(ngDevMode ? [{ debugName: "collapsible" }] : []));
8560
- /**
8561
- * Controls whether the aggregation component is in a collapsed state.
8562
- * When true, the component will be visually collapsed/hidden.
8563
- * When false, the component will be expanded/visible.
8564
- *
8565
- * @default false
8566
- */
8567
- collapsed = input(false, ...(ngDevMode ? [{ debugName: "collapsed" }] : []));
8568
- /**
8569
- * A computed signal that tracks the collapsed state of the component.
8570
- * This signal is linked to the `collapsed()` signal and automatically updates
8571
- * when the collapsed state changes.
8572
- */
8573
- isCollapsed = linkedSignal(() => this.collapsed(), ...(ngDevMode ? [{ debugName: "isCollapsed" }] : []));
8574
- /**
8575
- * Computed property that returns an empty string when the component is not collapsed,
8576
- * or null when the component is collapsed. This is typically used to control
8577
- * expansion state in UI components with conditional rendering or styling.
8578
- *
8579
- * @returns empty string if not collapsed, null if collapsed
8580
- */
8581
- expanded = computed(() => (this.isCollapsed() ? null : ""), ...(ngDevMode ? [{ debugName: "expanded" }] : []));
8582
- /**
8583
- * A boolean flag indicating whether the component is searchable.
8584
- * This property is initialized to `undefined` by default.
8585
- * "Undefined" and not "false" because this input overrides the custom json settings
8586
- */
8587
- searchable = input(undefined, ...(ngDevMode ? [{ debugName: "searchable" }] : []));
8588
- selection = signal(false, ...(ngDevMode ? [{ debugName: "selection" }] : []));
8589
- /**
8590
- * A boolean flag indicating whether we want to see the filters count when some is applied
8591
- * This property is initialized to `false` by default.
8592
- */
8593
- showFiltersCount = input(null, ...(ngDevMode ? [{ debugName: "showFiltersCount", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
8594
- /* aggregation */
8595
- aggregation = computed(() => {
8596
- // when the aggegation store updates, we need to check if the aggregation is still valid
8597
- getState(this.aggregationsStore);
8598
- const name = this.name();
8599
- const column = this.column();
8600
- if (name !== null) {
8601
- const agg = this.aggregationsService.processAggregation(name, column);
8602
- if (agg) {
8603
- if (agg.isTree) {
8604
- error("The aggregation component does not support tree aggregations. Please use the <AggregationTree /> component instead.");
8605
- }
8606
- // overrides "searchable" properties with the input if any
8607
- agg.searchable = this.searchable() ?? agg.searchable;
8608
- return agg;
8609
- }
8820
+ onApply = output();
8821
+ onClear = output();
8822
+ /* collapse state */
8823
+ isCollapsed = linkedSignal(() => this.collapsed(), ...(ngDevMode ? [{ debugName: "isCollapsed" }] : []));
8824
+ expanded = computed(() => (this.isCollapsed() ? null : ""), ...(ngDevMode ? [{ debugName: "expanded" }] : []));
8825
+ /* search state — unused by date component but required by AggregationBaseRefs */
8826
+ searchText = model("", ...(ngDevMode ? [{ debugName: "searchText" }] : []));
8827
+ searchInput = signal(undefined, ...(ngDevMode ? [{ debugName: "searchInput" }] : []));
8828
+ /* composable injects stores/services, wires search effects, provides shared methods */
8829
+ base = injectAggregationBase({
8830
+ name: this.name,
8831
+ column: this.column,
8832
+ searchText: this.searchText,
8833
+ searchInput: this.searchInput,
8834
+ expanded: this.expanded,
8835
+ });
8836
+ /* spread from base */
8837
+ aggregationsService = this.base.aggregationsService;
8838
+ queryParamsStore = this.base.queryParamsStore;
8839
+ hasFilters = this.base.hasFilters;
8840
+ destroyRef = this.base.destroyRef;
8841
+ /* injected services */
8842
+ allowCustomRange = inject(FILTER_DATE_ALLOW_CUSTOM_RANGE);
8843
+ transloco = inject(TranslocoService);
8844
+ /* state */
8845
+ selection = signal(false, ...(ngDevMode ? [{ debugName: "selection" }] : []));
8846
+ validSelection = signal(false, ...(ngDevMode ? [{ debugName: "validSelection" }] : []));
8847
+ aggregation = computed(() => {
8848
+ const name = this.name();
8849
+ if (name !== null) {
8850
+ const agg = this.aggregationsService.processAggregation(name, this.column());
8851
+ return {
8852
+ ...agg,
8853
+ items: agg?.items?.filter((item) => item.display !== "custom-range") ?? []
8854
+ };
8610
8855
  }
8611
8856
  return null;
8612
8857
  }, ...(ngDevMode ? [{ debugName: "aggregation" }] : []));
8613
- /* items of the aggregation */
8614
- items = computed(() => {
8615
- // when the aggegation store updates, we need to check if the aggregation is still valid
8616
- getState(this.aggregationsStore);
8617
- const agg = this.aggregation();
8618
- const searchedItems = this.searchedItems();
8619
- if (searchedItems.length > 0) {
8620
- return searchedItems;
8621
- }
8622
- else if (agg?.items) {
8623
- const res = this.addCurrentFiltersToItems();
8624
- return res;
8625
- }
8626
- return [];
8627
- }, ...(ngDevMode ? [{ debugName: "items" }] : []));
8628
- /**
8629
- * Computed signal that determines whether the items collection is empty.
8630
- * @returns True if the items array has no elements, false otherwise.
8631
- */
8632
- isEmpty = computed(() => this.items().length === 0, ...(ngDevMode ? [{ debugName: "isEmpty" }] : []));
8633
- /**
8634
- * A computed property that determines whether there are active filters
8635
- * for the current aggregation column.
8636
- *
8637
- * if True, the clear button is shown.
8638
- *
8639
- * @returns {boolean} `true` if the filter count for the aggregation column is greater than 0, otherwise `false`.
8640
- */
8641
- hasFilters = computed(() => {
8642
- const { count = 0 } = this.queryParamsStore.getFilter({ field: this.aggregation()?.column, name: this.aggregation()?.name }) || {};
8643
- return count > 0;
8644
- }, ...(ngDevMode ? [{ debugName: "hasFilters" }] : []));
8645
- /**
8646
- * A computed property that returns the number of items of this aggregation applied in the active filters
8647
- *
8648
- * if more than 0 and the showCount input is set as True, the count number is shown.
8649
- *
8650
- * @returns {number} the filters count.
8651
- */
8652
- filtersCount = computed(() => {
8653
- const { count = 0 } = this.queryParamsStore.getFilter({ field: this.aggregation()?.column, name: this.aggregation()?.name }) || {};
8654
- return count;
8655
- }, ...(ngDevMode ? [{ debugName: "filtersCount" }] : []));
8656
- isAllSelected = signal(false, ...(ngDevMode ? [{ debugName: "isAllSelected" }] : []));
8657
- /* search feature */
8658
- searchText = model("", ...(ngDevMode ? [{ debugName: "searchText" }] : []));
8659
- debouncedSearchText = debouncedSignal(this.searchText, 300);
8660
- normalizedSearchText = computed(() => this.debouncedSearchText()
8661
- .normalize("NFD")
8662
- .replace(/[\u0300-\u036f]/g, ""), ...(ngDevMode ? [{ debugName: "normalizedSearchText" }] : []));
8663
- /* suggestions */
8664
- suggests = signal([], ...(ngDevMode ? [{ debugName: "suggests" }] : []));
8665
- /* searched items */
8666
- searchedItems = computed(() => {
8667
- if (!this.suggests())
8668
- return [];
8669
- // if the aggregation is not a tree, we return the suggestions as is
8670
- return this.suggests()?.map(suggest => {
8671
- const column = this.appStore.getColumn(suggest.category);
8672
- const item = {
8673
- name: this.name(),
8674
- value: suggest.normalized || suggest.display || "",
8675
- display: suggest.display,
8676
- column: column?.name ?? suggest.category,
8677
- count: Number(suggest.frequency),
8678
- $selected: false
8679
- };
8680
- // if the column is of type boolean, we need to convert the value to a boolean
8681
- if (column?.eType === EngineType.bool) {
8682
- item.value = Boolean(item.value);
8683
- }
8684
- return item;
8685
- });
8686
- }, ...(ngDevMode ? [{ debugName: "searchedItems" }] : []));
8687
- query;
8688
- filters = signal([], ...(ngDevMode ? [{ debugName: "filters" }] : []));
8689
- constructor() {
8690
- this.query = buildQuery();
8691
- effect(() => {
8692
- // focus the search input when expanded
8693
- if (this.searchInput()?.nativeElement && this.expanded() !== null) {
8694
- setTimeout(() => {
8695
- this.searchInput()?.nativeElement.focus();
8696
- }, 0);
8697
- }
8698
- });
8699
- effect(async () => {
8700
- if (this.debouncedSearchText() === "" || this.aggregation() === null) {
8701
- this.suggests.set([]);
8702
- return;
8703
- }
8704
- const query = this.queryParamsStore.getQuery();
8705
- const suggests = (await withFetch(() => fetchSuggestField(this.normalizedSearchText(), [this.aggregation()?.column || ""], query), this.injector)) || [];
8706
- this.suggests.set(suggests);
8707
- });
8708
- this.destroyRef.onDestroy(() => {
8709
- // If the popover is closed with unapplied selections, reset $selected to undefined
8710
- // so that processAggregation can recompute it from active filters on next open
8711
- if (this.selection()) {
8712
- this.aggregation()?.items?.forEach(item => {
8713
- item.$selected = undefined;
8714
- });
8715
- }
8716
- });
8717
- }
8718
- /**
8719
- * Clears the current filter for the aggregation column.
8720
- *
8721
- * This method updates the filter in the `queryParamsStore` by setting the display value
8722
- * of the current aggregation column to an empty string.
8723
- */
8724
- clear() {
8725
- const agg = this.aggregation();
8726
- if (agg) {
8727
- this.queryParamsStore.removeFilterByName(agg.name, agg.column);
8728
- this.selection.set(false);
8729
- this.isAllSelected.set(false);
8730
- }
8731
- this.onSelect.emit([]);
8732
- this.onClear.emit();
8733
- }
8734
- /**
8735
- * Select all filters for the aggregation column.
8736
- */
8737
- selectAll() {
8738
- if (this.items().length) {
8739
- this.selectItems(this.items(), true);
8740
- this.selection.set(true);
8741
- this.isAllSelected.set(true);
8742
- }
8743
- }
8744
- /**
8745
- * Unselect all filters for the aggregation column.
8746
- */
8747
- unselectAll() {
8748
- if (this.items().length) {
8749
- this.selectItems(this.items(), false);
8750
- this.select();
8751
- this.isAllSelected.set(false);
8752
- }
8753
- }
8754
- /**
8755
- * Applies the current filters to the query parameters store.
8756
- *
8757
- * - If there are multiple filters, they are wrapped in an "or" filter.
8758
- * - If the aggregation is not a distribution, the filters are merged into a single filter with an "in" operator.
8759
- * - If there is only one filter, it is directly applied.
8760
- * - If there are no filters, the current filters are cleared.
8761
- *
8762
- * After applying the filters, the search text is reset.
8763
- */
8764
- apply() {
8765
- const filters = this.getFilters();
8766
- const agg = this.aggregation();
8767
- if (!agg)
8768
- return;
8769
- const { name, column: field } = agg;
8770
- // if filters length > 1, we need to wrap them in an "or" filter
8771
- if (filters.length > 1) {
8772
- const display = filters[0].display;
8773
- // if aggregation not a distribution, we need to merge the filters into a single filter with an in operator
8774
- // with the values of the filters
8775
- if (this.aggregation()?.isDistribution) {
8776
- this.queryParamsStore.updateFilter({
8777
- operator: "or",
8778
- filters,
8779
- name,
8780
- field,
8781
- display
8782
- });
8783
- }
8784
- else {
8785
- const values = filters.map(filter => filter.value);
8786
- this.queryParamsStore.updateFilter({
8787
- operator: "in",
8788
- name,
8789
- field,
8790
- values,
8791
- display,
8792
- filters
8793
- });
8794
- }
8795
- }
8796
- else if (filters.length === 1) {
8797
- this.queryParamsStore.updateFilter(filters[0]);
8798
- }
8799
- else {
8800
- this.clear();
8801
- }
8802
- this.searchText.set("");
8803
- this.selection.set(false);
8804
- this.onApply.emit();
8805
- }
8806
- loadMore() {
8807
- const q = this.queryParamsStore.getQuery();
8808
- this.aggregationsService.loadMore(q, this.aggregation()).subscribe(aggregation => {
8809
- this.aggregationsStore.updateAggregation(aggregation);
8810
- this.cdr.detectChanges();
8811
- });
8812
- }
8813
- /**
8814
- * Updates the selected state of the given item in the aggregation list.
8815
- *
8816
- * @param item - The item to be selected or deselected.
8817
- *
8818
- * This method iterates through the items in the aggregation list and updates
8819
- * the `$selected` property of the item that matches the value of the given item.
8820
- *
8821
- * If the item is selected, the selection count is incremented by 1.
8822
- * If the item is deselected, the selection count is decremented by 1.
8823
- */
8824
- select() {
8825
- const selectedItems = this.items().filter(item => item.$selected);
8826
- this.onSelect.emit(selectedItems);
8827
- // Keep apply visible if items are selected, or if active filters exist (user may be deselecting to clear)
8828
- this.selection.set(selectedItems.length > 0 || this.hasFilters());
8829
- }
8830
- /**
8831
- * Updates the collapsed status on header click if the component is collapsible.
8832
- */
8833
- onHeaderClick(event) {
8834
- event.preventDefault();
8835
- const isDate = this.aggregationsService.appStore.isDateColumn(this.aggregation()?.column || "");
8836
- // prevent header click if no items are present
8837
- if (!isDate && this.isEmpty()) {
8838
- return;
8839
- }
8840
- if (this.collapsible()) {
8841
- this.isCollapsed.update(value => !value);
8842
- }
8843
- }
8844
- /**
8845
- * Retrieves a list of filters based on the selected items.
8846
- *
8847
- * This method filters the items to include only those that are selected,
8848
- * and then maps each selected item to a filter using the `toFilter` method.
8849
- *
8850
- * @returns {LegacyFilter[]} An array of filters corresponding to the selected items.
8851
- */
8852
- getFilters() {
8853
- const items = this.addCurrentFiltersToItems().filter(item => item.$selected);
8854
- const searchedItems = this.searchedItems().filter(item => item.$selected);
8855
- const currentItems = [...items, ...searchedItems];
8856
- const { column, name, isDistribution = false } = this.aggregation() || {};
8857
- const selectedItems = currentItems.map(item => this.aggregationsService.toFilter(item, column, name, isDistribution));
8858
- return selectedItems;
8859
- }
8860
- addCurrentFiltersToItems() {
8861
- const aggItems = (this.aggregation()?.items) || [];
8862
- // add the current filters to the current items only if they are not already present
8863
- if (!this.aggregation()?.isTree && (!this.aggregation()?.isDistribution || this.aggregation()?.isDistribution === false)) {
8864
- // get all active filters for the current aggregation/column
8865
- const activeFilters = this.queryParamsStore.getFilter({
8866
- field: this.aggregation()?.column,
8867
- name: this.aggregation()?.name
8868
- });
8869
- // if there are active filters, we need to add them to the current items
8870
- if (activeFilters) {
8871
- // multiples filters
8872
- if (activeFilters.filters) {
8873
- activeFilters.filters.forEach((filter) => {
8874
- // check if the filter is already present in the current items
8875
- // if not, add it to the current items
8876
- const found = aggItems.find(item => item.value && item.value.toLocaleString().toLocaleLowerCase() === filter.value?.toLocaleLowerCase());
8877
- if (!found) {
8878
- // add it to the current items
8879
- aggItems.unshift({
8880
- value: filter.value,
8881
- display: filter.display,
8882
- $selected: true
8883
- });
8884
- }
8885
- });
8886
- }
8887
- else {
8888
- // single filter
8889
- const found = aggItems.find(item => item.value?.toString().toLocaleLowerCase() === activeFilters.value?.toLocaleLowerCase());
8890
- if (!found) {
8891
- // add it to the current items
8892
- aggItems.push({
8893
- value: activeFilters.value,
8894
- display: activeFilters.display,
8895
- $selected: true
8896
- });
8897
- }
8898
- }
8899
- }
8900
- }
8901
- return aggItems;
8902
- }
8903
- /**
8904
- * Update the $selected property to the selected parameter to all items
8905
- *
8906
- * @param items the items to apply to
8907
- * @param selected the selected status
8908
- */
8909
- selectItems(items, selected) {
8910
- items.forEach(item => {
8911
- // don't select disabled items
8912
- if (item.count > 0) {
8913
- item.$selected = selected;
8914
- }
8915
- });
8916
- }
8917
- onToggle(event) {
8918
- const e = event;
8919
- this.isCollapsed.set(e.newState === "closed");
8920
- }
8921
- clearSearch(e) {
8922
- e.stopImmediatePropagation();
8923
- this.searchText.set("");
8924
- }
8925
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AggregationListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
8926
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: AggregationListComponent, isStandalone: true, selector: "AggregationList, aggregation-list, aggregationlist", inputs: { class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null }, column: { classPropertyName: "column", publicName: "column", isSignal: true, isRequired: true, transformFunction: null }, collapsible: { classPropertyName: "collapsible", publicName: "collapsible", isSignal: true, isRequired: false, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, searchable: { classPropertyName: "searchable", publicName: "searchable", isSignal: true, isRequired: false, transformFunction: null }, showFiltersCount: { classPropertyName: "showFiltersCount", publicName: "showFiltersCount", isSignal: true, isRequired: false, transformFunction: null }, searchText: { classPropertyName: "searchText", publicName: "searchText", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSelect: "onSelect", onApply: "onApply", onClear: "onClear", searchText: "searchTextChange" }, host: { properties: { "class": "cn(\"block h-[inherit] max-h-[inherit]\",class())" } }, viewQueries: [{ propertyName: "scrollElement", first: true, predicate: ["scrollElement"], descendants: true, isSignal: true }, { propertyName: "searchInput", first: true, predicate: ["searchInput"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (aggregation()?.isTree) {\r\n <div class=\"p-2 text-sm text-red-500\">\r\n <triangle-alert-icon class=\"mr-1\" />\r\n The aggregation component no longer supports tree aggregations. Please use\r\n the &lt;AggregationTree /&gt; component instead.\r\n </div>\r\n}\r\n<details\r\n [attr.open]=\"expanded()\"\r\n [attr.name]=\"id()\"\r\n class=\"group space-y-2\"\r\n (toggle)=\"onToggle($event)\">\r\n <summary\r\n [class.cursor-pointer]=\"collapsible() && !isEmpty()\"\r\n [class.text-muted-foreground]=\"isEmpty()\"\r\n class=\"m-0 mt-1 flex h-8 w-full items-center gap-1 pl-1 font-semibold select-none\"\r\n (click)=\"onHeaderClick($event)\">\r\n <ng-content select=\"label\">\r\n @let icon = aggregation()?.icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\r\n }\r\n <span class=\"grow truncate\">{{\r\n aggregation()?.display | syslang | transloco\r\n }}</span>\r\n </ng-content>\r\n\r\n @if (showFiltersCount() && filtersCount() > 0) {\r\n <!-- count -->\r\n <Badge size=\"xs\" class=\"ml-1\">\r\n {{ filtersCount() }}\r\n </Badge>\r\n }\r\n <!-- apply filter block -->\r\n @if (!isCollapsed()) {\r\n @if (hasFilters()) {\r\n @let label = \"filters.clearFilters\" | transloco;\r\n <button\r\n variant=\"none\"\r\n icon-button\r\n [aria-label]=\"label\"\r\n (click)=\"$event.stopPropagation(); clear()\">\r\n <filter-x-icon />\r\n </button>\r\n }\r\n @if (selection()) {\r\n @let label = \"filters.apply\" | transloco;\r\n <button\r\n variant=\"accent\"\r\n size=\"sm\"\r\n [aria-label]=\"label\"\r\n (click)=\"$event.stopPropagation(); apply()\">\r\n <FilterIcon />\r\n {{ label }}\r\n </button>\r\n }\r\n\r\n <!-- select / unselect all -->\r\n @if (isAllSelected()) {\r\n @let label = \"filters.unselectAllFilters\" | transloco;\r\n <button\r\n variant=\"none\"\r\n icon-button\r\n [aria-label]=\"label\"\r\n (click)=\"$event.stopPropagation(); unselectAll()\">\r\n <square-check-icon />\r\n </button>\r\n } @else {\r\n @let label = \"filters.selectAllFilters\" | transloco;\r\n <button\r\n variant=\"none\"\r\n icon-button\r\n [aria-label]=\"label\"\r\n (click)=\"$event.stopPropagation(); selectAll()\">\r\n <square-icon />\r\n </button>\r\n }\r\n }\r\n\r\n @if (collapsible()) {\r\n <icon-button\r\n title=\"Open/Close\"\r\n class=\"cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\r\n <chevronright />\r\n <span class=\"sr-only\">{{ \"filters.toggle\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n </summary>\r\n\r\n <!-- content wrapper -->\r\n @if (aggregation()?.searchable && items().length) {\r\n <InputGroup class=\"group/item mt-1\">\r\n <input\r\n #searchInput\r\n input-group\r\n id=\"aggregation-input-{{ column() }}\"\r\n type=\"text\"\r\n [attr.placeholder]=\"'search' | transloco\"\r\n [(ngModel)]=\"searchText\"\r\n class=\"mt-1\" />\r\n <InputGroupAddon>\r\n <SearchIcon\r\n class=\"text-foreground size-4 rotate-0 transition-[rotate] duration-500 group-focus-within/item:rotate-90\" />\r\n </InputGroupAddon>\r\n <InputGroupAddon align=\"inline-end\" class=\"gap-0.5!\">\r\n <icon-button\r\n size=\"sm\"\r\n [class]=\"\r\n searchText().length > 0\r\n ? 'rotate-90 cursor-pointer opacity-100 transition-[rotate,opacity] duration-500'\r\n : 'pointer-events-none rotate-0 opacity-0 transition-[rotate,opacity] duration-500'\r\n \"\r\n aria-label=\"Clear search\"\r\n [tabindex]=\"searchText().length > 0 ? 0 : -1\"\r\n (keydown.enter)=\"clearSearch($event)\"\r\n (click)=\"clearSearch($event)\">\r\n <XMarkIcon />\r\n </icon-button>\r\n <ng-content />\r\n </InputGroupAddon>\r\n </InputGroup>\r\n }\r\n\r\n <div\r\n #scrollElement\r\n class=\"scrollbar-thin max-h-[calc(var(--height,100%)-100px)] w-full overflow-auto\">\r\n <div\r\n class=\"relative w-full\"\r\n [style.height]=\"virtualizer.getTotalSize() + 'px'\"\r\n role=\"list\"\r\n [attr.aria-label]=\"aggregation()?.display | syslang | transloco\">\r\n @for (vItem of virtualizer.getVirtualItems(); track vItem.index) {\r\n @let item = items()[vItem.index];\r\n <div\r\n class=\"absolute w-full\"\r\n [style.transform]=\"'translateY(' + vItem.start + 'px)'\"\r\n role=\"listitem\">\r\n <AggregationListItem\r\n [node]=\"item\"\r\n [field]=\"aggregation()?.column\"\r\n (onSelect)=\"select()\"\r\n (onFilter)=\"apply()\" />\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n @if (aggregation()?.$hasMore && this.searchedItems().length === 0) {\r\n <button\r\n class=\"mt-1 flex w-full justify-center\"\r\n [attr.aria-label]=\"'loadMore' | transloco\"\r\n (click)=\"loadMore()\">\r\n {{ \"loadMore\" | transloco }}\r\n </button>\r\n }\r\n</details>\r\n", styles: ["AggregationItem:has(+AggregationItem){margin-bottom:var(--agg-item-gap, 0)}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { 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"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: ButtonComponent, selector: "button", inputs: ["class", "variant", "decoration", "scheme", "iconOnly", "size", "solid"] }, { kind: "component", type: AggregationListItemComponent, selector: "aggregation-list-item, AggregationListItem, aggregationlistitem", inputs: ["node", "field"], outputs: ["onSelect", "onOpen", "onFilter"] }, { kind: "directive", type: BadgeComponent, selector: "badge, Badge", inputs: ["class", "variant", "scheme", "size"] }, { kind: "component", type: ChevronRightIcon, selector: "chevron-right, ChevronRight, chevronright, ChevronRightIcon, chevron-right-icon, chevronrighticon", inputs: ["class"] }, { kind: "directive", type: InputGroupInput, selector: "input[input-group]", inputs: ["class", "type", "placeholder", "disabled"] }, { kind: "directive", type: InputGroupComponent, selector: "input-group, inputgroup, InputGroup", inputs: ["class"] }, { kind: "directive", type: InputGroupAddonComponent, selector: "input-group-addon, inputgroupaddon, InputGroupAddon", inputs: ["class", "align"] }, { kind: "component", type: SearchIcon, selector: "SearchIcon", inputs: ["class"] }, { kind: "component", type: FilterIcon, selector: "filter-icon, FilterIcon", inputs: ["class"] }, { kind: "component", type: FaIconComponent, selector: "fa-icon, FaIcon", inputs: ["faClass", "class"] }, { kind: "component", type: TriangleAlertIcon, selector: "triangle-alert-icon, TriangleAlertIcon", inputs: ["class"] }, { kind: "component", type: FilterXIcon, selector: "filter-x-icon, FilterXIcon", inputs: ["class"] }, { kind: "component", type: SquareCheckIcon, selector: "square-check-icon, SquareCheckIcon", inputs: ["class"] }, { kind: "component", type: SquareIcon, selector: "square-icon, SquareIcon", inputs: ["class"] }, { kind: "directive", type: IconButtonComponent, selector: "button[icon-button], icon-button, IconButton", inputs: ["class", "size"] }, { kind: "component", type: XMarkIcon, selector: "XMarkIcon, xmark-icon, x-mark-icon", inputs: ["class"] }, { kind: "pipe", type: SyslangPipe, name: "syslang" }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
8927
- }
8928
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AggregationListComponent, decorators: [{
8929
- type: Component,
8930
- args: [{ selector: "AggregationList, aggregation-list, aggregationlist", imports: [
8931
- FormsModule,
8932
- ReactiveFormsModule,
8933
- ButtonComponent,
8934
- AggregationListItemComponent,
8935
- SyslangPipe,
8936
- TranslocoPipe,
8937
- BadgeComponent,
8938
- ChevronRightIcon,
8939
- InputGroupInput,
8940
- InputGroupComponent,
8941
- InputGroupAddonComponent,
8942
- SearchIcon,
8943
- FilterIcon,
8944
- AggregationListItemComponent,
8945
- FaIconComponent,
8946
- TriangleAlertIcon,
8947
- FilterIcon,
8948
- FilterXIcon,
8949
- SquareCheckIcon,
8950
- SquareIcon,
8951
- IconButtonComponent,
8952
- XMarkIcon
8953
- ], standalone: true, host: {
8954
- "[class]": 'cn("block h-[inherit] max-h-[inherit]",class())'
8955
- }, template: "@if (aggregation()?.isTree) {\r\n <div class=\"p-2 text-sm text-red-500\">\r\n <triangle-alert-icon class=\"mr-1\" />\r\n The aggregation component no longer supports tree aggregations. Please use\r\n the &lt;AggregationTree /&gt; component instead.\r\n </div>\r\n}\r\n<details\r\n [attr.open]=\"expanded()\"\r\n [attr.name]=\"id()\"\r\n class=\"group space-y-2\"\r\n (toggle)=\"onToggle($event)\">\r\n <summary\r\n [class.cursor-pointer]=\"collapsible() && !isEmpty()\"\r\n [class.text-muted-foreground]=\"isEmpty()\"\r\n class=\"m-0 mt-1 flex h-8 w-full items-center gap-1 pl-1 font-semibold select-none\"\r\n (click)=\"onHeaderClick($event)\">\r\n <ng-content select=\"label\">\r\n @let icon = aggregation()?.icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\r\n }\r\n <span class=\"grow truncate\">{{\r\n aggregation()?.display | syslang | transloco\r\n }}</span>\r\n </ng-content>\r\n\r\n @if (showFiltersCount() && filtersCount() > 0) {\r\n <!-- count -->\r\n <Badge size=\"xs\" class=\"ml-1\">\r\n {{ filtersCount() }}\r\n </Badge>\r\n }\r\n <!-- apply filter block -->\r\n @if (!isCollapsed()) {\r\n @if (hasFilters()) {\r\n @let label = \"filters.clearFilters\" | transloco;\r\n <button\r\n variant=\"none\"\r\n icon-button\r\n [aria-label]=\"label\"\r\n (click)=\"$event.stopPropagation(); clear()\">\r\n <filter-x-icon />\r\n </button>\r\n }\r\n @if (selection()) {\r\n @let label = \"filters.apply\" | transloco;\r\n <button\r\n variant=\"accent\"\r\n size=\"sm\"\r\n [aria-label]=\"label\"\r\n (click)=\"$event.stopPropagation(); apply()\">\r\n <FilterIcon />\r\n {{ label }}\r\n </button>\r\n }\r\n\r\n <!-- select / unselect all -->\r\n @if (isAllSelected()) {\r\n @let label = \"filters.unselectAllFilters\" | transloco;\r\n <button\r\n variant=\"none\"\r\n icon-button\r\n [aria-label]=\"label\"\r\n (click)=\"$event.stopPropagation(); unselectAll()\">\r\n <square-check-icon />\r\n </button>\r\n } @else {\r\n @let label = \"filters.selectAllFilters\" | transloco;\r\n <button\r\n variant=\"none\"\r\n icon-button\r\n [aria-label]=\"label\"\r\n (click)=\"$event.stopPropagation(); selectAll()\">\r\n <square-icon />\r\n </button>\r\n }\r\n }\r\n\r\n @if (collapsible()) {\r\n <icon-button\r\n title=\"Open/Close\"\r\n class=\"cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\r\n <chevronright />\r\n <span class=\"sr-only\">{{ \"filters.toggle\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n </summary>\r\n\r\n <!-- content wrapper -->\r\n @if (aggregation()?.searchable && items().length) {\r\n <InputGroup class=\"group/item mt-1\">\r\n <input\r\n #searchInput\r\n input-group\r\n id=\"aggregation-input-{{ column() }}\"\r\n type=\"text\"\r\n [attr.placeholder]=\"'search' | transloco\"\r\n [(ngModel)]=\"searchText\"\r\n class=\"mt-1\" />\r\n <InputGroupAddon>\r\n <SearchIcon\r\n class=\"text-foreground size-4 rotate-0 transition-[rotate] duration-500 group-focus-within/item:rotate-90\" />\r\n </InputGroupAddon>\r\n <InputGroupAddon align=\"inline-end\" class=\"gap-0.5!\">\r\n <icon-button\r\n size=\"sm\"\r\n [class]=\"\r\n searchText().length > 0\r\n ? 'rotate-90 cursor-pointer opacity-100 transition-[rotate,opacity] duration-500'\r\n : 'pointer-events-none rotate-0 opacity-0 transition-[rotate,opacity] duration-500'\r\n \"\r\n aria-label=\"Clear search\"\r\n [tabindex]=\"searchText().length > 0 ? 0 : -1\"\r\n (keydown.enter)=\"clearSearch($event)\"\r\n (click)=\"clearSearch($event)\">\r\n <XMarkIcon />\r\n </icon-button>\r\n <ng-content />\r\n </InputGroupAddon>\r\n </InputGroup>\r\n }\r\n\r\n <div\r\n #scrollElement\r\n class=\"scrollbar-thin max-h-[calc(var(--height,100%)-100px)] w-full overflow-auto\">\r\n <div\r\n class=\"relative w-full\"\r\n [style.height]=\"virtualizer.getTotalSize() + 'px'\"\r\n role=\"list\"\r\n [attr.aria-label]=\"aggregation()?.display | syslang | transloco\">\r\n @for (vItem of virtualizer.getVirtualItems(); track vItem.index) {\r\n @let item = items()[vItem.index];\r\n <div\r\n class=\"absolute w-full\"\r\n [style.transform]=\"'translateY(' + vItem.start + 'px)'\"\r\n role=\"listitem\">\r\n <AggregationListItem\r\n [node]=\"item\"\r\n [field]=\"aggregation()?.column\"\r\n (onSelect)=\"select()\"\r\n (onFilter)=\"apply()\" />\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n @if (aggregation()?.$hasMore && this.searchedItems().length === 0) {\r\n <button\r\n class=\"mt-1 flex w-full justify-center\"\r\n [attr.aria-label]=\"'loadMore' | transloco\"\r\n (click)=\"loadMore()\">\r\n {{ \"loadMore\" | transloco }}\r\n </button>\r\n }\r\n</details>\r\n", styles: ["AggregationItem:has(+AggregationItem){margin-bottom:var(--agg-item-gap, 0)}\n"] }]
8956
- }], ctorParameters: () => [], propDecorators: { scrollElement: [{ type: i0.ViewChild, args: ["scrollElement", { isSignal: true }] }], searchInput: [{ type: i0.ViewChild, args: ["searchInput", { isSignal: true }] }], class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }], column: [{ type: i0.Input, args: [{ isSignal: true, alias: "column", required: true }] }], onSelect: [{ type: i0.Output, args: ["onSelect"] }], onApply: [{ type: i0.Output, args: ["onApply"] }], onClear: [{ type: i0.Output, args: ["onClear"] }], collapsible: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsible", required: false }] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], showFiltersCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFiltersCount", required: false }] }], searchText: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchText", required: false }] }, { type: i0.Output, args: ["searchTextChange"] }] } });
8957
-
8958
- const options = {
8959
- year: 'numeric',
8960
- month: '2-digit',
8961
- day: '2-digit'
8962
- };
8963
- class AggregationDateComponent extends AggregationListComponent {
8964
- dateRangeDialog = viewChild(AggregationDateRangeDialogComponent, ...(ngDevMode ? [{ debugName: "dateRangeDialog" }] : []));
8965
- title = input({ label: "Date", icon: "far fa-calendar-day" }, ...(ngDevMode ? [{ debugName: "title" }] : []));
8966
- displayEmptyDistributionIntervals = input(false, ...(ngDevMode ? [{ debugName: "displayEmptyDistributionIntervals" }] : []));
8967
- allowCustomRange = inject(FILTER_DATE_ALLOW_CUSTOM_RANGE);
8968
- transloco = inject(TranslocoService);
8969
- name = input(null, ...(ngDevMode ? [{ debugName: "name" }] : []));
8858
+ isEmpty = computed(() => this.aggregation() === null, ...(ngDevMode ? [{ debugName: "isEmpty" }] : []));
8859
+ items = computed(() => this.aggregation()?.items ?? [], ...(ngDevMode ? [{ debugName: "items" }] : []));
8970
8860
  dateOptions = computed(() => translateAggregationToDateOptions(this.aggregation(), this.displayEmptyDistributionIntervals()), ...(ngDevMode ? [{ debugName: "dateOptions" }] : []));
8971
8861
  form = new FormGroup({
8972
8862
  option: new FormControl(null),
@@ -8977,7 +8867,6 @@ class AggregationDateComponent extends AggregationListComponent {
8977
8867
  });
8978
8868
  today = new Date();
8979
8869
  lang = signal(this.transloco.getActiveLang(), ...(ngDevMode ? [{ debugName: "lang" }] : []));
8980
- validSelection = signal(false, ...(ngDevMode ? [{ debugName: "validSelection" }] : []));
8981
8870
  formValue = toSignal(this.form.valueChanges, { initialValue: this.form.value });
8982
8871
  customRangeFrom = computed(() => {
8983
8872
  const from = this.formValue().customRange?.from;
@@ -8988,7 +8877,6 @@ class AggregationDateComponent extends AggregationListComponent {
8988
8877
  return to ? new Date(to).toLocaleDateString(this.lang()) : "";
8989
8878
  }, ...(ngDevMode ? [{ debugName: "customRangeTo" }] : []));
8990
8879
  constructor() {
8991
- super();
8992
8880
  this.transloco.langChanges$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((lang) => {
8993
8881
  this.lang.set(lang);
8994
8882
  });
@@ -9007,17 +8895,6 @@ class AggregationDateComponent extends AggregationListComponent {
9007
8895
  (changes.option !== "custom-range" || changes.customRange?.from !== null || changes.customRange?.to !== null));
9008
8896
  });
9009
8897
  }
9010
- aggregation = computed(() => {
9011
- const name = this.name();
9012
- if (name !== null) {
9013
- const agg = this.aggregationsService.processAggregation(name, this.column());
9014
- return {
9015
- ...agg,
9016
- items: agg?.items?.filter((item) => item.display !== "custom-range") ?? []
9017
- };
9018
- }
9019
- return null;
9020
- }, ...(ngDevMode ? [{ debugName: "aggregation" }] : []));
9021
8898
  select() {
9022
8899
  this.selection.set(true);
9023
8900
  }
@@ -9049,6 +8926,15 @@ class AggregationDateComponent extends AggregationListComponent {
9049
8926
  this.onClear.emit();
9050
8927
  }
9051
8928
  }
8929
+ onHeaderClick(event) {
8930
+ event.preventDefault();
8931
+ const isDate = this.aggregationsService.appStore.isDateColumn(this.aggregation()?.column || "");
8932
+ if (!isDate && this.isEmpty())
8933
+ return;
8934
+ if (this.collapsible()) {
8935
+ this.isCollapsed.update((value) => !value);
8936
+ }
8937
+ }
9052
8938
  selectAndOpenDialog() {
9053
8939
  this.select();
9054
8940
  this.dateRangeDialog()?.open();
@@ -9174,7 +9060,7 @@ class AggregationDateComponent extends AggregationListComponent {
9174
9060
  throw new Error("filters.filterInvalid");
9175
9061
  }
9176
9062
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AggregationDateComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
9177
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: AggregationDateComponent, isStandalone: true, selector: "aggregation-date, AggregationDate, aggregationdate", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, displayEmptyDistributionIntervals: { classPropertyName: "displayEmptyDistributionIntervals", publicName: "displayEmptyDistributionIntervals", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "@container" }, providers: [provideTranslocoScope("filters")], viewQueries: [{ propertyName: "dateRangeDialog", first: true, predicate: AggregationDateRangeDialogComponent, descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<details [attr.open]=\"expanded()\" [attr.name]=\"id()\" class=\"group space-y-2\">\r\n <summary\r\n [class.cursor-pointer]=\"collapsible()\"\r\n class=\"m-0 flex h-8 w-full items-center pl-1 font-semibold select-none\"\r\n (click)=\"onHeaderClick($event)\">\r\n <ng-content select=\"label\">\r\n @let icon = aggregation()?.icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\r\n }\r\n <span class=\"grow\">{{\r\n aggregation()?.display | syslang | transloco\r\n }}</span>\r\n </ng-content>\r\n\r\n @if (hasFilters()) {\r\n <icon-button\r\n [attr.title]=\"'filters.clearFilters' | transloco\"\r\n [attr.aria-label]=\"'filters.clearFilters' | transloco\"\r\n (click)=\"clear()\"\r\n (keydown.enter)=\"clear()\">\r\n <filter-x-icon />\r\n <span class=\"sr-only\">{{ \"filters.clearFilters\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n\r\n @if (selection() && validSelection()) {\r\n <icon-button\r\n [attr.title]=\"'filters.applyFilters' | transloco\"\r\n [attr.aria-label]=\"'filters.applyFilters' | transloco\"\r\n (click)=\"apply()\"\r\n (keydown.enter)=\"apply()\">\r\n <filter-icon />\r\n <span class=\"sr-only\">{{ \"filters.applyFilters\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n @if (collapsible()) {\r\n <icon-button\r\n title=\"Open/Close\"\r\n class=\"cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\r\n <chevronright />\r\n </icon-button>\r\n }\r\n </summary>\r\n\r\n <!-- content wrapper -->\r\n <form [formGroup]=\"form\">\r\n <ul\r\n class=\"scrollbar-thin flex max-h-[calc(var(--height,100%)-100px)] snap-y snap-start flex-col gap-1 overflow-auto pt-2\"\r\n role=\"list\">\r\n @for (option of dateOptions(); track $index) {\r\n <li\r\n role=\"listitem\"\r\n tabindex=\"0\"\r\n (click)=\"radio.click()\"\r\n [attr.aria-label]=\"option.display | syslang | transloco\"\r\n [class]=\"\r\n cn(\r\n 'flex p-0 px-2 leading-7',\r\n form.get('option')?.value === option.display && 'bg-accent',\r\n option.hidden && 'hidden',\r\n option.disabled && 'disabled pointer-events-none text-neutral-300'\r\n )\r\n \"\r\n [attr.aria-hidden]=\"option.disabled\">\r\n <input\r\n #radio\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-{{ option.display }}\"\r\n [attr.disabled]=\"option.disabled ? true : null\"\r\n [attr.aria-disabled]=\"option.disabled\"\r\n (click)=\"select()\"\r\n value=\"{{ option.display }}\" />\r\n\r\n <label\r\n for=\"date-filter-{{ option.display }}\"\r\n class=\"grow cursor-pointer p-1\">\r\n {{ option.display | syslang | transloco }}\r\n </label>\r\n </li>\r\n }\r\n\r\n @if (allowCustomRange) {\r\n <li\r\n role=\"listitem\"\r\n aria-label=\"open date range picker\"\r\n class=\"flex px-2 leading-7\"\r\n [class.select]=\"form.get('option')?.value === 'custom-range'\">\r\n <input\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-range-dialog\"\r\n value=\"custom-range\"\r\n (click)=\"select()\" />\r\n <div\r\n class=\"@container flex grow justify-end gap-1 p-1 @max-[340px]:flex-wrap\">\r\n <div class=\"flex gap-1\">\r\n <label for=\"datepicker-range-start\" class=\"min-w-10 truncate\">{{\r\n \"filters.from\" | transloco\r\n }}</label>\r\n <input\r\n id=\"datepicker-range-start\"\r\n name=\"start\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeFrom()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n <div class=\"flex gap-1\">\r\n <label\r\n for=\"datepicker-range-end\"\r\n class=\"min-w-10 truncate text-right\"\r\n >{{ \"filters.to\" | transloco }}</label\r\n >\r\n <input\r\n id=\"datepicker-range-end\"\r\n name=\"end\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeTo()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n </div>\r\n </li>\r\n }\r\n </ul>\r\n </form>\r\n</details>\r\n\r\n<aggregation-date-range-dialog\r\n [lang]=\"lang()\"\r\n [useDateRange]=\"false\"\r\n [min]=\"form.get('customRange.from')?.value || undefined\"\r\n [max]=\"form.get('customRange.to')?.value || undefined\"\r\n (rangeSelected)=\"onRangeSelected($event)\" />\r\n", styles: [":host{display:block;min-width:200px}ul[role=list]{scrollbar-width:thin}\n"], dependencies: [{ kind: "directive", type: InputComponent, selector: "input[type=\"text\"], input[type=\"email\"], input[type=\"number\"], input[type=\"password\"], input[type=\"tel\"], input[type=\"url\"], input[type=\"time\"], input[type=\"file\"]", inputs: ["class", "variant", "decoration"] }, { kind: "directive", type: ListItemComponent, selector: "[role=\"listitem\"], [role=\"option\"]", inputs: ["class", "variant", "decoration"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.RadioControlValueAccessor, selector: "input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: ChevronRightIcon, selector: "chevron-right, ChevronRight, chevronright, ChevronRightIcon, chevron-right-icon, chevronrighticon", inputs: ["class"] }, { kind: "component", type: AggregationDateRangeDialogComponent, selector: "aggregation-date-range-dialog", inputs: ["min", "max", "lang", "useDateRange"], outputs: ["rangeSelected"] }, { kind: "component", type: FaIconComponent, selector: "fa-icon, FaIcon", inputs: ["faClass", "class"] }, { kind: "component", type: FilterIcon, selector: "filter-icon, FilterIcon", inputs: ["class"] }, { kind: "component", type: FilterXIcon, selector: "filter-x-icon, FilterXIcon", inputs: ["class"] }, { kind: "directive", type: IconButtonComponent, selector: "button[icon-button], icon-button, IconButton", inputs: ["class", "size"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }, { kind: "pipe", type: SyslangPipe, name: "syslang" }] });
9063
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: AggregationDateComponent, isStandalone: true, selector: "aggregation-date, AggregationDate, aggregationdate", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, column: { classPropertyName: "column", publicName: "column", isSignal: true, isRequired: true, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, collapsible: { classPropertyName: "collapsible", publicName: "collapsible", isSignal: true, isRequired: false, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, searchable: { classPropertyName: "searchable", publicName: "searchable", isSignal: true, isRequired: false, transformFunction: null }, showFiltersCount: { classPropertyName: "showFiltersCount", publicName: "showFiltersCount", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, displayEmptyDistributionIntervals: { classPropertyName: "displayEmptyDistributionIntervals", publicName: "displayEmptyDistributionIntervals", isSignal: true, isRequired: false, transformFunction: null }, searchText: { classPropertyName: "searchText", publicName: "searchText", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSelect: "onSelect", onApply: "onApply", onClear: "onClear", searchText: "searchTextChange" }, host: { classAttribute: "@container" }, providers: [provideTranslocoScope("filters")], viewQueries: [{ propertyName: "dateRangeDialog", first: true, predicate: AggregationDateRangeDialogComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<details [attr.open]=\"expanded()\" [attr.name]=\"id()\" class=\"group space-y-2\">\r\n <summary\r\n [class.cursor-pointer]=\"collapsible()\"\r\n class=\"m-0 flex h-8 w-full items-center pl-1 font-semibold select-none\"\r\n (click)=\"onHeaderClick($event)\">\r\n <ng-content select=\"label\">\r\n @let icon = aggregation()?.icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\r\n }\r\n <span class=\"grow\">{{\r\n aggregation()?.display | syslang | transloco\r\n }}</span>\r\n </ng-content>\r\n\r\n @if (hasFilters()) {\r\n <icon-button\r\n [attr.title]=\"'filters.clearFilters' | transloco\"\r\n [attr.aria-label]=\"'filters.clearFilters' | transloco\"\r\n (click)=\"clear()\"\r\n (keydown.enter)=\"clear()\">\r\n <filter-x-icon />\r\n <span class=\"sr-only\">{{ \"filters.clearFilters\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n\r\n @if (selection() && validSelection()) {\r\n <icon-button\r\n [attr.title]=\"'filters.applyFilters' | transloco\"\r\n [attr.aria-label]=\"'filters.applyFilters' | transloco\"\r\n (click)=\"apply()\"\r\n (keydown.enter)=\"apply()\">\r\n <filter-icon />\r\n <span class=\"sr-only\">{{ \"filters.applyFilters\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n @if (collapsible()) {\r\n <icon-button\r\n title=\"Open/Close\"\r\n class=\"cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\r\n <chevronright />\r\n </icon-button>\r\n }\r\n </summary>\r\n\r\n <!-- content wrapper -->\r\n <form [formGroup]=\"form\">\r\n <ul\r\n class=\"scrollbar-thin flex max-h-[var(--scroll-height,20rem)] snap-y snap-start flex-col gap-1 overflow-auto pt-2\"\r\n role=\"list\">\r\n @for (option of dateOptions(); track $index) {\r\n <li\r\n role=\"listitem\"\r\n tabindex=\"0\"\r\n (click)=\"radio.click()\"\r\n [attr.aria-label]=\"option.display | syslang | transloco\"\r\n [class]=\"\r\n cn(\r\n 'flex p-0 px-2 leading-7',\r\n form.get('option')?.value === option.display && 'bg-accent',\r\n option.hidden && 'hidden',\r\n option.disabled && 'disabled pointer-events-none text-neutral-300'\r\n )\r\n \"\r\n [attr.aria-hidden]=\"option.disabled\">\r\n <input\r\n #radio\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-{{ option.display }}\"\r\n [attr.disabled]=\"option.disabled ? true : null\"\r\n [attr.aria-disabled]=\"option.disabled\"\r\n (click)=\"select()\"\r\n value=\"{{ option.display }}\" />\r\n\r\n <label\r\n for=\"date-filter-{{ option.display }}\"\r\n class=\"grow cursor-pointer p-1\">\r\n {{ option.display | syslang | transloco }}\r\n </label>\r\n </li>\r\n }\r\n\r\n @if (allowCustomRange) {\r\n <li\r\n role=\"listitem\"\r\n aria-label=\"open date range picker\"\r\n class=\"flex px-2 leading-7\"\r\n [class.select]=\"form.get('option')?.value === 'custom-range'\">\r\n <input\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-range-dialog\"\r\n value=\"custom-range\"\r\n (click)=\"select()\" />\r\n <div\r\n class=\"@container flex grow justify-end gap-1 p-1 @max-[340px]:flex-wrap\">\r\n <div class=\"flex gap-1\">\r\n <label for=\"datepicker-range-start\" class=\"min-w-10 truncate\">{{\r\n \"filters.from\" | transloco\r\n }}</label>\r\n <input\r\n id=\"datepicker-range-start\"\r\n name=\"start\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeFrom()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n <div class=\"flex gap-1\">\r\n <label\r\n for=\"datepicker-range-end\"\r\n class=\"min-w-10 truncate text-right\"\r\n >{{ \"filters.to\" | transloco }}</label\r\n >\r\n <input\r\n id=\"datepicker-range-end\"\r\n name=\"end\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeTo()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n </div>\r\n </li>\r\n }\r\n </ul>\r\n </form>\r\n</details>\r\n\r\n<aggregation-date-range-dialog\r\n [lang]=\"lang()\"\r\n [useDateRange]=\"false\"\r\n [min]=\"form.get('customRange.from')?.value || undefined\"\r\n [max]=\"form.get('customRange.to')?.value || undefined\"\r\n (rangeSelected)=\"onRangeSelected($event)\" />\r\n", styles: [":host{display:block;min-width:200px}ul[role=list]{scrollbar-width:thin}\n"], dependencies: [{ kind: "directive", type: InputComponent, selector: "input[type=\"text\"], input[type=\"email\"], input[type=\"number\"], input[type=\"password\"], input[type=\"tel\"], input[type=\"url\"], input[type=\"time\"], input[type=\"file\"]", inputs: ["class", "variant", "decoration"] }, { kind: "directive", type: ListItemComponent, selector: "[role=\"listitem\"], [role=\"option\"]", inputs: ["class", "variant", "decoration"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.RadioControlValueAccessor, selector: "input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: ChevronRightIcon, selector: "chevron-right, ChevronRight, chevronright, ChevronRightIcon, chevron-right-icon, chevronrighticon", inputs: ["class"] }, { kind: "component", type: AggregationDateRangeDialogComponent, selector: "aggregation-date-range-dialog", inputs: ["min", "max", "lang", "useDateRange"], outputs: ["rangeSelected"] }, { kind: "component", type: FaIconComponent, selector: "fa-icon, FaIcon", inputs: ["faClass", "class"] }, { kind: "component", type: FilterIcon, selector: "filter-icon, FilterIcon", inputs: ["class"] }, { kind: "component", type: FilterXIcon, selector: "filter-x-icon, FilterXIcon", inputs: ["class"] }, { kind: "directive", type: IconButtonComponent, selector: "button[icon-button], icon-button, IconButton", inputs: ["class", "size"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }, { kind: "pipe", type: SyslangPipe, name: "syslang" }] });
9178
9064
  }
9179
9065
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AggregationDateComponent, decorators: [{
9180
9066
  type: Component,
@@ -9192,8 +9078,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
9192
9078
  IconButtonComponent
9193
9079
  ], host: {
9194
9080
  class: "@container"
9195
- }, template: "<details [attr.open]=\"expanded()\" [attr.name]=\"id()\" class=\"group space-y-2\">\r\n <summary\r\n [class.cursor-pointer]=\"collapsible()\"\r\n class=\"m-0 flex h-8 w-full items-center pl-1 font-semibold select-none\"\r\n (click)=\"onHeaderClick($event)\">\r\n <ng-content select=\"label\">\r\n @let icon = aggregation()?.icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\r\n }\r\n <span class=\"grow\">{{\r\n aggregation()?.display | syslang | transloco\r\n }}</span>\r\n </ng-content>\r\n\r\n @if (hasFilters()) {\r\n <icon-button\r\n [attr.title]=\"'filters.clearFilters' | transloco\"\r\n [attr.aria-label]=\"'filters.clearFilters' | transloco\"\r\n (click)=\"clear()\"\r\n (keydown.enter)=\"clear()\">\r\n <filter-x-icon />\r\n <span class=\"sr-only\">{{ \"filters.clearFilters\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n\r\n @if (selection() && validSelection()) {\r\n <icon-button\r\n [attr.title]=\"'filters.applyFilters' | transloco\"\r\n [attr.aria-label]=\"'filters.applyFilters' | transloco\"\r\n (click)=\"apply()\"\r\n (keydown.enter)=\"apply()\">\r\n <filter-icon />\r\n <span class=\"sr-only\">{{ \"filters.applyFilters\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n @if (collapsible()) {\r\n <icon-button\r\n title=\"Open/Close\"\r\n class=\"cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\r\n <chevronright />\r\n </icon-button>\r\n }\r\n </summary>\r\n\r\n <!-- content wrapper -->\r\n <form [formGroup]=\"form\">\r\n <ul\r\n class=\"scrollbar-thin flex max-h-[calc(var(--height,100%)-100px)] snap-y snap-start flex-col gap-1 overflow-auto pt-2\"\r\n role=\"list\">\r\n @for (option of dateOptions(); track $index) {\r\n <li\r\n role=\"listitem\"\r\n tabindex=\"0\"\r\n (click)=\"radio.click()\"\r\n [attr.aria-label]=\"option.display | syslang | transloco\"\r\n [class]=\"\r\n cn(\r\n 'flex p-0 px-2 leading-7',\r\n form.get('option')?.value === option.display && 'bg-accent',\r\n option.hidden && 'hidden',\r\n option.disabled && 'disabled pointer-events-none text-neutral-300'\r\n )\r\n \"\r\n [attr.aria-hidden]=\"option.disabled\">\r\n <input\r\n #radio\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-{{ option.display }}\"\r\n [attr.disabled]=\"option.disabled ? true : null\"\r\n [attr.aria-disabled]=\"option.disabled\"\r\n (click)=\"select()\"\r\n value=\"{{ option.display }}\" />\r\n\r\n <label\r\n for=\"date-filter-{{ option.display }}\"\r\n class=\"grow cursor-pointer p-1\">\r\n {{ option.display | syslang | transloco }}\r\n </label>\r\n </li>\r\n }\r\n\r\n @if (allowCustomRange) {\r\n <li\r\n role=\"listitem\"\r\n aria-label=\"open date range picker\"\r\n class=\"flex px-2 leading-7\"\r\n [class.select]=\"form.get('option')?.value === 'custom-range'\">\r\n <input\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-range-dialog\"\r\n value=\"custom-range\"\r\n (click)=\"select()\" />\r\n <div\r\n class=\"@container flex grow justify-end gap-1 p-1 @max-[340px]:flex-wrap\">\r\n <div class=\"flex gap-1\">\r\n <label for=\"datepicker-range-start\" class=\"min-w-10 truncate\">{{\r\n \"filters.from\" | transloco\r\n }}</label>\r\n <input\r\n id=\"datepicker-range-start\"\r\n name=\"start\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeFrom()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n <div class=\"flex gap-1\">\r\n <label\r\n for=\"datepicker-range-end\"\r\n class=\"min-w-10 truncate text-right\"\r\n >{{ \"filters.to\" | transloco }}</label\r\n >\r\n <input\r\n id=\"datepicker-range-end\"\r\n name=\"end\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeTo()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n </div>\r\n </li>\r\n }\r\n </ul>\r\n </form>\r\n</details>\r\n\r\n<aggregation-date-range-dialog\r\n [lang]=\"lang()\"\r\n [useDateRange]=\"false\"\r\n [min]=\"form.get('customRange.from')?.value || undefined\"\r\n [max]=\"form.get('customRange.to')?.value || undefined\"\r\n (rangeSelected)=\"onRangeSelected($event)\" />\r\n", styles: [":host{display:block;min-width:200px}ul[role=list]{scrollbar-width:thin}\n"] }]
9196
- }], ctorParameters: () => [], propDecorators: { dateRangeDialog: [{ type: i0.ViewChild, args: [i0.forwardRef(() => AggregationDateRangeDialogComponent), { isSignal: true }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], displayEmptyDistributionIntervals: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayEmptyDistributionIntervals", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }] } });
9081
+ }, template: "<details [attr.open]=\"expanded()\" [attr.name]=\"id()\" class=\"group space-y-2\">\r\n <summary\r\n [class.cursor-pointer]=\"collapsible()\"\r\n class=\"m-0 flex h-8 w-full items-center pl-1 font-semibold select-none\"\r\n (click)=\"onHeaderClick($event)\">\r\n <ng-content select=\"label\">\r\n @let icon = aggregation()?.icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\r\n }\r\n <span class=\"grow\">{{\r\n aggregation()?.display | syslang | transloco\r\n }}</span>\r\n </ng-content>\r\n\r\n @if (hasFilters()) {\r\n <icon-button\r\n [attr.title]=\"'filters.clearFilters' | transloco\"\r\n [attr.aria-label]=\"'filters.clearFilters' | transloco\"\r\n (click)=\"clear()\"\r\n (keydown.enter)=\"clear()\">\r\n <filter-x-icon />\r\n <span class=\"sr-only\">{{ \"filters.clearFilters\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n\r\n @if (selection() && validSelection()) {\r\n <icon-button\r\n [attr.title]=\"'filters.applyFilters' | transloco\"\r\n [attr.aria-label]=\"'filters.applyFilters' | transloco\"\r\n (click)=\"apply()\"\r\n (keydown.enter)=\"apply()\">\r\n <filter-icon />\r\n <span class=\"sr-only\">{{ \"filters.applyFilters\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n @if (collapsible()) {\r\n <icon-button\r\n title=\"Open/Close\"\r\n class=\"cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\r\n <chevronright />\r\n </icon-button>\r\n }\r\n </summary>\r\n\r\n <!-- content wrapper -->\r\n <form [formGroup]=\"form\">\r\n <ul\r\n class=\"scrollbar-thin flex max-h-[var(--scroll-height,20rem)] snap-y snap-start flex-col gap-1 overflow-auto pt-2\"\r\n role=\"list\">\r\n @for (option of dateOptions(); track $index) {\r\n <li\r\n role=\"listitem\"\r\n tabindex=\"0\"\r\n (click)=\"radio.click()\"\r\n [attr.aria-label]=\"option.display | syslang | transloco\"\r\n [class]=\"\r\n cn(\r\n 'flex p-0 px-2 leading-7',\r\n form.get('option')?.value === option.display && 'bg-accent',\r\n option.hidden && 'hidden',\r\n option.disabled && 'disabled pointer-events-none text-neutral-300'\r\n )\r\n \"\r\n [attr.aria-hidden]=\"option.disabled\">\r\n <input\r\n #radio\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-{{ option.display }}\"\r\n [attr.disabled]=\"option.disabled ? true : null\"\r\n [attr.aria-disabled]=\"option.disabled\"\r\n (click)=\"select()\"\r\n value=\"{{ option.display }}\" />\r\n\r\n <label\r\n for=\"date-filter-{{ option.display }}\"\r\n class=\"grow cursor-pointer p-1\">\r\n {{ option.display | syslang | transloco }}\r\n </label>\r\n </li>\r\n }\r\n\r\n @if (allowCustomRange) {\r\n <li\r\n role=\"listitem\"\r\n aria-label=\"open date range picker\"\r\n class=\"flex px-2 leading-7\"\r\n [class.select]=\"form.get('option')?.value === 'custom-range'\">\r\n <input\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-range-dialog\"\r\n value=\"custom-range\"\r\n (click)=\"select()\" />\r\n <div\r\n class=\"@container flex grow justify-end gap-1 p-1 @max-[340px]:flex-wrap\">\r\n <div class=\"flex gap-1\">\r\n <label for=\"datepicker-range-start\" class=\"min-w-10 truncate\">{{\r\n \"filters.from\" | transloco\r\n }}</label>\r\n <input\r\n id=\"datepicker-range-start\"\r\n name=\"start\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeFrom()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n <div class=\"flex gap-1\">\r\n <label\r\n for=\"datepicker-range-end\"\r\n class=\"min-w-10 truncate text-right\"\r\n >{{ \"filters.to\" | transloco }}</label\r\n >\r\n <input\r\n id=\"datepicker-range-end\"\r\n name=\"end\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeTo()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n </div>\r\n </li>\r\n }\r\n </ul>\r\n </form>\r\n</details>\r\n\r\n<aggregation-date-range-dialog\r\n [lang]=\"lang()\"\r\n [useDateRange]=\"false\"\r\n [min]=\"form.get('customRange.from')?.value || undefined\"\r\n [max]=\"form.get('customRange.to')?.value || undefined\"\r\n (rangeSelected)=\"onRangeSelected($event)\" />\r\n", styles: [":host{display:block;min-width:200px}ul[role=list]{scrollbar-width:thin}\n"] }]
9082
+ }], ctorParameters: () => [], propDecorators: { dateRangeDialog: [{ type: i0.ViewChild, args: [i0.forwardRef(() => AggregationDateRangeDialogComponent), { isSignal: true }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], column: [{ type: i0.Input, args: [{ isSignal: true, alias: "column", required: true }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], collapsible: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsible", required: false }] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], showFiltersCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFiltersCount", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], displayEmptyDistributionIntervals: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayEmptyDistributionIntervals", required: false }] }], onSelect: [{ type: i0.Output, args: ["onSelect"] }], onApply: [{ type: i0.Output, args: ["onApply"] }], onClear: [{ type: i0.Output, args: ["onClear"] }], searchText: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchText", required: false }] }, { type: i0.Output, args: ["searchTextChange"] }] } });
9197
9083
 
9198
9084
  /**
9199
9085
  * Component that allows users to select a date or a date range for filtering search results.
@@ -9202,7 +9088,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
9202
9088
  */
9203
9089
  class DateComponent extends AggregationDateComponent {
9204
9090
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: DateComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
9205
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: DateComponent, isStandalone: true, selector: "date-filter,DateFilter", host: { classAttribute: "@container" }, providers: [provideTranslocoScope("filters")], usesInheritance: true, ngImport: i0, template: "<details [attr.open]=\"expanded()\" [attr.name]=\"id()\" class=\"group space-y-2\">\r\n <summary\r\n [class.cursor-pointer]=\"collapsible()\"\r\n class=\"m-0 flex h-8 w-full items-center pl-1 font-semibold select-none\"\r\n (click)=\"onHeaderClick($event)\">\r\n <ng-content select=\"label\">\r\n @let icon = aggregation()?.icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\r\n }\r\n <span class=\"grow\">{{\r\n aggregation()?.display | syslang | transloco\r\n }}</span>\r\n </ng-content>\r\n\r\n @if (hasFilters()) {\r\n <icon-button\r\n [attr.title]=\"'filters.clearFilters' | transloco\"\r\n [attr.aria-label]=\"'filters.clearFilters' | transloco\"\r\n (click)=\"clear()\"\r\n (keydown.enter)=\"clear()\">\r\n <filter-x-icon />\r\n <span class=\"sr-only\">{{ \"filters.clearFilters\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n\r\n @if (selection() && validSelection()) {\r\n <icon-button\r\n [attr.title]=\"'filters.applyFilters' | transloco\"\r\n [attr.aria-label]=\"'filters.applyFilters' | transloco\"\r\n (click)=\"apply()\"\r\n (keydown.enter)=\"apply()\">\r\n <filter-icon />\r\n <span class=\"sr-only\">{{ \"filters.applyFilters\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n @if (collapsible()) {\r\n <icon-button\r\n title=\"Open/Close\"\r\n class=\"cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\r\n <chevronright />\r\n </icon-button>\r\n }\r\n </summary>\r\n\r\n <!-- content wrapper -->\r\n <form [formGroup]=\"form\">\r\n <ul\r\n class=\"scrollbar-thin flex max-h-[calc(var(--height,100%)-100px)] snap-y snap-start flex-col gap-1 overflow-auto pt-2\"\r\n role=\"list\">\r\n @for (option of dateOptions(); track $index) {\r\n <li\r\n role=\"listitem\"\r\n tabindex=\"0\"\r\n (click)=\"radio.click()\"\r\n [attr.aria-label]=\"option.display | syslang | transloco\"\r\n [class]=\"\r\n cn(\r\n 'flex p-0 px-2 leading-7',\r\n form.get('option')?.value === option.display && 'bg-accent',\r\n option.hidden && 'hidden',\r\n option.disabled && 'disabled pointer-events-none text-neutral-300'\r\n )\r\n \"\r\n [attr.aria-hidden]=\"option.disabled\">\r\n <input\r\n #radio\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-{{ option.display }}\"\r\n [attr.disabled]=\"option.disabled ? true : null\"\r\n [attr.aria-disabled]=\"option.disabled\"\r\n (click)=\"select()\"\r\n value=\"{{ option.display }}\" />\r\n\r\n <label\r\n for=\"date-filter-{{ option.display }}\"\r\n class=\"grow cursor-pointer p-1\">\r\n {{ option.display | syslang | transloco }}\r\n </label>\r\n </li>\r\n }\r\n\r\n @if (allowCustomRange) {\r\n <li\r\n role=\"listitem\"\r\n aria-label=\"open date range picker\"\r\n class=\"flex px-2 leading-7\"\r\n [class.select]=\"form.get('option')?.value === 'custom-range'\">\r\n <input\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-range-dialog\"\r\n value=\"custom-range\"\r\n (click)=\"select()\" />\r\n <div\r\n class=\"@container flex grow justify-end gap-1 p-1 @max-[340px]:flex-wrap\">\r\n <div class=\"flex gap-1\">\r\n <label for=\"datepicker-range-start\" class=\"min-w-10 truncate\">{{\r\n \"filters.from\" | transloco\r\n }}</label>\r\n <input\r\n id=\"datepicker-range-start\"\r\n name=\"start\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeFrom()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n <div class=\"flex gap-1\">\r\n <label\r\n for=\"datepicker-range-end\"\r\n class=\"min-w-10 truncate text-right\"\r\n >{{ \"filters.to\" | transloco }}</label\r\n >\r\n <input\r\n id=\"datepicker-range-end\"\r\n name=\"end\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeTo()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n </div>\r\n </li>\r\n }\r\n </ul>\r\n </form>\r\n</details>\r\n\r\n<aggregation-date-range-dialog\r\n [lang]=\"lang()\"\r\n [useDateRange]=\"false\"\r\n [min]=\"form.get('customRange.from')?.value || undefined\"\r\n [max]=\"form.get('customRange.to')?.value || undefined\"\r\n (rangeSelected)=\"onRangeSelected($event)\" />\r\n", styles: [":host{display:block;min-width:200px}ul[role=list]{scrollbar-width:thin}\n"], dependencies: [{ kind: "directive", type: IconButtonComponent, selector: "button[icon-button], icon-button, IconButton", inputs: ["class", "size"] }, { kind: "directive", type: ListItemComponent, selector: "[role=\"listitem\"], [role=\"option\"]", inputs: ["class", "variant", "decoration"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.RadioControlValueAccessor, selector: "input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: ChevronRightIcon, selector: "chevron-right, ChevronRight, chevronright, ChevronRightIcon, chevron-right-icon, chevronrighticon", inputs: ["class"] }, { kind: "component", type: AggregationDateRangeDialogComponent, selector: "aggregation-date-range-dialog", inputs: ["min", "max", "lang", "useDateRange"], outputs: ["rangeSelected"] }, { kind: "component", type: FaIconComponent, selector: "fa-icon, FaIcon", inputs: ["faClass", "class"] }, { kind: "component", type: FilterIcon, selector: "filter-icon, FilterIcon", inputs: ["class"] }, { kind: "component", type: FilterXIcon, selector: "filter-x-icon, FilterXIcon", inputs: ["class"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }, { kind: "pipe", type: SyslangPipe, name: "syslang" }] });
9091
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: DateComponent, isStandalone: true, selector: "date-filter,DateFilter", host: { classAttribute: "@container" }, providers: [provideTranslocoScope("filters")], usesInheritance: true, ngImport: i0, template: "<details [attr.open]=\"expanded()\" [attr.name]=\"id()\" class=\"group space-y-2\">\r\n <summary\r\n [class.cursor-pointer]=\"collapsible()\"\r\n class=\"m-0 flex h-8 w-full items-center pl-1 font-semibold select-none\"\r\n (click)=\"onHeaderClick($event)\">\r\n <ng-content select=\"label\">\r\n @let icon = aggregation()?.icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\r\n }\r\n <span class=\"grow\">{{\r\n aggregation()?.display | syslang | transloco\r\n }}</span>\r\n </ng-content>\r\n\r\n @if (hasFilters()) {\r\n <icon-button\r\n [attr.title]=\"'filters.clearFilters' | transloco\"\r\n [attr.aria-label]=\"'filters.clearFilters' | transloco\"\r\n (click)=\"clear()\"\r\n (keydown.enter)=\"clear()\">\r\n <filter-x-icon />\r\n <span class=\"sr-only\">{{ \"filters.clearFilters\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n\r\n @if (selection() && validSelection()) {\r\n <icon-button\r\n [attr.title]=\"'filters.applyFilters' | transloco\"\r\n [attr.aria-label]=\"'filters.applyFilters' | transloco\"\r\n (click)=\"apply()\"\r\n (keydown.enter)=\"apply()\">\r\n <filter-icon />\r\n <span class=\"sr-only\">{{ \"filters.applyFilters\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n @if (collapsible()) {\r\n <icon-button\r\n title=\"Open/Close\"\r\n class=\"cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\r\n <chevronright />\r\n </icon-button>\r\n }\r\n </summary>\r\n\r\n <!-- content wrapper -->\r\n <form [formGroup]=\"form\">\r\n <ul\r\n class=\"scrollbar-thin flex max-h-[var(--scroll-height,20rem)] snap-y snap-start flex-col gap-1 overflow-auto pt-2\"\r\n role=\"list\">\r\n @for (option of dateOptions(); track $index) {\r\n <li\r\n role=\"listitem\"\r\n tabindex=\"0\"\r\n (click)=\"radio.click()\"\r\n [attr.aria-label]=\"option.display | syslang | transloco\"\r\n [class]=\"\r\n cn(\r\n 'flex p-0 px-2 leading-7',\r\n form.get('option')?.value === option.display && 'bg-accent',\r\n option.hidden && 'hidden',\r\n option.disabled && 'disabled pointer-events-none text-neutral-300'\r\n )\r\n \"\r\n [attr.aria-hidden]=\"option.disabled\">\r\n <input\r\n #radio\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-{{ option.display }}\"\r\n [attr.disabled]=\"option.disabled ? true : null\"\r\n [attr.aria-disabled]=\"option.disabled\"\r\n (click)=\"select()\"\r\n value=\"{{ option.display }}\" />\r\n\r\n <label\r\n for=\"date-filter-{{ option.display }}\"\r\n class=\"grow cursor-pointer p-1\">\r\n {{ option.display | syslang | transloco }}\r\n </label>\r\n </li>\r\n }\r\n\r\n @if (allowCustomRange) {\r\n <li\r\n role=\"listitem\"\r\n aria-label=\"open date range picker\"\r\n class=\"flex px-2 leading-7\"\r\n [class.select]=\"form.get('option')?.value === 'custom-range'\">\r\n <input\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-range-dialog\"\r\n value=\"custom-range\"\r\n (click)=\"select()\" />\r\n <div\r\n class=\"@container flex grow justify-end gap-1 p-1 @max-[340px]:flex-wrap\">\r\n <div class=\"flex gap-1\">\r\n <label for=\"datepicker-range-start\" class=\"min-w-10 truncate\">{{\r\n \"filters.from\" | transloco\r\n }}</label>\r\n <input\r\n id=\"datepicker-range-start\"\r\n name=\"start\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeFrom()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n <div class=\"flex gap-1\">\r\n <label\r\n for=\"datepicker-range-end\"\r\n class=\"min-w-10 truncate text-right\"\r\n >{{ \"filters.to\" | transloco }}</label\r\n >\r\n <input\r\n id=\"datepicker-range-end\"\r\n name=\"end\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeTo()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n </div>\r\n </li>\r\n }\r\n </ul>\r\n </form>\r\n</details>\r\n\r\n<aggregation-date-range-dialog\r\n [lang]=\"lang()\"\r\n [useDateRange]=\"false\"\r\n [min]=\"form.get('customRange.from')?.value || undefined\"\r\n [max]=\"form.get('customRange.to')?.value || undefined\"\r\n (rangeSelected)=\"onRangeSelected($event)\" />\r\n", styles: [":host{display:block;min-width:200px}ul[role=list]{scrollbar-width:thin}\n"], dependencies: [{ kind: "directive", type: IconButtonComponent, selector: "button[icon-button], icon-button, IconButton", inputs: ["class", "size"] }, { kind: "directive", type: ListItemComponent, selector: "[role=\"listitem\"], [role=\"option\"]", inputs: ["class", "variant", "decoration"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.RadioControlValueAccessor, selector: "input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: ChevronRightIcon, selector: "chevron-right, ChevronRight, chevronright, ChevronRightIcon, chevron-right-icon, chevronrighticon", inputs: ["class"] }, { kind: "component", type: AggregationDateRangeDialogComponent, selector: "aggregation-date-range-dialog", inputs: ["min", "max", "lang", "useDateRange"], outputs: ["rangeSelected"] }, { kind: "component", type: FaIconComponent, selector: "fa-icon, FaIcon", inputs: ["faClass", "class"] }, { kind: "component", type: FilterIcon, selector: "filter-icon, FilterIcon", inputs: ["class"] }, { kind: "component", type: FilterXIcon, selector: "filter-x-icon, FilterXIcon", inputs: ["class"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }, { kind: "pipe", type: SyslangPipe, name: "syslang" }] });
9206
9092
  }
9207
9093
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: DateComponent, decorators: [{
9208
9094
  type: Component,
@@ -9219,7 +9105,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
9219
9105
  FilterXIcon
9220
9106
  ], host: {
9221
9107
  class: "@container"
9222
- }, template: "<details [attr.open]=\"expanded()\" [attr.name]=\"id()\" class=\"group space-y-2\">\r\n <summary\r\n [class.cursor-pointer]=\"collapsible()\"\r\n class=\"m-0 flex h-8 w-full items-center pl-1 font-semibold select-none\"\r\n (click)=\"onHeaderClick($event)\">\r\n <ng-content select=\"label\">\r\n @let icon = aggregation()?.icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\r\n }\r\n <span class=\"grow\">{{\r\n aggregation()?.display | syslang | transloco\r\n }}</span>\r\n </ng-content>\r\n\r\n @if (hasFilters()) {\r\n <icon-button\r\n [attr.title]=\"'filters.clearFilters' | transloco\"\r\n [attr.aria-label]=\"'filters.clearFilters' | transloco\"\r\n (click)=\"clear()\"\r\n (keydown.enter)=\"clear()\">\r\n <filter-x-icon />\r\n <span class=\"sr-only\">{{ \"filters.clearFilters\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n\r\n @if (selection() && validSelection()) {\r\n <icon-button\r\n [attr.title]=\"'filters.applyFilters' | transloco\"\r\n [attr.aria-label]=\"'filters.applyFilters' | transloco\"\r\n (click)=\"apply()\"\r\n (keydown.enter)=\"apply()\">\r\n <filter-icon />\r\n <span class=\"sr-only\">{{ \"filters.applyFilters\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n @if (collapsible()) {\r\n <icon-button\r\n title=\"Open/Close\"\r\n class=\"cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\r\n <chevronright />\r\n </icon-button>\r\n }\r\n </summary>\r\n\r\n <!-- content wrapper -->\r\n <form [formGroup]=\"form\">\r\n <ul\r\n class=\"scrollbar-thin flex max-h-[calc(var(--height,100%)-100px)] snap-y snap-start flex-col gap-1 overflow-auto pt-2\"\r\n role=\"list\">\r\n @for (option of dateOptions(); track $index) {\r\n <li\r\n role=\"listitem\"\r\n tabindex=\"0\"\r\n (click)=\"radio.click()\"\r\n [attr.aria-label]=\"option.display | syslang | transloco\"\r\n [class]=\"\r\n cn(\r\n 'flex p-0 px-2 leading-7',\r\n form.get('option')?.value === option.display && 'bg-accent',\r\n option.hidden && 'hidden',\r\n option.disabled && 'disabled pointer-events-none text-neutral-300'\r\n )\r\n \"\r\n [attr.aria-hidden]=\"option.disabled\">\r\n <input\r\n #radio\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-{{ option.display }}\"\r\n [attr.disabled]=\"option.disabled ? true : null\"\r\n [attr.aria-disabled]=\"option.disabled\"\r\n (click)=\"select()\"\r\n value=\"{{ option.display }}\" />\r\n\r\n <label\r\n for=\"date-filter-{{ option.display }}\"\r\n class=\"grow cursor-pointer p-1\">\r\n {{ option.display | syslang | transloco }}\r\n </label>\r\n </li>\r\n }\r\n\r\n @if (allowCustomRange) {\r\n <li\r\n role=\"listitem\"\r\n aria-label=\"open date range picker\"\r\n class=\"flex px-2 leading-7\"\r\n [class.select]=\"form.get('option')?.value === 'custom-range'\">\r\n <input\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-range-dialog\"\r\n value=\"custom-range\"\r\n (click)=\"select()\" />\r\n <div\r\n class=\"@container flex grow justify-end gap-1 p-1 @max-[340px]:flex-wrap\">\r\n <div class=\"flex gap-1\">\r\n <label for=\"datepicker-range-start\" class=\"min-w-10 truncate\">{{\r\n \"filters.from\" | transloco\r\n }}</label>\r\n <input\r\n id=\"datepicker-range-start\"\r\n name=\"start\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeFrom()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n <div class=\"flex gap-1\">\r\n <label\r\n for=\"datepicker-range-end\"\r\n class=\"min-w-10 truncate text-right\"\r\n >{{ \"filters.to\" | transloco }}</label\r\n >\r\n <input\r\n id=\"datepicker-range-end\"\r\n name=\"end\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeTo()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n </div>\r\n </li>\r\n }\r\n </ul>\r\n </form>\r\n</details>\r\n\r\n<aggregation-date-range-dialog\r\n [lang]=\"lang()\"\r\n [useDateRange]=\"false\"\r\n [min]=\"form.get('customRange.from')?.value || undefined\"\r\n [max]=\"form.get('customRange.to')?.value || undefined\"\r\n (rangeSelected)=\"onRangeSelected($event)\" />\r\n", styles: [":host{display:block;min-width:200px}ul[role=list]{scrollbar-width:thin}\n"] }]
9108
+ }, template: "<details [attr.open]=\"expanded()\" [attr.name]=\"id()\" class=\"group space-y-2\">\r\n <summary\r\n [class.cursor-pointer]=\"collapsible()\"\r\n class=\"m-0 flex h-8 w-full items-center pl-1 font-semibold select-none\"\r\n (click)=\"onHeaderClick($event)\">\r\n <ng-content select=\"label\">\r\n @let icon = aggregation()?.icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\r\n }\r\n <span class=\"grow\">{{\r\n aggregation()?.display | syslang | transloco\r\n }}</span>\r\n </ng-content>\r\n\r\n @if (hasFilters()) {\r\n <icon-button\r\n [attr.title]=\"'filters.clearFilters' | transloco\"\r\n [attr.aria-label]=\"'filters.clearFilters' | transloco\"\r\n (click)=\"clear()\"\r\n (keydown.enter)=\"clear()\">\r\n <filter-x-icon />\r\n <span class=\"sr-only\">{{ \"filters.clearFilters\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n\r\n @if (selection() && validSelection()) {\r\n <icon-button\r\n [attr.title]=\"'filters.applyFilters' | transloco\"\r\n [attr.aria-label]=\"'filters.applyFilters' | transloco\"\r\n (click)=\"apply()\"\r\n (keydown.enter)=\"apply()\">\r\n <filter-icon />\r\n <span class=\"sr-only\">{{ \"filters.applyFilters\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n @if (collapsible()) {\r\n <icon-button\r\n title=\"Open/Close\"\r\n class=\"cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\r\n <chevronright />\r\n </icon-button>\r\n }\r\n </summary>\r\n\r\n <!-- content wrapper -->\r\n <form [formGroup]=\"form\">\r\n <ul\r\n class=\"scrollbar-thin flex max-h-[var(--scroll-height,20rem)] snap-y snap-start flex-col gap-1 overflow-auto pt-2\"\r\n role=\"list\">\r\n @for (option of dateOptions(); track $index) {\r\n <li\r\n role=\"listitem\"\r\n tabindex=\"0\"\r\n (click)=\"radio.click()\"\r\n [attr.aria-label]=\"option.display | syslang | transloco\"\r\n [class]=\"\r\n cn(\r\n 'flex p-0 px-2 leading-7',\r\n form.get('option')?.value === option.display && 'bg-accent',\r\n option.hidden && 'hidden',\r\n option.disabled && 'disabled pointer-events-none text-neutral-300'\r\n )\r\n \"\r\n [attr.aria-hidden]=\"option.disabled\">\r\n <input\r\n #radio\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-{{ option.display }}\"\r\n [attr.disabled]=\"option.disabled ? true : null\"\r\n [attr.aria-disabled]=\"option.disabled\"\r\n (click)=\"select()\"\r\n value=\"{{ option.display }}\" />\r\n\r\n <label\r\n for=\"date-filter-{{ option.display }}\"\r\n class=\"grow cursor-pointer p-1\">\r\n {{ option.display | syslang | transloco }}\r\n </label>\r\n </li>\r\n }\r\n\r\n @if (allowCustomRange) {\r\n <li\r\n role=\"listitem\"\r\n aria-label=\"open date range picker\"\r\n class=\"flex px-2 leading-7\"\r\n [class.select]=\"form.get('option')?.value === 'custom-range'\">\r\n <input\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-range-dialog\"\r\n value=\"custom-range\"\r\n (click)=\"select()\" />\r\n <div\r\n class=\"@container flex grow justify-end gap-1 p-1 @max-[340px]:flex-wrap\">\r\n <div class=\"flex gap-1\">\r\n <label for=\"datepicker-range-start\" class=\"min-w-10 truncate\">{{\r\n \"filters.from\" | transloco\r\n }}</label>\r\n <input\r\n id=\"datepicker-range-start\"\r\n name=\"start\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeFrom()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n <div class=\"flex gap-1\">\r\n <label\r\n for=\"datepicker-range-end\"\r\n class=\"min-w-10 truncate text-right\"\r\n >{{ \"filters.to\" | transloco }}</label\r\n >\r\n <input\r\n id=\"datepicker-range-end\"\r\n name=\"end\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeTo()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n </div>\r\n </li>\r\n }\r\n </ul>\r\n </form>\r\n</details>\r\n\r\n<aggregation-date-range-dialog\r\n [lang]=\"lang()\"\r\n [useDateRange]=\"false\"\r\n [min]=\"form.get('customRange.from')?.value || undefined\"\r\n [max]=\"form.get('customRange.to')?.value || undefined\"\r\n (rangeSelected)=\"onRangeSelected($event)\" />\r\n", styles: [":host{display:block;min-width:200px}ul[role=list]{scrollbar-width:thin}\n"] }]
9223
9109
  }] });
9224
9110
 
9225
9111
  class ArticleEntities {
@@ -9862,6 +9748,8 @@ class AlertDialog {
9862
9748
  if (!this.alert)
9863
9749
  return;
9864
9750
  const q = this.alert.query;
9751
+ // `q.filters` widened to `Filter[] | LegacyFilter[]` upstream, but the
9752
+ // query-params store deliberately deals in `LegacyFilter[]` only.
9865
9753
  const filters = Array.isArray(q.filters) ? q.filters : undefined;
9866
9754
  this.queryParamsStore.patch({ text: q.text, tab: q.tab, basket: q.basket, sort: q.sort, filters, name: q.name });
9867
9755
  this.dialog()?.close();
@@ -10654,6 +10542,15 @@ class SignInComponent {
10654
10542
  destroyRef;
10655
10543
  cn = cn;
10656
10544
  config = globalConfig;
10545
+ /**
10546
+ * True when authentication is handled outside the credentials form — i.e. by the
10547
+ * browser/proxy (`useSSO`) or by an auto-configured OAuth/SAML provider. In those
10548
+ * modes this screen shows a loader instead of a login form and initiates the
10549
+ * handshake automatically by calling `handleLogin()`.
10550
+ */
10551
+ externalAuth = !!(globalConfig.useSSO ||
10552
+ globalConfig.autoOAuthProvider ||
10553
+ globalConfig.autoSAMLProvider);
10657
10554
  class = input(...(ngDevMode ? [undefined, { debugName: "class" }] : []));
10658
10555
  forgotPassword = output();
10659
10556
  username = model("", ...(ngDevMode ? [{ debugName: "username" }] : []));
@@ -10675,6 +10572,30 @@ class SignInComponent {
10675
10572
  expiresSoonNotified = signal(false, ...(ngDevMode ? [{ debugName: "expiresSoonNotified" }] : []));
10676
10573
  constructor(destroyRef) {
10677
10574
  this.destroyRef = destroyRef;
10575
+ // If the user is already authenticated when landing here (e.g. page refresh on
10576
+ // /login, or an external handshake completed before this screen was created),
10577
+ // don't sit on the loader: go straight to the returnUrl.
10578
+ if (this.authenticated()) {
10579
+ const url = this.route.snapshot.queryParams["returnUrl"] || "/";
10580
+ this.router.navigateByUrl(url);
10581
+ }
10582
+ // When authentication is delegated to the browser/proxy (SSO) or an OAuth/SAML
10583
+ // provider, no credentials form is shown: this screen shows a loader and initiates
10584
+ // the handshake automatically by calling `handleLogin()`. If the handshake never
10585
+ // completes, fall back to /error after 5s; the fallback is cancelled as soon as
10586
+ // the login succeeds (the `authenticated` event then drives navigation).
10587
+ if (this.externalAuth && !this.authenticated()) {
10588
+ const timeout = setTimeout(() => {
10589
+ this.router.navigate(["/error"], {
10590
+ queryParams: { returnUrl: this.route.snapshot.queryParams["returnUrl"] }
10591
+ });
10592
+ }, 5000);
10593
+ destroyRef.onDestroy(() => clearTimeout(timeout));
10594
+ this.handleLogin().then(result => {
10595
+ if (result)
10596
+ clearTimeout(timeout);
10597
+ });
10598
+ }
10678
10599
  effect(() => {
10679
10600
  const principal = getState(this.principalStore);
10680
10601
  if (this.authenticated() && principal && !this.expiresSoonNotified()) {
@@ -10720,14 +10641,16 @@ class SignInComponent {
10720
10641
  this.router.navigate(["/login"]);
10721
10642
  }
10722
10643
  async handleLogin() {
10723
- login().then((result) => {
10644
+ return login().then((result) => {
10724
10645
  if (result) {
10725
10646
  this.auditService.notifyLogin();
10726
10647
  }
10648
+ return result;
10727
10649
  }).catch(error => {
10728
10650
  warn("An error occurred while logging in", error);
10729
10651
  this.auditService.notify({ type: 'Login_Denied' });
10730
10652
  this.router.navigate(["error"]);
10653
+ return false;
10731
10654
  });
10732
10655
  }
10733
10656
  async handleLoginWithCredentials() {
@@ -10770,7 +10693,7 @@ class SignInComponent {
10770
10693
  }
10771
10694
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: SignInComponent, deps: [{ token: i0.DestroyRef }], target: i0.ɵɵFactoryTarget.Component });
10772
10695
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: SignInComponent, isStandalone: true, selector: "signIn, signin, sign-in", inputs: { class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, username: { classPropertyName: "username", publicName: "username", isSignal: true, isRequired: false, transformFunction: null }, password: { classPropertyName: "password", publicName: "password", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { forgotPassword: "forgotPassword", username: "usernameChange", password: "passwordChange" }, host: { properties: { "class": "cn('grid h-dvh w-full place-content-center', class())" } }, providers: [provideTranslocoScope("login")], ngImport: i0, template: `
10773
- @if (!authenticated()) {
10696
+ @if (!authenticated() && !externalAuth) {
10774
10697
  <Card
10775
10698
  hover="no"
10776
10699
  cdkTrapFocus
@@ -10781,60 +10704,54 @@ class SignInComponent {
10781
10704
  </CardHeader>
10782
10705
 
10783
10706
  <CardContent class="grid gap-4">
10784
- @let useCredentials =
10785
- !config.autoOAuthProvider && !config.autoSAMLProvider;
10786
- @if (useCredentials) {
10787
- <!-- authentication using credentials -->
10788
- <div class="grid gap-2">
10789
- <label class="text-sm font-medium" for="username">{{
10790
- "login.username" | transloco
10791
- }}</label>
10792
- <input
10793
- id="username"
10794
- type="text"
10795
- required
10796
- [(ngModel)]="username"
10797
- (keydown.enter)="handleLoginWithCredentials()" />
10798
- </div>
10799
-
10800
- <div class="grid gap-2">
10801
- <label class="text-sm font-medium" for="password">{{
10802
- "login.password" | transloco
10803
- }}</label>
10804
- <input
10805
- id="password"
10806
- type="password"
10807
- required
10808
- [(ngModel)]="password"
10809
- (keydown.enter)="handleLoginWithCredentials()" />
10810
- </div>
10811
-
10812
- <span
10813
- class="text-muted-foreground cursor-pointer justify-self-start text-xs hover:underline"
10814
- role="button"
10815
- tabindex="0"
10816
- (click)="forgotPassword.emit()"
10817
- (keydown.enter)="forgotPassword.emit()">
10818
- {{ "login.forgotPassword" | transloco }}
10819
- </span>
10820
- <button variant="primary"
10821
- [disabled]="!isValid()"
10822
- (click)="handleLoginWithCredentials()">
10823
- {{ "login.connect" | transloco }}
10824
- </button>
10825
- }
10826
- @else {
10827
- <!-- authentication using OAuth or SAML provider -->
10828
- <button (click)="handleLogin()">
10829
- {{ "login.SignInWith" | transloco : { provider: config.autoOAuthProvider ? "OAuth" : "SAML" } }}
10830
- </button>
10831
- }
10707
+ <!-- authentication using credentials -->
10708
+ <div class="grid gap-2">
10709
+ <label class="text-sm font-medium" for="username">{{
10710
+ "login.username" | transloco
10711
+ }}</label>
10712
+ <input
10713
+ id="username"
10714
+ type="text"
10715
+ required
10716
+ [(ngModel)]="username"
10717
+ (keydown.enter)="handleLoginWithCredentials()" />
10718
+ </div>
10719
+
10720
+ <div class="grid gap-2">
10721
+ <label class="text-sm font-medium" for="password">{{
10722
+ "login.password" | transloco
10723
+ }}</label>
10724
+ <input
10725
+ id="password"
10726
+ type="password"
10727
+ required
10728
+ [(ngModel)]="password"
10729
+ (keydown.enter)="handleLoginWithCredentials()" />
10730
+ </div>
10731
+
10732
+ <span
10733
+ class="text-muted-foreground cursor-pointer justify-self-start text-xs hover:underline"
10734
+ role="button"
10735
+ tabindex="0"
10736
+ (click)="forgotPassword.emit()"
10737
+ (keydown.enter)="forgotPassword.emit()">
10738
+ {{ "login.forgotPassword" | transloco }}
10739
+ </span>
10740
+ <button variant="primary"
10741
+ [disabled]="!isValid()"
10742
+ (click)="handleLoginWithCredentials()">
10743
+ {{ "login.connect" | transloco }}
10744
+ </button>
10832
10745
  </CardContent>
10833
10746
  </Card>
10834
10747
  } @else {
10835
- <app-wait />
10748
+ <div class="flex h-dvh w-full items-center justify-center">
10749
+ <div class="flex flex-col items-center space-y-4">
10750
+ <span class="loader"></span>
10751
+ </div>
10752
+ </div>
10836
10753
  }
10837
- `, isInline: true, styles: ["input{background-color:var(--background)}\n"], dependencies: [{ kind: "ngmodule", type: RouterModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: i2.CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }, { kind: "directive", type: InputComponent, selector: "input[type=\"text\"], input[type=\"email\"], input[type=\"number\"], input[type=\"password\"], input[type=\"tel\"], input[type=\"url\"], input[type=\"time\"], input[type=\"file\"]", inputs: ["class", "variant", "decoration"] }, { kind: "directive", type: ButtonComponent, selector: "button", inputs: ["class", "variant", "decoration", "scheme", "iconOnly", "size", "solid"] }, { kind: "directive", type: CardComponent, selector: ".card, card, Card", inputs: ["class", "variant", "hover"] }, { kind: "directive", type: CardHeaderComponent, selector: ".card-header, card-header, CardHeader, cardheader", inputs: ["class"] }, { kind: "directive", type: CardContentComponent, selector: ".card-content, card-content, CardContent, cardcontent", inputs: ["class"] }, { kind: "component", type: LoadingComponent, selector: "app-wait" }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
10754
+ `, isInline: true, styles: ["input{background-color:var(--background)}.loader{--w: 96px;--h: 96px;transform:rotate(45deg);perspective:1000px;border-radius:50%;width:var(--w);height:var(--h);color:#0040bf}.loader:before,.loader:after{content:\"\";display:block;position:absolute;top:0;left:0;width:inherit;height:inherit;border-radius:50%;transform:rotateX(70deg);animation:1s spin linear infinite}.loader:after{color:#ff854a;transform:rotateY(70deg);animation-delay:.4s}@keyframes spin{0%,to{box-shadow:.4em 0 0 0 currentcolor}12%{box-shadow:.4em .4em 0 0 currentcolor}25%{box-shadow:0 .4em 0 0 currentcolor}37%{box-shadow:-.4em .4em 0 0 currentcolor}50%{box-shadow:-.4em 0 0 0 currentcolor}62%{box-shadow:-.4em -.4em 0 0 currentcolor}75%{box-shadow:0 -.4em 0 0 currentcolor}87%{box-shadow:.4em -.4em 0 0 currentcolor}}\n"], dependencies: [{ kind: "ngmodule", type: RouterModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: i2.CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }, { kind: "directive", type: InputComponent, selector: "input[type=\"text\"], input[type=\"email\"], input[type=\"number\"], input[type=\"password\"], input[type=\"tel\"], input[type=\"url\"], input[type=\"time\"], input[type=\"file\"]", inputs: ["class", "variant", "decoration"] }, { kind: "directive", type: ButtonComponent, selector: "button", inputs: ["class", "variant", "decoration", "scheme", "iconOnly", "size", "solid"] }, { kind: "directive", type: CardComponent, selector: ".card, card, Card", inputs: ["class", "variant", "hover"] }, { kind: "directive", type: CardHeaderComponent, selector: ".card-header, card-header, CardHeader, cardheader", inputs: ["class"] }, { kind: "directive", type: CardContentComponent, selector: ".card-content, card-content, CardContent, cardcontent", inputs: ["class"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
10838
10755
  }
10839
10756
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: SignInComponent, decorators: [{
10840
10757
  type: Component,
@@ -10847,10 +10764,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
10847
10764
  ButtonComponent,
10848
10765
  CardComponent,
10849
10766
  CardHeaderComponent,
10850
- CardContentComponent,
10851
- LoadingComponent
10767
+ CardContentComponent
10852
10768
  ], providers: [provideTranslocoScope("login")], template: `
10853
- @if (!authenticated()) {
10769
+ @if (!authenticated() && !externalAuth) {
10854
10770
  <Card
10855
10771
  hover="no"
10856
10772
  cdkTrapFocus
@@ -10861,62 +10777,56 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
10861
10777
  </CardHeader>
10862
10778
 
10863
10779
  <CardContent class="grid gap-4">
10864
- @let useCredentials =
10865
- !config.autoOAuthProvider && !config.autoSAMLProvider;
10866
- @if (useCredentials) {
10867
- <!-- authentication using credentials -->
10868
- <div class="grid gap-2">
10869
- <label class="text-sm font-medium" for="username">{{
10870
- "login.username" | transloco
10871
- }}</label>
10872
- <input
10873
- id="username"
10874
- type="text"
10875
- required
10876
- [(ngModel)]="username"
10877
- (keydown.enter)="handleLoginWithCredentials()" />
10878
- </div>
10879
-
10880
- <div class="grid gap-2">
10881
- <label class="text-sm font-medium" for="password">{{
10882
- "login.password" | transloco
10883
- }}</label>
10884
- <input
10885
- id="password"
10886
- type="password"
10887
- required
10888
- [(ngModel)]="password"
10889
- (keydown.enter)="handleLoginWithCredentials()" />
10890
- </div>
10891
-
10892
- <span
10893
- class="text-muted-foreground cursor-pointer justify-self-start text-xs hover:underline"
10894
- role="button"
10895
- tabindex="0"
10896
- (click)="forgotPassword.emit()"
10897
- (keydown.enter)="forgotPassword.emit()">
10898
- {{ "login.forgotPassword" | transloco }}
10899
- </span>
10900
- <button variant="primary"
10901
- [disabled]="!isValid()"
10902
- (click)="handleLoginWithCredentials()">
10903
- {{ "login.connect" | transloco }}
10904
- </button>
10905
- }
10906
- @else {
10907
- <!-- authentication using OAuth or SAML provider -->
10908
- <button (click)="handleLogin()">
10909
- {{ "login.SignInWith" | transloco : { provider: config.autoOAuthProvider ? "OAuth" : "SAML" } }}
10910
- </button>
10911
- }
10780
+ <!-- authentication using credentials -->
10781
+ <div class="grid gap-2">
10782
+ <label class="text-sm font-medium" for="username">{{
10783
+ "login.username" | transloco
10784
+ }}</label>
10785
+ <input
10786
+ id="username"
10787
+ type="text"
10788
+ required
10789
+ [(ngModel)]="username"
10790
+ (keydown.enter)="handleLoginWithCredentials()" />
10791
+ </div>
10792
+
10793
+ <div class="grid gap-2">
10794
+ <label class="text-sm font-medium" for="password">{{
10795
+ "login.password" | transloco
10796
+ }}</label>
10797
+ <input
10798
+ id="password"
10799
+ type="password"
10800
+ required
10801
+ [(ngModel)]="password"
10802
+ (keydown.enter)="handleLoginWithCredentials()" />
10803
+ </div>
10804
+
10805
+ <span
10806
+ class="text-muted-foreground cursor-pointer justify-self-start text-xs hover:underline"
10807
+ role="button"
10808
+ tabindex="0"
10809
+ (click)="forgotPassword.emit()"
10810
+ (keydown.enter)="forgotPassword.emit()">
10811
+ {{ "login.forgotPassword" | transloco }}
10812
+ </span>
10813
+ <button variant="primary"
10814
+ [disabled]="!isValid()"
10815
+ (click)="handleLoginWithCredentials()">
10816
+ {{ "login.connect" | transloco }}
10817
+ </button>
10912
10818
  </CardContent>
10913
10819
  </Card>
10914
10820
  } @else {
10915
- <app-wait />
10821
+ <div class="flex h-dvh w-full items-center justify-center">
10822
+ <div class="flex flex-col items-center space-y-4">
10823
+ <span class="loader"></span>
10824
+ </div>
10825
+ </div>
10916
10826
  }
10917
10827
  `, host: {
10918
10828
  "[class]": "cn('grid h-dvh w-full place-content-center', class())"
10919
- }, styles: ["input{background-color:var(--background)}\n"] }]
10829
+ }, styles: ["input{background-color:var(--background)}.loader{--w: 96px;--h: 96px;transform:rotate(45deg);perspective:1000px;border-radius:50%;width:var(--w);height:var(--h);color:#0040bf}.loader:before,.loader:after{content:\"\";display:block;position:absolute;top:0;left:0;width:inherit;height:inherit;border-radius:50%;transform:rotateX(70deg);animation:1s spin linear infinite}.loader:after{color:#ff854a;transform:rotateY(70deg);animation-delay:.4s}@keyframes spin{0%,to{box-shadow:.4em 0 0 0 currentcolor}12%{box-shadow:.4em .4em 0 0 currentcolor}25%{box-shadow:0 .4em 0 0 currentcolor}37%{box-shadow:-.4em .4em 0 0 currentcolor}50%{box-shadow:-.4em 0 0 0 currentcolor}62%{box-shadow:-.4em -.4em 0 0 currentcolor}75%{box-shadow:0 -.4em 0 0 currentcolor}87%{box-shadow:.4em -.4em 0 0 currentcolor}}\n"] }]
10920
10830
  }], ctorParameters: () => [{ type: i0.DestroyRef }], propDecorators: { class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], forgotPassword: [{ type: i0.Output, args: ["forgotPassword"] }], username: [{ type: i0.Input, args: [{ isSignal: true, alias: "username", required: false }] }, { type: i0.Output, args: ["usernameChange"] }], password: [{ type: i0.Input, args: [{ isSignal: true, alias: "password", required: false }] }, { type: i0.Output, args: ["passwordChange"] }] } });
10921
10831
 
10922
10832
  class AuthPageComponent {
@@ -11308,7 +11218,7 @@ class OverrideUserDialogComponent {
11308
11218
  }
11309
11219
  }
11310
11220
  handleOverrideUser(username, domain) {
11311
- const { useSSO, createRoutes } = globalConfig;
11221
+ const { useSSO, createRoutes, useCredentials } = globalConfig;
11312
11222
  if (username === undefined || domain === undefined) {
11313
11223
  setGlobalConfig({ userOverrideActive: false, userOverride: undefined });
11314
11224
  }
@@ -11316,7 +11226,7 @@ class OverrideUserDialogComponent {
11316
11226
  setGlobalConfig({ userOverrideActive: true, userOverride: { username, domain } });
11317
11227
  }
11318
11228
  // Login with the new user
11319
- if (useSSO) {
11229
+ if (useSSO && !useCredentials) {
11320
11230
  this.appService
11321
11231
  .initialize(createRoutes)
11322
11232
  .then(() => {
@@ -11341,6 +11251,7 @@ class OverrideUserDialogComponent {
11341
11251
  })
11342
11252
  .catch((err) => {
11343
11253
  error("An error occured while overriding (initialize)", err);
11254
+ notify.error(err.message, { duration: 2000 });
11344
11255
  setGlobalConfig({ userOverrideActive: false, userOverride: undefined });
11345
11256
  });
11346
11257
  }
@@ -11898,11 +11809,16 @@ class DrawerAdvancedFiltersComponent extends DrawerComponent {
11898
11809
  text = "";
11899
11810
  constructor() {
11900
11811
  super();
11901
- this.getFirstPageQuery();
11812
+ effect(() => {
11813
+ getState(this.appStore);
11814
+ const query = this.appStore.getDefaultQuery();
11815
+ if (query?.name) {
11816
+ this.getFirstPageQuery(query?.name);
11817
+ }
11818
+ });
11902
11819
  }
11903
- async getFirstPageQuery() {
11904
- const query = this.appStore.getDefaultQuery() || { name: "_default" };
11905
- const response = await fetchQuery({ isFirstPage: true, name: query.name });
11820
+ async getFirstPageQuery(queryName) {
11821
+ const response = await fetchQuery({ isFirstPage: true, name: queryName });
11906
11822
  this.aggregations.set(response.aggregations);
11907
11823
  }
11908
11824
  onTabChange(tab) {
@@ -13037,201 +12953,156 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
13037
12953
  args: [{ selector: 'feedback, Feedback', standalone: true, imports: [ButtonComponent, MenuComponent, MenuContentComponent, MenuItemComponent, TranslocoPipe, FeedbackDialogComponent, CommentIcon, XMarkIcon, ThumbsUpIcon, ThumbsDownIcon, FaIconComponent], providers: [provideTranslocoScope('feedback')], template: "<menu>\r\n @let feedback = \"feedback.label\" | transloco;\r\n <button [variant]=\"variant()\" [solid]=\"solid()\" [aria-label]=\"feedback\">\r\n <comment-icon />\r\n <span>{{ feedback }}</span>\r\n <x-mark-icon class=\"ms-2\" (click)=\"close($event)\" />\r\n </button>\r\n\r\n <MenuContent>\r\n @if (!disliked()) {\r\n @let feedbackLike = \"feedback.like\" | transloco;\r\n @let feedbackLiked = \"feedback.liked\" | transloco;\r\n <menuitem (click)=\"like()\" aria-label=\"feedback\">\r\n @if (liked()) {\r\n <thumbs-up-icon [fill]=\"true\" />\r\n {{ feedbackLiked }}\r\n } @else {\r\n <thumbs-up-icon />\r\n {{ feedbackLike }}\r\n }\r\n </menuitem>\r\n }\r\n @if (!liked()) {\r\n <menuitem\r\n (click)=\"dislike()\"\r\n [aria-label]=\"'feedback.dislike' | transloco\">\r\n @if (disliked()) {\r\n <thumbs-down-icon [fill]=\"true\" />\r\n {{ \"feedback.disliked\" | transloco }}\r\n } @else {\r\n <thumbs-down-icon />\r\n {{ \"feedback.dislike\" | transloco }}\r\n }\r\n </menuitem>\r\n }\r\n @for (menu of menus; track $index) {\r\n @let feedbackTitle = \"feedback.\" + menu.type + \".title\" | transloco;\r\n <menuitem\r\n (click)=\"openFeedbackDialog(menu.type)\"\r\n [aria-label]=\"feedbackTitle\">\r\n <fa-icon [faClass]=\"menu.icon\" />\r\n {{ feedbackTitle }}\r\n </menuitem>\r\n }\r\n </MenuContent>\r\n</menu>\r\n\r\n<feedback-dialog />\r\n" }]
13038
12954
  }], propDecorators: { onClose: [{ type: i0.Output, args: ["onClose"] }], feedbackDialog: [{ type: i0.ViewChild, args: [i0.forwardRef(() => FeedbackDialogComponent), { isSignal: true }] }], pages: [{ type: i0.Input, args: [{ isSignal: true, alias: "pages", required: true }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], solid: [{ type: i0.Input, args: [{ isSignal: true, alias: "solid", required: false }] }] } });
13039
12955
 
13040
- class AggregationTreeItemComponent {
13041
- cn = cn;
13042
- get disabled() {
13043
- return this.node().count === 0 ? "disabled" : null;
13044
- }
13045
- onSelect = output();
13046
- onOpen = output();
13047
- onFilter = output();
13048
- node = input.required(...(ngDevMode ? [{ debugName: "node" }] : []));
13049
- path = input.required(...(ngDevMode ? [{ debugName: "path" }] : []));
13050
- field = input(...(ngDevMode ? [undefined, { debugName: "field" }] : []));
13051
- appStore = inject(AppStore);
13052
- queryParamsStore = inject(QueryParamsStore);
13053
- searchText = inject(AggregationTreeComponent).searchText;
13054
- // is the count of items displayed, default to false
13055
- showCount = computed(() => this.appStore.general()?.features?.showAggregationItemCount ?? false, ...(ngDevMode ? [{ debugName: "showCount" }] : []));
13056
- quickFilter = computed(() => this.appStore.general()?.features?.quickFilter, ...(ngDevMode ? [{ debugName: "quickFilter" }] : []));
13057
- linkChildren = computed(() => this.appStore.general()?.features?.filterLinkChildren, ...(ngDevMode ? [{ debugName: "linkChildren" }] : []));
13058
- isFiltered = computed(() => {
13059
- const filters = this.queryParamsStore.getFilter({ field: this.field(), name: this.name() });
13060
- if (!filters)
13061
- return false;
13062
- const values = [this.node().value, `/${this.node().$path}/*`]; // to also consider the treepath value
13063
- return (values.some((v) => v === filters.value) ||
13064
- (filters.values?.length && filters.values.some((value) => values.some((v) => v === value))));
13065
- }, ...(ngDevMode ? [{ debugName: "isFiltered" }] : []));
13066
- childrenPath = computed(() => this.path().concat(`/${this.node().$path}/*`), ...(ngDevMode ? [{ debugName: "childrenPath" }] : []));
13067
- name = computed(() => {
13068
- const value = this.node().display || this.node().value;
13069
- return typeof value === "string" ? value : `${value}`;
13070
- }, ...(ngDevMode ? [{ debugName: "name" }] : []));
13071
- level = computed(() => {
13072
- const level = (this.node().$level ?? 0) - 1 + (!this.node().hasChildren ? 1 : 0);
13073
- if (this.node().hasChildren === false) {
13074
- return level + 1;
13075
- }
13076
- return level;
13077
- }, ...(ngDevMode ? [{ debugName: "level" }] : []));
13078
- select(item, e, updateChildren) {
13079
- e?.stopImmediatePropagation();
13080
- const selected = !item.$selected && !item.$selectedVisually;
13081
- item.$selected = selected;
13082
- item.$selectedVisually = false;
13083
- if (updateChildren) {
13084
- // apply selection to chilren when selected
13085
- this.selectChildren(item.items, item.$selected);
13086
- }
13087
- this.onSelect.emit(item);
13088
- }
13089
- selectChildren(items, select = true) {
13090
- if (!this.linkChildren() || !items?.length || select === undefined)
13091
- return;
13092
- items.forEach((item) => {
13093
- item.$selectedVisually = select;
13094
- if (select) {
13095
- item.$selected = false;
13096
- }
13097
- if (item.items?.length) {
13098
- this.selectChildren(item.items, select);
12956
+ class AggregationPanelComponent {
12957
+ /* collapse */
12958
+ id = input(null, ...(ngDevMode ? [{ debugName: "id" }] : []));
12959
+ collapsible = input(false, ...(ngDevMode ? [{ debugName: "collapsible" }] : []));
12960
+ collapsed = input(false, ...(ngDevMode ? [{ debugName: "collapsed" }] : []));
12961
+ isDate = input(false, ...(ngDevMode ? [{ debugName: "isDate" }] : []));
12962
+ isEmpty = input(false, ...(ngDevMode ? [{ debugName: "isEmpty" }] : []));
12963
+ /* aggregation data — used for label, search input id and load-more condition */
12964
+ aggregation = input(null, ...(ngDevMode ? [{ debugName: "aggregation" }] : []));
12965
+ /* header button state (driven by parent) */
12966
+ showFiltersCount = input(false, ...(ngDevMode ? [{ debugName: "showFiltersCount", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
12967
+ filtersCount = input(0, ...(ngDevMode ? [{ debugName: "filtersCount" }] : []));
12968
+ hasFilters = input(false, ...(ngDevMode ? [{ debugName: "hasFilters" }] : []));
12969
+ selection = input(false, ...(ngDevMode ? [{ debugName: "selection" }] : []));
12970
+ isAllSelected = input(false, ...(ngDevMode ? [{ debugName: "isAllSelected" }] : []));
12971
+ /* search */
12972
+ searchText = model("", ...(ngDevMode ? [{ debugName: "searchText" }] : []));
12973
+ itemsLength = input(0, ...(ngDevMode ? [{ debugName: "itemsLength" }] : []));
12974
+ /* load more */
12975
+ hasMore = input(false, ...(ngDevMode ? [{ debugName: "hasMore" }] : []));
12976
+ searchedItemsLength = input(0, ...(ngDevMode ? [{ debugName: "searchedItemsLength" }] : []));
12977
+ /* outputs — parent reacts to user actions */
12978
+ cleared = output();
12979
+ applied = output();
12980
+ allSelected = output();
12981
+ allUnselected = output();
12982
+ loadedMore = output();
12983
+ /* internal collapse state */
12984
+ isCollapsed = linkedSignal(() => this.collapsed(), ...(ngDevMode ? [{ debugName: "isCollapsed" }] : []));
12985
+ expanded = computed(() => (this.isCollapsed() ? null : ""), ...(ngDevMode ? [{ debugName: "expanded" }] : []));
12986
+ searchInput = viewChild("searchInput", ...(ngDevMode ? [{ debugName: "searchInput" }] : []));
12987
+ isInPopover = !!(inject(PopoverContentComponent, { optional: true }) ||
12988
+ inject(DropdownContentComponent, { optional: true }) ||
12989
+ inject(DropdownDirective, { optional: true }));
12990
+ constructor() {
12991
+ effect(() => {
12992
+ if (this.isInPopover && this.searchInput()?.nativeElement && this.expanded() !== null) {
12993
+ setTimeout(() => this.searchInput()?.nativeElement.focus(), 0);
13099
12994
  }
13100
12995
  });
13101
12996
  }
13102
- open(e, node) {
13103
- // fetch aggregation items
13104
- e.preventDefault();
13105
- e.stopImmediatePropagation();
13106
- if (node.items && node.$opened === true) {
13107
- node.$opened = false;
13108
- return;
13109
- }
13110
- if (node.items && !node.$opened) {
13111
- node.$opened = true;
12997
+ onHeaderClick(event) {
12998
+ if (!this.isDate() && this.isEmpty()) {
12999
+ event.preventDefault();
13112
13000
  return;
13113
13001
  }
13114
- this.onOpen.emit(node);
13115
- }
13116
- onTextClick(event) {
13117
- if (this.quickFilter()) {
13118
- this.select(this.node(), event, true);
13119
- this.onFilter.emit();
13002
+ if (this.collapsible()) {
13003
+ this.isCollapsed.update((v) => !v);
13120
13004
  }
13005
+ event.preventDefault();
13121
13006
  }
13122
- onChildSelect(item) {
13123
- // if some items are selected visually, this means all these items got selected visually and if one
13124
- // got changed we need to change their selection status
13125
- if (item && this.linkChildren() && !item.$selected && this.node().items.some((i) => i.$selectedVisually)) {
13126
- this.node().items.forEach((i) => {
13127
- if (i !== item) {
13128
- i.$selectedVisually = false;
13129
- i.$selected = true;
13130
- }
13131
- });
13132
- }
13133
- if (this.linkChildren() && this.node().items.some((i) => !i.$selectedVisually && !i.$selected)) {
13134
- this.node().$selected = false;
13135
- this.node().$selectedVisually = false;
13136
- }
13137
- this.onSelect.emit(this.node());
13007
+ onToggle(event) {
13008
+ const e = event;
13009
+ this.isCollapsed.set(e.newState === "closed");
13010
+ }
13011
+ clearSearch(e) {
13012
+ e.stopImmediatePropagation();
13013
+ this.searchText.set("");
13138
13014
  }
13139
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AggregationTreeItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
13140
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: AggregationTreeItemComponent, isStandalone: true, selector: "aggregation-tree-item, AggregationTreeItem, aggregationtreeitem", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null }, path: { classPropertyName: "path", publicName: "path", isSignal: true, isRequired: true, transformFunction: null }, field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSelect: "onSelect", onOpen: "onOpen", onFilter: "onFilter" }, host: { properties: { "attr.disabled": "this.disabled" } }, ngImport: i0, template: "<a\r\n role=\"listitem\"\r\n [attr.aria-selected]=\"node().$selected || node().$selectedVisually\"\r\n [attr.aria-label]=\"name() | syslang\"\r\n [style.--level]=\"level()\"\r\n [class]=\"\r\n cn(\r\n 'flex grow items-center gap-2 p-1 leading-7',\r\n node().count === 0 && 'disabled pointer-events-none',\r\n (node().$selected || node().$selectedVisually) && ''\r\n )\r\n \"\r\n (click)=\"select(node(), $event, true)\">\r\n <!-- chrevron is visible only if the node has children -->\r\n <button\r\n (click)=\"open($event, node())\"\r\n class=\"transition-transform ease-in hover:scale-125\"\r\n aria-label=\"Open\">\r\n <ChevronRight\r\n [class]=\"\r\n cn(\r\n 'size-4 translate-x-1',\r\n node().$opened && 'rotate-90',\r\n !node().hasChildren && 'hidden'\r\n )\r\n \"\r\n width=\"16\"\r\n height=\"16\" />\r\n </button>\r\n\r\n <input\r\n type=\"checkbox\"\r\n role=\"checkbox\"\r\n value=\"{{ node().value }}\"\r\n [attr.disabled]=\"node().count === 0 ? true : null\"\r\n [attr.aria-disabled]=\"node().count === 0\"\r\n (keydown.enter)=\"select(node(), $event)\"\r\n [checked]=\"node().$selected || node().$selectedVisually\" />\r\n\r\n @let icon = node().icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"self-center justify-self-center\" />\r\n }\r\n <span\r\n [class]=\"\r\n cn(\r\n 'line-clamp-1 break-all text-ellipsis',\r\n quickFilter() && 'hover:underline'\r\n )\r\n \"\r\n [title]=\"\r\n quickFilter()\r\n ? ((isFiltered() ? 'filters.removeFilter' : 'filters.addFilter')\r\n | transloco) +\r\n ': ' +\r\n (name() | syslang)\r\n : (name() | syslang)\r\n \"\r\n (click)=\"onTextClick($event)\">\r\n @for (\r\n chunk of (name() | syslang) ?? \"\" | highlightWord: searchText() : 10;\r\n track $index\r\n ) {\r\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\r\n chunk.text\r\n }}</span>\r\n }\r\n </span>\r\n @if (showCount() && node().count > 0) {\r\n <span class=\"ml-auto px-1 text-xs empty:hidden\" aria-hidden=\"true\">{{\r\n node().count\r\n }}</span>\r\n }\r\n</a>\r\n\r\n@if (node().hasChildren && node().$opened) {\r\n @for (item of node().items; track $index) {\r\n <AggregationTreeItem\r\n [node]=\"item\"\r\n [path]=\"childrenPath()\"\r\n [field]=\"field()\"\r\n (onOpen)=\"onOpen.emit($event)\"\r\n (onFilter)=\"onFilter.emit()\"\r\n (onSelect)=\"onChildSelect($event)\" />\r\n }\r\n}\r\n", styles: [":host{display:block;-webkit-user-select:none;user-select:none}:host a{padding-left:calc((var(--agg-tree-indent, .5rem) * var(--level)))}a{line-height:var(--agg-item-height, inherit)}\n"], dependencies: [{ kind: "component", type: AggregationTreeItemComponent, selector: "aggregation-tree-item, AggregationTreeItem, aggregationtreeitem", inputs: ["node", "path", "field"], outputs: ["onSelect", "onOpen", "onFilter"] }, { kind: "directive", type: ListItemComponent, selector: "[role=\"listitem\"], [role=\"option\"]", inputs: ["class", "variant", "decoration"] }, { kind: "component", type: ChevronRightIcon, selector: "chevron-right, ChevronRight, chevronright, ChevronRightIcon, chevron-right-icon, chevronrighticon", inputs: ["class"] }, { kind: "component", type: FaIconComponent, selector: "fa-icon, FaIcon", inputs: ["faClass", "class"] }, { kind: "pipe", type: HighlightWordPipe, name: "highlightWord" }, { kind: "pipe", type: SyslangPipe, name: "syslang" }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
13015
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AggregationPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
13016
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: AggregationPanelComponent, isStandalone: true, selector: "AggregationPanel, aggregation-panel", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, collapsible: { classPropertyName: "collapsible", publicName: "collapsible", isSignal: true, isRequired: false, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, isDate: { classPropertyName: "isDate", publicName: "isDate", isSignal: true, isRequired: false, transformFunction: null }, isEmpty: { classPropertyName: "isEmpty", publicName: "isEmpty", isSignal: true, isRequired: false, transformFunction: null }, aggregation: { classPropertyName: "aggregation", publicName: "aggregation", isSignal: true, isRequired: false, transformFunction: null }, showFiltersCount: { classPropertyName: "showFiltersCount", publicName: "showFiltersCount", isSignal: true, isRequired: false, transformFunction: null }, filtersCount: { classPropertyName: "filtersCount", publicName: "filtersCount", isSignal: true, isRequired: false, transformFunction: null }, hasFilters: { classPropertyName: "hasFilters", publicName: "hasFilters", isSignal: true, isRequired: false, transformFunction: null }, selection: { classPropertyName: "selection", publicName: "selection", isSignal: true, isRequired: false, transformFunction: null }, isAllSelected: { classPropertyName: "isAllSelected", publicName: "isAllSelected", isSignal: true, isRequired: false, transformFunction: null }, searchText: { classPropertyName: "searchText", publicName: "searchText", isSignal: true, isRequired: false, transformFunction: null }, itemsLength: { classPropertyName: "itemsLength", publicName: "itemsLength", isSignal: true, isRequired: false, transformFunction: null }, hasMore: { classPropertyName: "hasMore", publicName: "hasMore", isSignal: true, isRequired: false, transformFunction: null }, searchedItemsLength: { classPropertyName: "searchedItemsLength", publicName: "searchedItemsLength", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { searchText: "searchTextChange", cleared: "cleared", applied: "applied", allSelected: "allSelected", allUnselected: "allUnselected", loadedMore: "loadedMore" }, viewQueries: [{ propertyName: "searchInput", first: true, predicate: ["searchInput"], descendants: true, isSignal: true }], ngImport: i0, template: "<details\r\n [attr.open]=\"expanded()\"\r\n [attr.name]=\"id()\"\r\n class=\"group space-y-2\"\r\n (toggle)=\"onToggle($event)\">\r\n <summary\r\n [class.cursor-pointer]=\"collapsible() && !isEmpty()\"\r\n [class.text-muted-foreground]=\"isEmpty()\"\r\n class=\"m-0 mt-1 flex h-8 w-full items-center gap-1 pl-1 font-semibold select-none\"\r\n (click)=\"onHeaderClick($event)\">\r\n <ng-content select=\"label\">\r\n @let icon = aggregation()?.icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\r\n }\r\n <span class=\"grow truncate\">{{\r\n aggregation()?.display | syslang | transloco\r\n }}</span>\r\n </ng-content>\r\n\r\n @if (showFiltersCount() && filtersCount() > 0) {\r\n <Badge size=\"xs\" class=\"ml-1\">\r\n {{ filtersCount() }}\r\n </Badge>\r\n }\r\n @if (!isCollapsed()) {\r\n @if (hasFilters()) {\r\n @let label = \"filters.clearFilters\" | transloco;\r\n <button\r\n variant=\"none\"\r\n icon-button\r\n [aria-label]=\"label\"\r\n (click)=\"$event.stopPropagation(); cleared.emit()\">\r\n <filter-x-icon />\r\n </button>\r\n }\r\n @if (selection()) {\r\n @let label = \"filters.apply\" | transloco;\r\n <button\r\n variant=\"accent\"\r\n size=\"sm\"\r\n [aria-label]=\"label\"\r\n (click)=\"$event.stopPropagation(); applied.emit()\">\r\n <FilterIcon />\r\n {{ label }}\r\n </button>\r\n }\r\n\r\n @if (isAllSelected()) {\r\n @let label = \"filters.unselectAllFilters\" | transloco;\r\n <button\r\n variant=\"none\"\r\n icon-button\r\n [aria-label]=\"label\"\r\n (click)=\"$event.stopPropagation(); allUnselected.emit()\">\r\n <square-check-icon />\r\n </button>\r\n } @else {\r\n @let label = \"filters.selectAllFilters\" | transloco;\r\n <button\r\n variant=\"none\"\r\n icon-button\r\n [aria-label]=\"label\"\r\n (click)=\"$event.stopPropagation(); allSelected.emit()\">\r\n <square-icon />\r\n </button>\r\n }\r\n }\r\n\r\n @if (collapsible()) {\r\n <icon-button\r\n title=\"Open/Close\"\r\n class=\"cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\r\n <chevronright />\r\n <span class=\"sr-only\">{{ \"filters.toggle\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n </summary>\r\n\r\n @if (aggregation()?.searchable && itemsLength() > 0) {\r\n <InputGroup class=\"group/item mt-1\">\r\n <input\r\n #searchInput\r\n input-group\r\n id=\"aggregation-input-{{ aggregation()?.column }}\"\r\n type=\"text\"\r\n [attr.aria-label]=\"'search' | transloco\"\r\n [attr.placeholder]=\"'search' | transloco\"\r\n [(ngModel)]=\"searchText\"\r\n class=\"mt-1\" />\r\n <InputGroupAddon>\r\n <SearchIcon\r\n class=\"text-foreground size-4 rotate-0 transition-[rotate] duration-500 group-focus-within/item:rotate-90\" />\r\n </InputGroupAddon>\r\n <InputGroupAddon align=\"inline-end\" class=\"gap-0.5!\">\r\n <icon-button\r\n size=\"sm\"\r\n [class]=\"\r\n searchText().length > 0\r\n ? 'rotate-90 cursor-pointer opacity-100 transition-[rotate,opacity] duration-500'\r\n : 'pointer-events-none rotate-0 opacity-0 transition-[rotate,opacity] duration-500'\r\n \"\r\n aria-label=\"Clear search\"\r\n [tabindex]=\"searchText().length > 0 ? 0 : -1\"\r\n (keydown.enter)=\"clearSearch($event)\"\r\n (click)=\"clearSearch($event)\">\r\n <XMarkIcon />\r\n </icon-button>\r\n <ng-content select=\"[search-addon]\" />\r\n </InputGroupAddon>\r\n </InputGroup>\r\n }\r\n\r\n <ng-content />\r\n\r\n @if (hasMore() && searchedItemsLength() === 0) {\r\n <button\r\n class=\"mt-1 flex w-full justify-center\"\r\n [attr.aria-label]=\"'loadMore' | transloco\"\r\n (click)=\"loadedMore.emit()\">\r\n {{ \"loadMore\" | transloco }}\r\n </button>\r\n }\r\n</details>\r\n", dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { 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"] }, { kind: "directive", type: ButtonComponent, selector: "button", inputs: ["class", "variant", "decoration", "scheme", "iconOnly", "size", "solid"] }, { kind: "directive", type: BadgeComponent, selector: "badge, Badge", inputs: ["class", "variant", "scheme", "size"] }, { kind: "component", type: ChevronRightIcon, selector: "chevron-right, ChevronRight, chevronright, ChevronRightIcon, chevron-right-icon, chevronrighticon", inputs: ["class"] }, { kind: "directive", type: InputGroupInput, selector: "input[input-group]", inputs: ["class", "type", "placeholder", "disabled"] }, { kind: "directive", type: InputGroupComponent, selector: "input-group, inputgroup, InputGroup", inputs: ["class"] }, { kind: "directive", type: InputGroupAddonComponent, selector: "input-group-addon, inputgroupaddon, InputGroupAddon", inputs: ["class", "align"] }, { kind: "component", type: SearchIcon, selector: "SearchIcon", inputs: ["class"] }, { kind: "component", type: FilterIcon, selector: "filter-icon, FilterIcon", inputs: ["class"] }, { kind: "component", type: FaIconComponent, selector: "fa-icon, FaIcon", inputs: ["faClass", "class"] }, { kind: "component", type: FilterXIcon, selector: "filter-x-icon, FilterXIcon", inputs: ["class"] }, { kind: "component", type: SquareCheckIcon, selector: "square-check-icon, SquareCheckIcon", inputs: ["class"] }, { kind: "component", type: SquareIcon, selector: "square-icon, SquareIcon", inputs: ["class"] }, { kind: "component", type: XMarkIcon, selector: "XMarkIcon, xmark-icon, x-mark-icon", inputs: ["class"] }, { kind: "directive", type: IconButtonComponent, selector: "button[icon-button], icon-button, IconButton", inputs: ["class", "size"] }, { kind: "pipe", type: SyslangPipe, name: "syslang" }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
13141
13017
  }
13142
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AggregationTreeItemComponent, decorators: [{
13018
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AggregationPanelComponent, decorators: [{
13143
13019
  type: Component,
13144
- args: [{ selector: "aggregation-tree-item, AggregationTreeItem, aggregationtreeitem", standalone: true, imports: [HighlightWordPipe, ListItemComponent, SyslangPipe, ChevronRightIcon, TranslocoPipe, FaIconComponent], template: "<a\r\n role=\"listitem\"\r\n [attr.aria-selected]=\"node().$selected || node().$selectedVisually\"\r\n [attr.aria-label]=\"name() | syslang\"\r\n [style.--level]=\"level()\"\r\n [class]=\"\r\n cn(\r\n 'flex grow items-center gap-2 p-1 leading-7',\r\n node().count === 0 && 'disabled pointer-events-none',\r\n (node().$selected || node().$selectedVisually) && ''\r\n )\r\n \"\r\n (click)=\"select(node(), $event, true)\">\r\n <!-- chrevron is visible only if the node has children -->\r\n <button\r\n (click)=\"open($event, node())\"\r\n class=\"transition-transform ease-in hover:scale-125\"\r\n aria-label=\"Open\">\r\n <ChevronRight\r\n [class]=\"\r\n cn(\r\n 'size-4 translate-x-1',\r\n node().$opened && 'rotate-90',\r\n !node().hasChildren && 'hidden'\r\n )\r\n \"\r\n width=\"16\"\r\n height=\"16\" />\r\n </button>\r\n\r\n <input\r\n type=\"checkbox\"\r\n role=\"checkbox\"\r\n value=\"{{ node().value }}\"\r\n [attr.disabled]=\"node().count === 0 ? true : null\"\r\n [attr.aria-disabled]=\"node().count === 0\"\r\n (keydown.enter)=\"select(node(), $event)\"\r\n [checked]=\"node().$selected || node().$selectedVisually\" />\r\n\r\n @let icon = node().icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"self-center justify-self-center\" />\r\n }\r\n <span\r\n [class]=\"\r\n cn(\r\n 'line-clamp-1 break-all text-ellipsis',\r\n quickFilter() && 'hover:underline'\r\n )\r\n \"\r\n [title]=\"\r\n quickFilter()\r\n ? ((isFiltered() ? 'filters.removeFilter' : 'filters.addFilter')\r\n | transloco) +\r\n ': ' +\r\n (name() | syslang)\r\n : (name() | syslang)\r\n \"\r\n (click)=\"onTextClick($event)\">\r\n @for (\r\n chunk of (name() | syslang) ?? \"\" | highlightWord: searchText() : 10;\r\n track $index\r\n ) {\r\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\r\n chunk.text\r\n }}</span>\r\n }\r\n </span>\r\n @if (showCount() && node().count > 0) {\r\n <span class=\"ml-auto px-1 text-xs empty:hidden\" aria-hidden=\"true\">{{\r\n node().count\r\n }}</span>\r\n }\r\n</a>\r\n\r\n@if (node().hasChildren && node().$opened) {\r\n @for (item of node().items; track $index) {\r\n <AggregationTreeItem\r\n [node]=\"item\"\r\n [path]=\"childrenPath()\"\r\n [field]=\"field()\"\r\n (onOpen)=\"onOpen.emit($event)\"\r\n (onFilter)=\"onFilter.emit()\"\r\n (onSelect)=\"onChildSelect($event)\" />\r\n }\r\n}\r\n", styles: [":host{display:block;-webkit-user-select:none;user-select:none}:host a{padding-left:calc((var(--agg-tree-indent, .5rem) * var(--level)))}a{line-height:var(--agg-item-height, inherit)}\n"] }]
13145
- }], propDecorators: { disabled: [{
13146
- type: HostBinding,
13147
- args: ["attr.disabled"]
13148
- }], onSelect: [{ type: i0.Output, args: ["onSelect"] }], onOpen: [{ type: i0.Output, args: ["onOpen"] }], onFilter: [{ type: i0.Output, args: ["onFilter"] }], node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: true }] }], path: [{ type: i0.Input, args: [{ isSignal: true, alias: "path", required: true }] }], field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: false }] }] } });
13020
+ args: [{ selector: "AggregationPanel, aggregation-panel", standalone: true, imports: [
13021
+ FormsModule,
13022
+ ButtonComponent,
13023
+ SyslangPipe,
13024
+ TranslocoPipe,
13025
+ BadgeComponent,
13026
+ ChevronRightIcon,
13027
+ InputGroupInput,
13028
+ InputGroupComponent,
13029
+ InputGroupAddonComponent,
13030
+ SearchIcon,
13031
+ FilterIcon,
13032
+ FaIconComponent,
13033
+ FilterXIcon,
13034
+ SquareCheckIcon,
13035
+ SquareIcon,
13036
+ XMarkIcon,
13037
+ IconButtonComponent,
13038
+ ], template: "<details\r\n [attr.open]=\"expanded()\"\r\n [attr.name]=\"id()\"\r\n class=\"group space-y-2\"\r\n (toggle)=\"onToggle($event)\">\r\n <summary\r\n [class.cursor-pointer]=\"collapsible() && !isEmpty()\"\r\n [class.text-muted-foreground]=\"isEmpty()\"\r\n class=\"m-0 mt-1 flex h-8 w-full items-center gap-1 pl-1 font-semibold select-none\"\r\n (click)=\"onHeaderClick($event)\">\r\n <ng-content select=\"label\">\r\n @let icon = aggregation()?.icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\r\n }\r\n <span class=\"grow truncate\">{{\r\n aggregation()?.display | syslang | transloco\r\n }}</span>\r\n </ng-content>\r\n\r\n @if (showFiltersCount() && filtersCount() > 0) {\r\n <Badge size=\"xs\" class=\"ml-1\">\r\n {{ filtersCount() }}\r\n </Badge>\r\n }\r\n @if (!isCollapsed()) {\r\n @if (hasFilters()) {\r\n @let label = \"filters.clearFilters\" | transloco;\r\n <button\r\n variant=\"none\"\r\n icon-button\r\n [aria-label]=\"label\"\r\n (click)=\"$event.stopPropagation(); cleared.emit()\">\r\n <filter-x-icon />\r\n </button>\r\n }\r\n @if (selection()) {\r\n @let label = \"filters.apply\" | transloco;\r\n <button\r\n variant=\"accent\"\r\n size=\"sm\"\r\n [aria-label]=\"label\"\r\n (click)=\"$event.stopPropagation(); applied.emit()\">\r\n <FilterIcon />\r\n {{ label }}\r\n </button>\r\n }\r\n\r\n @if (isAllSelected()) {\r\n @let label = \"filters.unselectAllFilters\" | transloco;\r\n <button\r\n variant=\"none\"\r\n icon-button\r\n [aria-label]=\"label\"\r\n (click)=\"$event.stopPropagation(); allUnselected.emit()\">\r\n <square-check-icon />\r\n </button>\r\n } @else {\r\n @let label = \"filters.selectAllFilters\" | transloco;\r\n <button\r\n variant=\"none\"\r\n icon-button\r\n [aria-label]=\"label\"\r\n (click)=\"$event.stopPropagation(); allSelected.emit()\">\r\n <square-icon />\r\n </button>\r\n }\r\n }\r\n\r\n @if (collapsible()) {\r\n <icon-button\r\n title=\"Open/Close\"\r\n class=\"cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\r\n <chevronright />\r\n <span class=\"sr-only\">{{ \"filters.toggle\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n </summary>\r\n\r\n @if (aggregation()?.searchable && itemsLength() > 0) {\r\n <InputGroup class=\"group/item mt-1\">\r\n <input\r\n #searchInput\r\n input-group\r\n id=\"aggregation-input-{{ aggregation()?.column }}\"\r\n type=\"text\"\r\n [attr.aria-label]=\"'search' | transloco\"\r\n [attr.placeholder]=\"'search' | transloco\"\r\n [(ngModel)]=\"searchText\"\r\n class=\"mt-1\" />\r\n <InputGroupAddon>\r\n <SearchIcon\r\n class=\"text-foreground size-4 rotate-0 transition-[rotate] duration-500 group-focus-within/item:rotate-90\" />\r\n </InputGroupAddon>\r\n <InputGroupAddon align=\"inline-end\" class=\"gap-0.5!\">\r\n <icon-button\r\n size=\"sm\"\r\n [class]=\"\r\n searchText().length > 0\r\n ? 'rotate-90 cursor-pointer opacity-100 transition-[rotate,opacity] duration-500'\r\n : 'pointer-events-none rotate-0 opacity-0 transition-[rotate,opacity] duration-500'\r\n \"\r\n aria-label=\"Clear search\"\r\n [tabindex]=\"searchText().length > 0 ? 0 : -1\"\r\n (keydown.enter)=\"clearSearch($event)\"\r\n (click)=\"clearSearch($event)\">\r\n <XMarkIcon />\r\n </icon-button>\r\n <ng-content select=\"[search-addon]\" />\r\n </InputGroupAddon>\r\n </InputGroup>\r\n }\r\n\r\n <ng-content />\r\n\r\n @if (hasMore() && searchedItemsLength() === 0) {\r\n <button\r\n class=\"mt-1 flex w-full justify-center\"\r\n [attr.aria-label]=\"'loadMore' | transloco\"\r\n (click)=\"loadedMore.emit()\">\r\n {{ \"loadMore\" | transloco }}\r\n </button>\r\n }\r\n</details>\r\n" }]
13039
+ }], ctorParameters: () => [], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], collapsible: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsible", required: false }] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }], isDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "isDate", required: false }] }], isEmpty: [{ type: i0.Input, args: [{ isSignal: true, alias: "isEmpty", required: false }] }], aggregation: [{ type: i0.Input, args: [{ isSignal: true, alias: "aggregation", required: false }] }], showFiltersCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFiltersCount", required: false }] }], filtersCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "filtersCount", required: false }] }], hasFilters: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasFilters", required: false }] }], selection: [{ type: i0.Input, args: [{ isSignal: true, alias: "selection", required: false }] }], isAllSelected: [{ type: i0.Input, args: [{ isSignal: true, alias: "isAllSelected", required: false }] }], searchText: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchText", required: false }] }, { type: i0.Output, args: ["searchTextChange"] }], itemsLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemsLength", required: false }] }], hasMore: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasMore", required: false }] }], searchedItemsLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchedItemsLength", required: false }] }], cleared: [{ type: i0.Output, args: ["cleared"] }], applied: [{ type: i0.Output, args: ["applied"] }], allSelected: [{ type: i0.Output, args: ["allSelected"] }], allUnselected: [{ type: i0.Output, args: ["allUnselected"] }], loadedMore: [{ type: i0.Output, args: ["loadedMore"] }], searchInput: [{ type: i0.ViewChild, args: ["searchInput", { isSignal: true }] }] } });
13149
13040
 
13150
13041
  class AggregationTreeComponent {
13151
13042
  cn = cn;
13152
- virtualItems = viewChildren('virtualItem', ...(ngDevMode ? [{ debugName: "virtualItems" }] : []));
13153
- scrollElement = viewChild("scrollElement", ...(ngDevMode ? [{ debugName: "scrollElement" }] : []));
13154
- virtualizer = injectVirtualizer(() => ({
13155
- count: this.items().length,
13156
- estimateSize: () => 32,
13157
- scrollElement: this.scrollElement()
13158
- }));
13159
- #measureItems = effect(() => this.virtualItems().forEach((el) => {
13160
- this.virtualizer.measureElement(el.nativeElement);
13161
- }), ...(ngDevMode ? [{ debugName: "#measureItems" }] : []));
13162
- searchInput = viewChild("searchInput", ...(ngDevMode ? [{ debugName: "searchInput" }] : []));
13163
- /* stores */
13164
- aggregationsStore = inject(AggregationsStore);
13165
- queryParamsStore = inject(QueryParamsStore);
13166
- appStore = inject(AppStore);
13167
- /* services */
13168
- aggregationsService = inject(AggregationsService);
13169
- el = inject(ElementRef);
13170
- injector = inject(Injector);
13171
- destroyRef = inject(DestroyRef);
13043
+ /* inputs */
13172
13044
  class = input("", ...(ngDevMode ? [{ debugName: "class" }] : []));
13173
- /**
13174
- * The name of the <details> element. When you provide the same id, the component work as an accordion
13175
- * @defaultValue null
13176
- */
13177
13045
  id = input(null, ...(ngDevMode ? [{ debugName: "id" }] : []));
13178
13046
  name = input.required(...(ngDevMode ? [{ debugName: "name" }] : []));
13179
13047
  column = input.required(...(ngDevMode ? [{ debugName: "column" }] : []));
13048
+ collapsible = input(false, ...(ngDevMode ? [{ debugName: "collapsible" }] : []));
13049
+ collapsed = input(false, ...(ngDevMode ? [{ debugName: "collapsed" }] : []));
13050
+ searchable = input(undefined, ...(ngDevMode ? [{ debugName: "searchable" }] : []));
13051
+ showFiltersCount = input(false, ...(ngDevMode ? [{ debugName: "showFiltersCount", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
13180
13052
  expandedLevel = input(undefined, ...(ngDevMode ? [{ debugName: "expandedLevel", transform: (v) => {
13181
13053
  const n = numberAttribute(v);
13182
13054
  return Number.isNaN(n) ? undefined : n;
13183
- } }] : [{ transform: (v) => {
13055
+ } }] : [{
13056
+ transform: (v) => {
13184
13057
  const n = numberAttribute(v);
13185
13058
  return Number.isNaN(n) ? undefined : n;
13186
- } }]));
13059
+ },
13060
+ }]));
13061
+ /* outputs */
13187
13062
  onSelect = output();
13188
13063
  onApply = output();
13189
13064
  onClear = output();
13190
- /**
13191
- * Determines whether the aggregation component can be collapsed or expanded.
13192
- * When true, the component will display collapse/expand controls allowing users
13193
- * to show or hide the aggregation content.
13194
- *
13195
- * @default false
13196
- */
13197
- collapsible = input(false, ...(ngDevMode ? [{ debugName: "collapsible" }] : []));
13198
- /**
13199
- * Controls whether the aggregation component is in a collapsed state.
13200
- * When true, the component will be visually collapsed/hidden.
13201
- * When false, the component will be expanded/visible.
13202
- *
13203
- * @default false
13204
- */
13205
- collapsed = input(false, ...(ngDevMode ? [{ debugName: "collapsed" }] : []));
13206
- /**
13207
- * A computed signal that tracks the collapsed state of the component.
13208
- * This signal is linked to the `collapsed()` signal and automatically updates
13209
- * when the collapsed state changes.
13210
- */
13211
- isCollapsed = linkedSignal(() => this.collapsed(), ...(ngDevMode ? [{ debugName: "isCollapsed" }] : []));
13212
- /**
13213
- * Computed property that returns an empty string when the component is not collapsed,
13214
- * or null when the component is collapsed. This is typically used to control
13215
- * expansion state in UI components with conditional rendering or styling.
13216
- *
13217
- * @returns Empty string if not collapsed, null if collapsed
13218
- */
13219
- expanded = computed(() => (this.isCollapsed() ? null : ''), ...(ngDevMode ? [{ debugName: "expanded" }] : []));
13220
- /**
13221
- * A boolean flag indicating whether the component is searchable.
13222
- * This property is initialized to `undefined` by default.
13223
- * "Undefined" and not "false" because this input overrides the custom json settings
13224
- */
13225
- searchable = input(undefined, ...(ngDevMode ? [{ debugName: "searchable" }] : []));
13065
+ /* view queries */
13066
+ virtualItems = viewChildren("virtualItem", ...(ngDevMode ? [{ debugName: "virtualItems" }] : []));
13067
+ scrollElement = viewChild("scrollElement", ...(ngDevMode ? [{ debugName: "scrollElement" }] : []));
13068
+ /* selection state */
13226
13069
  selection = signal(false, ...(ngDevMode ? [{ debugName: "selection" }] : []));
13227
- /**
13228
- * A boolean flag indicating whether we want to see the filters count when some is applied
13229
- * This property is initialized to `false` by default.
13230
- */
13231
- showFiltersCount = input(false, ...(ngDevMode ? [{ debugName: "showFiltersCount", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
13232
- /* aggregation */
13070
+ isAllSelected = signal(false, ...(ngDevMode ? [{ debugName: "isAllSelected" }] : []));
13071
+ /* search state */
13072
+ searchText = model("", ...(ngDevMode ? [{ debugName: "searchText" }] : []));
13073
+ /* composable — injects stores/services, wires search effects, provides shared methods */
13074
+ base = injectAggregationBase({
13075
+ name: this.name,
13076
+ column: this.column,
13077
+ searchText: this.searchText,
13078
+ });
13079
+ /* spread from base */
13080
+ aggregationsStore = this.base.aggregationsStore;
13081
+ queryParamsStore = this.base.queryParamsStore;
13082
+ appStore = this.base.appStore;
13083
+ aggregationsService = this.base.aggregationsService;
13084
+ injector = this.base.injector;
13085
+ destroyRef = this.base.destroyRef;
13086
+ debouncedSearchText = this.base.debouncedSearchText;
13087
+ normalizedSearchText = this.base.normalizedSearchText;
13088
+ suggests = this.base.suggests;
13089
+ hasFilters = this.base.hasFilters;
13090
+ filtersCount = this.base.filtersCount;
13091
+ query = this.base.query;
13092
+ filters = this.base.filters;
13093
+ /* features from appStore */
13094
+ showCount = computed(() => this.appStore.general()?.features?.showAggregationItemCount ?? false, ...(ngDevMode ? [{ debugName: "showCount" }] : []));
13095
+ quickFilter = computed(() => this.appStore.general()?.features?.quickFilter, ...(ngDevMode ? [{ debugName: "quickFilter" }] : []));
13096
+ isDate = computed(() => this.appStore.isDateColumn(this.aggregation()?.column || ""), ...(ngDevMode ? [{ debugName: "isDate" }] : []));
13097
+ /* virtualizer */
13098
+ virtualizer = injectVirtualizer(() => ({
13099
+ count: this.items().length,
13100
+ estimateSize: () => 32,
13101
+ scrollElement: this.scrollElement(),
13102
+ }));
13103
+ #measureItems = effect(() => this.virtualItems().forEach((el) => this.virtualizer.measureElement(el.nativeElement)), ...(ngDevMode ? [{ debugName: "#measureItems" }] : []));
13104
+ linkChildren = computed(() => this.appStore.general()?.features?.filterLinkChildren, ...(ngDevMode ? [{ debugName: "linkChildren" }] : []));
13233
13105
  aggregation = computed(() => {
13234
- // when the aggegation store updates, we need to check if the aggregation is still valid
13235
13106
  getState(this.aggregationsStore);
13236
13107
  const name = this.name();
13237
13108
  const column = this.column();
@@ -13241,21 +13112,17 @@ class AggregationTreeComponent {
13241
13112
  if (!agg.isTree) {
13242
13113
  error("The aggregation tree component does not support list aggregations. Please use the <Aggregation /> component instead.");
13243
13114
  }
13244
- // overrides "expandedLevel" from custom JSON file
13245
13115
  const expandedLevel = this.expandedLevel() ?? agg.expandedLevel;
13246
13116
  if (expandedLevel) {
13247
13117
  this.expandItems(agg.items, expandedLevel);
13248
13118
  }
13249
- // overrides "searchable" properties with the input if any
13250
13119
  agg.searchable = this.searchable() ?? agg.searchable;
13251
13120
  return agg;
13252
13121
  }
13253
13122
  }
13254
13123
  return null;
13255
13124
  }, ...(ngDevMode ? [{ debugName: "aggregation" }] : []));
13256
- /* items of the aggregation */
13257
13125
  items = computed(() => {
13258
- // when the aggegation store updates, we need to check if the aggregation is still valid
13259
13126
  getState(this.aggregationsStore);
13260
13127
  const agg = this.aggregation();
13261
13128
  const searchedItems = this.searchedItems();
@@ -13266,57 +13133,17 @@ class AggregationTreeComponent {
13266
13133
  else if (agg?.items) {
13267
13134
  res = this.addCurrentFiltersToItems();
13268
13135
  }
13269
- // use session storage to keep the selected statuses
13270
13136
  const sessionAggItem = sessionStorage.getItem(`agg-${agg?.column}`);
13271
13137
  const sessionAgg = JSON.parse(sessionAggItem || "[]");
13272
13138
  return this.processAggregations(sessionAgg.length ? this.setSelected(res, sessionAgg) : res);
13273
13139
  }, ...(ngDevMode ? [{ debugName: "items" }] : []));
13274
- /**
13275
- * Computed signal that determines whether the items collection is empty.
13276
- * @returns True if the items array has no elements, false otherwise.
13277
- */
13278
13140
  isEmpty = computed(() => this.items().length === 0, ...(ngDevMode ? [{ debugName: "isEmpty" }] : []));
13279
- /**
13280
- * A computed property that determines whether there are active filters
13281
- * for the current aggregation column.
13282
- *
13283
- * if True, the clear button is shown.
13284
- *
13285
- * @returns {boolean} `true` if the filter count for the aggregation column is greater than 0, otherwise `false`.
13286
- */
13287
- hasFilters = computed(() => {
13288
- const { count = 0 } = this.queryParamsStore.getFilter({ field: this.aggregation()?.column, name: this.aggregation()?.name }) || {};
13289
- return count > 0;
13290
- }, ...(ngDevMode ? [{ debugName: "hasFilters" }] : []));
13291
- /**
13292
- * A computed property that returns the number of items of this aggregation applied in the active filters
13293
- *
13294
- * if more than 0 and the showCount input is set as True, the count number is shown.
13295
- *
13296
- * @returns {number} the filters count.
13297
- */
13298
- filtersCount = computed(() => {
13299
- const { count = 0 } = this.queryParamsStore.getFilter({ field: this.aggregation()?.column, name: this.aggregation()?.name }) || {};
13300
- return count;
13301
- }, ...(ngDevMode ? [{ debugName: "filtersCount" }] : []));
13302
- isAllSelected = signal(false, ...(ngDevMode ? [{ debugName: "isAllSelected" }] : []));
13303
- /* search feature */
13304
- searchText = model("", ...(ngDevMode ? [{ debugName: "searchText" }] : []));
13305
- debouncedSearchText = debouncedSignal(this.searchText, 300);
13306
- normalizedSearchText = computed(() => this.debouncedSearchText()
13307
- .normalize("NFD")
13308
- .replace(/[\u0300-\u036f]/g, ""), ...(ngDevMode ? [{ debugName: "normalizedSearchText" }] : []));
13309
- /* suggestions */
13310
- suggests = signal([], ...(ngDevMode ? [{ debugName: "suggests" }] : []));
13311
- /* searched items */
13312
13141
  searchedItems = computed(() => {
13313
13142
  if (!this.suggests())
13314
13143
  return [];
13315
- // if the aggregation is a tree, we transform the suggestions into tree nodes
13316
13144
  if (this.aggregation()?.isTree) {
13317
13145
  return suggestionsToTreeAggregationNodes(this.suggests(), this.searchText());
13318
13146
  }
13319
- // if the aggregation is not a tree, we return the suggestions as is
13320
13147
  return this.suggests()?.map((suggest) => ({
13321
13148
  name: this.name(),
13322
13149
  value: suggest.normalized || suggest.display || "",
@@ -13324,41 +13151,18 @@ class AggregationTreeComponent {
13324
13151
  column: suggest.category,
13325
13152
  count: Number(suggest.frequency),
13326
13153
  $selected: false,
13327
- items: []
13154
+ items: [],
13328
13155
  }));
13329
13156
  }, ...(ngDevMode ? [{ debugName: "searchedItems" }] : []));
13330
- linkChildren = computed(() => this.appStore.general()?.features?.filterLinkChildren, ...(ngDevMode ? [{ debugName: "linkChildren" }] : []));
13331
- query;
13332
- filters = signal([], ...(ngDevMode ? [{ debugName: "filters" }] : []));
13333
13157
  constructor() {
13334
- this.query = buildQuery();
13335
13158
  effect(() => {
13336
- // if the aggregation store changes, remove previous session storage
13337
13159
  getState(this.aggregationsStore);
13338
13160
  sessionStorage.removeItem(`agg-${this.column()}`);
13339
13161
  });
13340
- effect(() => {
13341
- // focus the search input when expanded
13342
- if (this.searchInput()?.nativeElement && this.expanded() !== null) {
13343
- setTimeout(() => {
13344
- this.searchInput()?.nativeElement.focus();
13345
- }, 0);
13346
- }
13347
- });
13348
- effect(async () => {
13349
- if (this.debouncedSearchText() === "" || this.aggregation() === null) {
13350
- this.suggests.set([]);
13351
- return;
13352
- }
13353
- const query = this.queryParamsStore.getQuery();
13354
- const suggests = (await withFetch(() => fetchSuggestField(this.normalizedSearchText(), [this.aggregation()?.column || ""], query), this.injector)) || [];
13355
- this.suggests.set(suggests);
13356
- });
13357
13162
  effect(() => {
13358
13163
  this.filters.set(this.getFilters());
13359
13164
  });
13360
13165
  this.destroyRef.onDestroy(() => {
13361
- // If the popover is closed with unapplied selections, reset state so it doesn't persist when reopening
13362
13166
  sessionStorage.removeItem(`agg-${this.aggregation()?.column}`);
13363
13167
  if (this.selection()) {
13364
13168
  const unselect = (items) => {
@@ -13373,40 +13177,6 @@ class AggregationTreeComponent {
13373
13177
  }
13374
13178
  });
13375
13179
  }
13376
- // compare currentItems and updatedItems to add the new sub items
13377
- // from updatedItems into currentItems to not alter the already loaded items
13378
- addNewItems(currentItems, updatedItems, selectedParent) {
13379
- updatedItems.forEach((item) => {
13380
- const currentItem = currentItems.find((i) => i.value === item.value || i.value === `/${item.$path}/*`);
13381
- if (currentItem) {
13382
- if (currentItem.items?.length) {
13383
- this.addNewItems(currentItem.items, item.items, item.$selected);
13384
- }
13385
- else {
13386
- if (selectedParent || currentItem.$selected) {
13387
- // select children
13388
- const selectedItems = (items) => items.map((item) => {
13389
- item.$selected = true;
13390
- if (item.items)
13391
- item.items = selectedItems(item.items);
13392
- return item;
13393
- });
13394
- if (item.items)
13395
- currentItem.items = selectedItems(item.items);
13396
- }
13397
- else {
13398
- currentItem.items = item.items;
13399
- }
13400
- }
13401
- }
13402
- });
13403
- }
13404
- /**
13405
- * Clears the current filter for the aggregation column.
13406
- *
13407
- * This method updates the filter in the `queryParamsStore` by setting the display value
13408
- * of the current aggregation column to an empty string.
13409
- */
13410
13180
  clear() {
13411
13181
  const agg = this.aggregation();
13412
13182
  if (agg) {
@@ -13426,72 +13196,25 @@ class AggregationTreeComponent {
13426
13196
  this.onClear.emit();
13427
13197
  }
13428
13198
  }
13429
- /**
13430
- * Select all filters for the aggregation column.
13431
- */
13432
13199
  selectAll() {
13433
13200
  if (this.items().length) {
13434
- this.selectItems(this.items(), true);
13201
+ this.base.selectItems(this.items(), true, true);
13435
13202
  this.selection.set(true);
13436
13203
  this.isAllSelected.set(true);
13437
13204
  }
13438
13205
  }
13439
- /**
13440
- * Unselect all filters for the aggregation column.
13441
- */
13442
13206
  unselectAll() {
13443
13207
  if (this.items().length) {
13444
- this.selectItems(this.items(), false);
13208
+ this.base.selectItems(this.items(), false, true);
13445
13209
  this.select();
13446
13210
  this.isAllSelected.set(false);
13447
13211
  }
13448
13212
  }
13449
- /**
13450
- * Applies the current filters to the query parameters store.
13451
- *
13452
- * - If there are multiple filters, they are wrapped in an "or" filter.
13453
- * - If the aggregation is not a distribution, the filters are merged into a single filter with an "in" operator.
13454
- * - If there is only one filter, it is directly applied.
13455
- * - If there are no filters, the current filters are cleared.
13456
- *
13457
- * After applying the filters, the search text is reset.
13458
- */
13459
13213
  apply(overrideFilters) {
13460
13214
  sessionStorage.setItem(`agg-${this.aggregation()?.column}`, JSON.stringify([...this.items()]));
13461
13215
  const filters = overrideFilters || this.getFilters();
13462
- const { name, column: field } = this.aggregation();
13463
- // if filters length > 1, we need to wrap them in an "or" filter
13464
- if (filters.length > 1) {
13465
- const display = filters[0].display;
13466
- // if aggregation not a distribution, we need to merge the filters into a single filter with an in operator
13467
- // with the values of the filters
13468
- if (this.aggregation()?.isDistribution) {
13469
- this.queryParamsStore.updateFilter({
13470
- operator: "or",
13471
- filters,
13472
- name,
13473
- field,
13474
- display
13475
- });
13476
- }
13477
- else {
13478
- const values = filters.map((filter) => filter.value);
13479
- this.queryParamsStore.updateFilter({
13480
- operator: "in",
13481
- name,
13482
- field,
13483
- values,
13484
- display,
13485
- filters
13486
- });
13487
- }
13488
- }
13489
- else if (filters.length === 1) {
13490
- this.queryParamsStore.updateFilter(filters[0]);
13491
- }
13492
- else {
13493
- this.clear();
13494
- }
13216
+ const agg = this.aggregation();
13217
+ this.base.applyFilters(filters, agg, () => this.clear());
13495
13218
  this.searchText.set("");
13496
13219
  this.selection.set(false);
13497
13220
  this.onApply.emit();
@@ -13504,214 +13227,118 @@ class AggregationTreeComponent {
13504
13227
  }
13505
13228
  async open(node) {
13506
13229
  const q = this.queryParamsStore.getQuery();
13507
- delete q.filters; // remove filters to get all items
13230
+ delete q.filters;
13508
13231
  const agg = await firstValueFrom(this.aggregationsService.open(q, this.aggregation(), node));
13509
13232
  node.$opened = true;
13510
13233
  this.aggregationsStore.updateAggregation(agg);
13511
13234
  }
13512
- /**
13513
- * Updates the selected state of the given item in the aggregation list.
13514
- *
13515
- * @param item - The item to be selected or deselected.
13516
- *
13517
- * This method iterates through the items in the aggregation list and updates
13518
- * the `$selected` property of the item that matches the value of the given item.
13519
- *
13520
- * If the item is selected, the selection count is incremented by 1.
13521
- * If the item is deselected, the selection count is decremented by 1.
13522
- */
13523
13235
  select() {
13524
- const selectedItems = this.getFlattenTreeItems().filter(item => item.$selected);
13236
+ const selectedItems = this.getFlattenTreeItems().filter((item) => item.$selected);
13525
13237
  this.onSelect.emit(selectedItems);
13526
- // Keep apply visible if items are selected, or if active filters exist (user may be deselecting to clear)
13527
13238
  this.selection.set(selectedItems.length > 0 || this.hasFilters());
13528
13239
  this.verifySelected();
13529
13240
  sessionStorage.setItem(`agg-${this.aggregation()?.column}`, JSON.stringify([...this.items()]));
13530
13241
  }
13531
- /**
13532
- * Updates the collapsed status on header click if the component is collapsible.
13533
- */
13534
- onHeaderClick(event) {
13535
- const isDate = this.aggregationsService.appStore.isDateColumn(this.aggregation()?.column || "");
13536
- // prevent header click if no items are present
13537
- if (!isDate && this.isEmpty()) {
13538
- event.preventDefault();
13242
+ /* item-level methods — called from the ng-template */
13243
+ treeItemName(item) {
13244
+ const value = item.display || item.value;
13245
+ return typeof value === "string" ? value : `${value}`;
13246
+ }
13247
+ isTreeItemFiltered(item, field) {
13248
+ const filters = this.queryParamsStore.getFilter({ field: field ?? undefined, name: this.treeItemName(item) });
13249
+ if (!filters)
13250
+ return false;
13251
+ const values = [item.value, `/${item.$path}/*`];
13252
+ return (values.some((v) => v === filters.value) ||
13253
+ !!(filters.values?.length && filters.values.some((value) => values.some((v) => v === value))));
13254
+ }
13255
+ treeItemLevel(item) {
13256
+ const level = (item.$level ?? 0) - 1 + (!item.hasChildren ? 1 : 0);
13257
+ return item.hasChildren === false ? level + 1 : level;
13258
+ }
13259
+ treeChildrenPath(item, parentPath) {
13260
+ return parentPath.concat(`/${item.$path}/*`);
13261
+ }
13262
+ selectTreeItem(node, parent, e, updateChildren = false) {
13263
+ e?.stopImmediatePropagation();
13264
+ const selected = !node.$selected && !node.$selectedVisually;
13265
+ node.$selected = selected;
13266
+ node.$selectedVisually = false;
13267
+ if (updateChildren)
13268
+ this.selectTreeItemChildren(node.items, node.$selected);
13269
+ if (parent)
13270
+ this.handleTreeChildSelect(parent, node);
13271
+ this.select();
13272
+ }
13273
+ toggleTreeNode(e, node) {
13274
+ e.preventDefault();
13275
+ e.stopImmediatePropagation();
13276
+ if (node.items && node.$opened) {
13277
+ node.$opened = false;
13539
13278
  return;
13540
13279
  }
13541
- if (this.collapsible()) {
13542
- this.isCollapsed.update((value) => !value);
13280
+ if (node.items && !node.$opened) {
13281
+ node.$opened = true;
13282
+ return;
13283
+ }
13284
+ this.open(node);
13285
+ }
13286
+ onTreeItemTextClick(node, parent, event) {
13287
+ if (this.quickFilter()) {
13288
+ this.selectTreeItem(node, parent, event, true);
13289
+ this.apply();
13543
13290
  }
13544
- event.preventDefault();
13545
13291
  }
13546
- /**
13547
- * Retrieves the appropriate filters based on the aggregation type.
13548
- *
13549
- * If the aggregation is a tree structure, it returns filters specific to trees.
13550
- * Otherwise, it returns filters for a list structure.
13551
- *
13552
- * @returns Filters for either a tree or list aggregation.
13553
- */
13554
13292
  getFilters() {
13555
13293
  if (this.aggregation()?.isTree) {
13556
13294
  return this.getFiltersForTree();
13557
13295
  }
13558
13296
  return this.getFiltersForList();
13559
13297
  }
13560
- /**
13561
- * Retrieves the filters for the tree structure.
13562
- *
13563
- * This method collects the selected items from the tree, constructs their paths,
13564
- * and creates a filter object based on these paths. If no items are selected,
13565
- * it returns an empty array.
13566
- *
13567
- * @returns {LegacyFilter[]} An array of filters for the tree structure.
13568
- */
13569
13298
  getFiltersForTree() {
13570
13299
  const { name, column: field } = this.aggregation() || {};
13571
- if (!name || !field) {
13300
+ if (!name || !field)
13572
13301
  return [];
13573
- }
13574
13302
  const items = this.getFlattenTreeItems()
13575
13303
  .filter((item) => item.$selected)
13576
- .map((item) => (item.$path ? `/${item.$path}/*` : ""));
13577
- const notSelectedItems = this.getFlattenTreeItems()
13578
- .filter((item) => !item.$selected)
13579
- .map((item) => (item.$path ? `/${item.$path}/*` : ""));
13580
- // add the current filters to the current items
13581
- const currentFilters = this.queryParamsStore.getFilter({ field, name })?.values || [];
13582
- // if there are current filters, we need to add them to the current items
13583
- if (currentFilters) {
13584
- // remove filters that are not selected
13585
- const filteredCurrentFilters = currentFilters.filter((filter) => !notSelectedItems.includes(filter));
13586
- // if the current filters are not selected, we need to add them to the items
13587
- if (filteredCurrentFilters.length > 0) {
13588
- // if the current filters are not selected, we need to add them to the items
13589
- items.push(...filteredCurrentFilters);
13590
- }
13591
- }
13592
- // remove duplicates
13593
- const uniqueItems = Array.from(new Set(items));
13594
- // if no items are selected, return an empty array
13595
- if (uniqueItems.length === 0)
13596
- return [];
13597
- const filter = { operator: "in", name, field, values: uniqueItems, display: uniqueItems[0] };
13598
- return [filter];
13599
- }
13600
- /**
13601
- * Retrieves a list of filters based on the selected items.
13602
- *
13603
- * This method filters the items to include only those that are selected,
13604
- * and then maps each selected item to a filter using the `toFilter` method.
13605
- *
13606
- * @returns {LegacyFilter[]} An array of filters corresponding to the selected items.
13607
- */
13608
- getFiltersForList() {
13609
- const items = this.addCurrentFiltersToItems().filter((item) => item.$selected); // this.items().filter(item => item.$selected) || [];
13610
- const searchedItems = this.searchedItems().filter((item) => item.$selected);
13611
- const currentItems = [...items, ...searchedItems];
13612
- const { column, name, isDistribution = false } = this.aggregation() || {};
13613
- const selectedItems = currentItems.map((item) => this.aggregationsService.toFilter(item, column, name, isDistribution));
13614
- return selectedItems;
13615
- }
13616
- /**
13617
- * Recursively flattens a tree structure of `TreeAggregationNode` items into a single array.
13618
- *
13619
- * @returns {TreeAggregationNode[]} An array containing all nodes from the tree structure, flattened.
13620
- */
13621
- getFlattenTreeItems() {
13622
- const flattenItems = (items) => {
13623
- return items.reduce((flat, item) => {
13624
- return flat.concat(item, item.items ? flattenItems(item.items) : []);
13625
- }, []);
13626
- };
13627
- // we need to flatten both the searched items and the current items to get all the items in the tree
13628
- const searchedItemsFiltered = (this.searchedItems() || []).filter((item) => "items" in item);
13629
- const searchItems = flattenItems(searchedItemsFiltered);
13630
- const items = flattenItems(this.aggregation()?.items || []);
13631
- const flattenedTreeItems = [...searchItems, ...items];
13632
- return flattenedTreeItems;
13633
- }
13634
- addCurrentFiltersToItems() {
13635
- const currentItems = this.aggregation()?.items || [];
13636
- // add the current filters to the current items only if they are not already present
13637
- if (!this.aggregation()?.isTree &&
13638
- (!this.aggregation()?.isDistribution || this.aggregation()?.isDistribution === false)) {
13639
- // get the current filters for the current aggregation
13640
- const currentFilters = this.queryParamsStore.getFilter({
13641
- field: this.aggregation()?.column,
13642
- name: this.aggregation()?.name
13643
- });
13644
- // if there are current filters, we need to add them to the current items
13645
- if (currentFilters) {
13646
- // multiples filters
13647
- if (currentFilters.filters) {
13648
- currentFilters.filters.forEach((filter) => {
13649
- // check if the filter is already present in the current items
13650
- // if not, add it to the current items
13651
- const found = currentItems.find((item) => item.value.toLocaleLowerCase() === filter.value?.toLocaleLowerCase());
13652
- if (!found) {
13653
- // add it to the current items
13654
- currentItems.unshift({
13655
- value: filter.value,
13656
- display: filter.display,
13657
- $selected: true
13658
- });
13659
- }
13660
- else {
13661
- // mark as selected the existing item
13662
- found.$selected = true;
13663
- }
13664
- });
13665
- }
13666
- else {
13667
- // single filter
13668
- const found = currentItems.find((item) => item.value.toLocaleLowerCase() === currentFilters.value?.toLocaleLowerCase());
13669
- if (!found) {
13670
- // add it to the current items
13671
- currentItems.push({
13672
- value: currentFilters.value,
13673
- display: currentFilters.display,
13674
- $selected: true
13675
- });
13676
- }
13677
- else {
13678
- // mark as selected the existing item
13679
- found.$selected = true;
13680
- }
13681
- }
13304
+ .map((item) => (item.$path ? `/${item.$path}/*` : ""));
13305
+ const notSelectedItems = this.getFlattenTreeItems()
13306
+ .filter((item) => !item.$selected)
13307
+ .map((item) => (item.$path ? `/${item.$path}/*` : ""));
13308
+ const currentFilters = this.queryParamsStore.getFilter({ field, name })?.values || [];
13309
+ if (currentFilters) {
13310
+ const filteredCurrentFilters = currentFilters.filter((filter) => !notSelectedItems.includes(filter));
13311
+ if (filteredCurrentFilters.length > 0) {
13312
+ items.push(...filteredCurrentFilters);
13682
13313
  }
13683
13314
  }
13684
- return currentItems;
13315
+ const uniqueItems = Array.from(new Set(items));
13316
+ if (uniqueItems.length === 0)
13317
+ return [];
13318
+ return [{ operator: "in", name, field, values: uniqueItems, display: uniqueItems[0] }];
13685
13319
  }
13686
- /**
13687
- * Update the $selected property to the selected parameter to all items
13688
- *
13689
- * @param items the items to apply to
13690
- * @param selected the selected status
13691
- */
13692
- selectItems(items, selected) {
13693
- items.forEach((item) => {
13694
- if (item.count > 0) {
13695
- // don't select disabled items
13696
- item.$selected = selected;
13697
- }
13698
- if (item.items?.length) {
13699
- this.selectItems(item.items, selected);
13700
- }
13701
- });
13320
+ getFiltersForList() {
13321
+ const items = this.addCurrentFiltersToItems().filter((item) => item.$selected);
13322
+ const searchedItems = this.searchedItems().filter((item) => item.$selected);
13323
+ const currentItems = [...items, ...searchedItems];
13324
+ const { column, name, isDistribution = false } = this.aggregation() || {};
13325
+ return currentItems.map((item) => this.aggregationsService.toFilter(item, column, name, isDistribution));
13326
+ }
13327
+ getFlattenTreeItems() {
13328
+ const flattenItems = (items) => items.reduce((flat, item) => flat.concat(item, item.items ? flattenItems(item.items) : []), []);
13329
+ const searchedItemsFiltered = (this.searchedItems() || []).filter((item) => "items" in item);
13330
+ return [
13331
+ ...flattenItems(searchedItemsFiltered),
13332
+ ...flattenItems(this.aggregation()?.items || []),
13333
+ ];
13334
+ }
13335
+ addCurrentFiltersToItems() {
13336
+ return this.base.addCurrentFiltersToItems(this.aggregation());
13702
13337
  }
13703
- /**
13704
- * Check whether all items are selected and update isAllSelected accordingly
13705
- */
13706
13338
  verifySelected() {
13707
- const someItemsUnselected = (items) => {
13708
- return items.some((item) => !item.$selected || (item.items?.length && someItemsUnselected(item.items)));
13709
- };
13339
+ const someItemsUnselected = (items) => items.some((item) => !item.$selected || (item.items?.length && someItemsUnselected(item.items)));
13710
13340
  this.isAllSelected.set(!someItemsUnselected(this.items()));
13711
13341
  }
13712
- /**
13713
- * set @items $selected and $selectedVisually to the values from @savedItems
13714
- */
13715
13342
  setSelected(items, savedItems) {
13716
13343
  return items.map((item) => {
13717
13344
  const savedItem = savedItems.find((i) => i.value === item.value);
@@ -13729,9 +13356,8 @@ class AggregationTreeComponent {
13729
13356
  if (!this.linkChildren())
13730
13357
  return items;
13731
13358
  items.forEach((item) => {
13732
- if (item.items?.length) {
13359
+ if (item.items?.length)
13733
13360
  this.selectVisually(item.items, item.$selected || false);
13734
- }
13735
13361
  });
13736
13362
  return items;
13737
13363
  }
@@ -13742,39 +13368,49 @@ class AggregationTreeComponent {
13742
13368
  this.selectVisually(item.items, item.$selected || item.$selectedVisually);
13743
13369
  });
13744
13370
  }
13745
- onToggle(event) {
13746
- const e = event;
13747
- this.isCollapsed.set(e.newState === "closed");
13371
+ selectTreeItemChildren(items, select) {
13372
+ if (!this.linkChildren() || !items?.length)
13373
+ return;
13374
+ items.forEach((item) => {
13375
+ item.$selectedVisually = select;
13376
+ if (select)
13377
+ item.$selected = false;
13378
+ if (item.items?.length)
13379
+ this.selectTreeItemChildren(item.items, select);
13380
+ });
13381
+ }
13382
+ handleTreeChildSelect(parent, child) {
13383
+ if (this.linkChildren() && !child.$selected && parent.items.some((i) => i.$selectedVisually)) {
13384
+ parent.items.forEach((i) => {
13385
+ if (i !== child) {
13386
+ i.$selectedVisually = false;
13387
+ i.$selected = true;
13388
+ }
13389
+ });
13390
+ }
13391
+ if (this.linkChildren() && parent.items.some((i) => !i.$selectedVisually && !i.$selected)) {
13392
+ parent.$selected = false;
13393
+ parent.$selectedVisually = false;
13394
+ }
13748
13395
  }
13749
13396
  expandItems(items, expandedLevel) {
13750
- this.traverse(items, (lineage, node, level) => {
13751
- if (!node.$opened && node.items?.length >= 0 && level < expandedLevel) {
13397
+ this.traverse(items, (_lineage, node, level) => {
13398
+ if (!node.$opened && node.items?.length > 0 && level < expandedLevel) {
13752
13399
  node.$opened = true;
13753
13400
  }
13754
- // if(node.$opened && level >= expandedLevel) {
13755
- // node.$opened = false;
13756
- // }
13757
13401
  return false;
13758
13402
  });
13759
13403
  }
13760
- /**
13761
- * Traverses a tree structure, executing a callback function at every node
13762
- * @param nodes the nodes to traverse
13763
- * @param callback the callback function
13764
- */
13765
13404
  traverse(nodes, callback) {
13766
- if (!nodes || nodes.length === 0) {
13405
+ if (!nodes || nodes.length === 0)
13767
13406
  return false;
13768
- }
13769
- if (!callback) {
13407
+ if (!callback)
13770
13408
  return false;
13771
- }
13772
13409
  const lineage = [];
13773
13410
  const stack = [];
13774
13411
  let _i = nodes.length;
13775
- while (_i--) {
13412
+ while (_i--)
13776
13413
  stack.push(nodes[_i]);
13777
- }
13778
13414
  while (stack.length) {
13779
13415
  const node = stack.pop();
13780
13416
  if (!node) {
@@ -13782,54 +13418,248 @@ class AggregationTreeComponent {
13782
13418
  }
13783
13419
  else {
13784
13420
  lineage.push(node);
13785
- if (callback(lineage, node, lineage.length - 1)) {
13421
+ if (callback(lineage, node, lineage.length - 1))
13786
13422
  return true;
13787
- }
13788
13423
  stack.push(undefined);
13789
13424
  if (node.items && node.items.length > 0) {
13790
13425
  _i = node.items.length;
13791
- while (_i--) {
13426
+ while (_i--)
13792
13427
  stack.push(node.items[_i]);
13793
- }
13794
13428
  }
13795
13429
  }
13796
13430
  }
13797
13431
  return false;
13798
13432
  }
13799
- clearSearch(e) {
13800
- e.stopImmediatePropagation();
13801
- this.searchText.set("");
13802
- }
13803
13433
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AggregationTreeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
13804
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: AggregationTreeComponent, isStandalone: true, selector: "AggregationTree, aggregation-tree, aggregationtree", inputs: { class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null }, column: { classPropertyName: "column", publicName: "column", isSignal: true, isRequired: true, transformFunction: null }, expandedLevel: { classPropertyName: "expandedLevel", publicName: "expandedLevel", isSignal: true, isRequired: false, transformFunction: null }, collapsible: { classPropertyName: "collapsible", publicName: "collapsible", isSignal: true, isRequired: false, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, searchable: { classPropertyName: "searchable", publicName: "searchable", isSignal: true, isRequired: false, transformFunction: null }, showFiltersCount: { classPropertyName: "showFiltersCount", publicName: "showFiltersCount", isSignal: true, isRequired: false, transformFunction: null }, searchText: { classPropertyName: "searchText", publicName: "searchText", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSelect: "onSelect", onApply: "onApply", onClear: "onClear", searchText: "searchTextChange" }, host: { properties: { "class": "cn(\"block h-[inherit] max-h-[inherit] w-[inherit]\",class())" } }, viewQueries: [{ propertyName: "virtualItems", predicate: ["virtualItem"], descendants: true, isSignal: true }, { propertyName: "scrollElement", first: true, predicate: ["scrollElement"], descendants: true, isSignal: true }, { propertyName: "searchInput", first: true, predicate: ["searchInput"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (!aggregation()?.isTree) {\r\n <div class=\"p-2 text-sm text-red-500\">\r\n <triangle-alert-icon class=\"mr-1\" />\r\n The aggregationTree component does not support list aggregations. Please use\r\n the &lt;Aggregation /&gt; component instead.\r\n </div>\r\n}\r\n<details\r\n [attr.open]=\"expanded()\"\r\n [attr.name]=\"id()\"\r\n class=\"group space-y-2\"\r\n (toggle)=\"onToggle($event)\">\r\n <summary\r\n [class.cursor-pointer]=\"collapsible() && !isEmpty()\"\r\n [class.text-muted-foreground]=\"isEmpty()\"\r\n class=\"m-0 mt-1 flex h-8 w-full items-center gap-1 pl-1 font-semibold select-none\"\r\n (click)=\"onHeaderClick($event)\">\r\n <ng-content select=\"label\">\r\n @let icon = aggregation()?.icon;\r\n @if (icon) {\r\n <fa-icon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\r\n }\r\n <span class=\"grow truncate\">{{\r\n aggregation()?.display | syslang | transloco\r\n }}</span>\r\n </ng-content>\r\n\r\n @if (showFiltersCount() && filtersCount() > 0) {\r\n <!-- count -->\r\n <Badge size=\"xs\" class=\"ml-1\">\r\n {{ filtersCount() }}\r\n </Badge>\r\n }\r\n <!-- apply filter block -->\r\n @if (!isCollapsed()) {\r\n @if (hasFilters()) {\r\n @let label = \"filters.clearFilters\" | transloco;\r\n <button\r\n variant=\"none\"\r\n icon-button\r\n [aria-label]=\"label\"\r\n (click)=\"$event.stopPropagation(); clear()\">\r\n <filter-x-icon />\r\n </button>\r\n }\r\n @if (selection()) {\r\n @let label = \"filters.apply\" | transloco;\r\n <button\r\n variant=\"accent\"\r\n size=\"sm\"\r\n [aria-label]=\"label\"\r\n (click)=\"$event.stopPropagation(); apply()\">\r\n <FilterIcon />\r\n {{ label }}\r\n </button>\r\n }\r\n\r\n <!-- select / unselect all -->\r\n @if (isAllSelected()) {\r\n @let label = \"filters.unselectAllFilters\" | transloco;\r\n <button\r\n variant=\"none\"\r\n icon-button\r\n [aria-label]=\"label\"\r\n (click)=\"$event.stopPropagation(); unselectAll()\">\r\n <square-check-icon />\r\n </button>\r\n } @else {\r\n @let label = \"filters.selectAllFilters\" | transloco;\r\n <button\r\n variant=\"none\"\r\n icon-button\r\n [aria-label]=\"label\"\r\n (click)=\"$event.stopPropagation(); selectAll()\">\r\n <square-icon />\r\n </button>\r\n }\r\n }\r\n\r\n @if (collapsible()) {\r\n <icon-button\r\n title=\"Open/Close\"\r\n class=\"cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\r\n <chevronright />\r\n <span class=\"sr-only\">{{ \"filters.toggle\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n </summary>\r\n\r\n <!-- content wrapper -->\r\n @if (aggregation()?.searchable && items().length) {\r\n <InputGroup class=\"group/item mt-1\">\r\n <input\r\n #searchInput\r\n input-group\r\n id=\"aggregation-input-{{ column() }}\"\r\n type=\"text\"\r\n [attr.placeholder]=\"'search' | transloco\"\r\n [(ngModel)]=\"searchText\"\r\n class=\"mt-1\" />\r\n <InputGroupAddon>\r\n <SearchIcon\r\n class=\"text-foreground size-4 rotate-0 transition-[rotate] duration-500 group-focus-within/item:rotate-90\" />\r\n </InputGroupAddon>\r\n <InputGroupAddon align=\"inline-end\" class=\"gap-0.5!\">\r\n <icon-button\r\n size=\"sm\"\r\n [class]=\"\r\n searchText().length > 0\r\n ? 'rotate-90 cursor-pointer opacity-100 transition-[rotate,opacity] duration-500'\r\n : 'pointer-events-none rotate-0 opacity-0 transition-[rotate,opacity] duration-500'\r\n \"\r\n aria-label=\"Clear search\"\r\n [tabindex]=\"searchText().length > 0 ? 0 : -1\"\r\n (keydown.enter)=\"clearSearch($event)\"\r\n (click)=\"clearSearch($event)\">\r\n <XMarkIcon />\r\n </icon-button>\r\n <ng-content />\r\n </InputGroupAddon>\r\n </InputGroup>\r\n }\r\n\r\n <div\r\n #scrollElement\r\n class=\"scrollbar-thin max-h-[calc(var(--height,100%)-100px)] w-full overflow-auto\">\r\n <div\r\n class=\"relative w-full\"\r\n [style.height]=\"virtualizer.getTotalSize() + 'px'\"\r\n role=\"list\"\r\n [attr.aria-label]=\"aggregation()?.display | syslang | transloco\">\r\n <div\r\n class=\"absolute top-0 left-0 w-full\"\r\n [style.transform]=\"\r\n 'translateY(' +\r\n (virtualizer.getVirtualItems()[0]\r\n ? virtualizer.getVirtualItems()[0].start\r\n : 0) +\r\n 'px)'\r\n \"\r\n role=\"listitem\">\r\n @for (vItem of virtualizer.getVirtualItems(); track vItem.index) {\r\n @let item = items()[vItem.index];\r\n <div #virtualItem [attr.data-index]=\"vItem.index\">\r\n <AggregationTreeItem\r\n [node]=\"item\"\r\n [path]=\"[]\"\r\n [field]=\"aggregation()?.column\"\r\n (onSelect)=\"select()\"\r\n (onOpen)=\"open($event)\"\r\n (onFilter)=\"apply()\" />\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n @if (aggregation()?.$hasMore && this.searchedItems().length === 0) {\r\n <button\r\n class=\"mt-1 flex w-full justify-center\"\r\n [attr.aria-label]=\"'loadMore' | transloco\"\r\n (click)=\"loadMore()\">\r\n {{ \"loadMore\" | transloco }}\r\n </button>\r\n }\r\n</details>\r\n", styles: ["AggregationTreeItem:has(+AggregationTreeItem){margin-bottom:var(--agg-item-gap, 0)}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { 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"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: ButtonComponent, selector: "button", inputs: ["class", "variant", "decoration", "scheme", "iconOnly", "size", "solid"] }, { kind: "component", type: AggregationTreeItemComponent, selector: "aggregation-tree-item, AggregationTreeItem, aggregationtreeitem", inputs: ["node", "path", "field"], outputs: ["onSelect", "onOpen", "onFilter"] }, { kind: "directive", type: BadgeComponent, selector: "badge, Badge", inputs: ["class", "variant", "scheme", "size"] }, { kind: "component", type: ChevronRightIcon, selector: "chevron-right, ChevronRight, chevronright, ChevronRightIcon, chevron-right-icon, chevronrighticon", inputs: ["class"] }, { kind: "directive", type: InputGroupInput, selector: "input[input-group]", inputs: ["class", "type", "placeholder", "disabled"] }, { kind: "directive", type: InputGroupComponent, selector: "input-group, inputgroup, InputGroup", inputs: ["class"] }, { kind: "directive", type: InputGroupAddonComponent, selector: "input-group-addon, inputgroupaddon, InputGroupAddon", inputs: ["class", "align"] }, { kind: "component", type: SearchIcon, selector: "SearchIcon", inputs: ["class"] }, { kind: "component", type: FilterIcon, selector: "filter-icon, FilterIcon", inputs: ["class"] }, { kind: "component", type: FaIconComponent, selector: "fa-icon, FaIcon", inputs: ["faClass", "class"] }, { kind: "component", type: TriangleAlertIcon, selector: "triangle-alert-icon, TriangleAlertIcon", inputs: ["class"] }, { kind: "component", type: FilterXIcon, selector: "filter-x-icon, FilterXIcon", inputs: ["class"] }, { kind: "component", type: SquareCheckIcon, selector: "square-check-icon, SquareCheckIcon", inputs: ["class"] }, { kind: "component", type: SquareIcon, selector: "square-icon, SquareIcon", inputs: ["class"] }, { kind: "component", type: XMarkIcon, selector: "XMarkIcon, xmark-icon, x-mark-icon", inputs: ["class"] }, { kind: "directive", type: IconButtonComponent, selector: "button[icon-button], icon-button, IconButton", inputs: ["class", "size"] }, { kind: "pipe", type: SyslangPipe, name: "syslang" }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
13434
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: AggregationTreeComponent, isStandalone: true, selector: "AggregationTree, aggregation-tree, aggregationtree", inputs: { class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null }, column: { classPropertyName: "column", publicName: "column", isSignal: true, isRequired: true, transformFunction: null }, collapsible: { classPropertyName: "collapsible", publicName: "collapsible", isSignal: true, isRequired: false, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, searchable: { classPropertyName: "searchable", publicName: "searchable", isSignal: true, isRequired: false, transformFunction: null }, showFiltersCount: { classPropertyName: "showFiltersCount", publicName: "showFiltersCount", isSignal: true, isRequired: false, transformFunction: null }, expandedLevel: { classPropertyName: "expandedLevel", publicName: "expandedLevel", isSignal: true, isRequired: false, transformFunction: null }, searchText: { classPropertyName: "searchText", publicName: "searchText", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSelect: "onSelect", onApply: "onApply", onClear: "onClear", searchText: "searchTextChange" }, host: { properties: { "class": "cn(\"block h-[inherit] max-h-[inherit] w-[inherit]\", class())" } }, viewQueries: [{ propertyName: "virtualItems", predicate: ["virtualItem"], descendants: true, isSignal: true }, { propertyName: "scrollElement", first: true, predicate: ["scrollElement"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (!aggregation()?.isTree) {\r\n <div class=\"p-2 text-sm text-red-500\">\r\n <triangle-alert-icon class=\"mr-1\" />\r\n The aggregationTree component does not support list aggregations. Please use\r\n the &lt;Aggregation /&gt; component instead.\r\n </div>\r\n}\r\n<aggregation-panel\r\n [id]=\"id()\"\r\n [collapsible]=\"collapsible()\"\r\n [collapsed]=\"collapsed()\"\r\n [isEmpty]=\"isEmpty()\"\r\n [isDate]=\"isDate()\"\r\n [aggregation]=\"aggregation()\"\r\n [showFiltersCount]=\"showFiltersCount()\"\r\n [filtersCount]=\"filtersCount()\"\r\n [hasFilters]=\"hasFilters()\"\r\n [selection]=\"selection()\"\r\n [isAllSelected]=\"isAllSelected()\"\r\n [(searchText)]=\"searchText\"\r\n [itemsLength]=\"items().length\"\r\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\r\n [searchedItemsLength]=\"searchedItems().length\"\r\n (cleared)=\"clear()\"\r\n (applied)=\"apply()\"\r\n (allSelected)=\"selectAll()\"\r\n (allUnselected)=\"unselectAll()\"\r\n (loadedMore)=\"loadMore()\">\r\n <div\r\n #scrollElement\r\n class=\"scrollbar-thin max-h-(--scroll-height,20rem) w-full overflow-auto\">\r\n <div\r\n class=\"relative w-full\"\r\n [style.height]=\"virtualizer.getTotalSize() + 'px'\"\r\n role=\"tree\"\r\n [attr.aria-label]=\"aggregation()?.display | syslang | transloco\">\r\n <div\r\n class=\"absolute top-0 left-0 w-full\"\r\n [style.transform]=\"\r\n 'translateY(' +\r\n (virtualizer.getVirtualItems()[0]\r\n ? virtualizer.getVirtualItems()[0].start\r\n : 0) +\r\n 'px)'\r\n \">\r\n @for (vItem of virtualizer.getVirtualItems(); track vItem.index) {\r\n @let item = items()[vItem.index];\r\n <div #virtualItem [attr.data-index]=\"vItem.index\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"treeItemTpl\"\r\n [ngTemplateOutletContext]=\"{\r\n node: item,\r\n path: [],\r\n field: aggregation()?.column,\r\n tpl: treeItemTpl,\r\n parent: null\r\n }\">\r\n </ng-container>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n</aggregation-panel>\r\n\r\n<ng-template\r\n #treeItemTpl\r\n let-node=\"node\"\r\n let-path=\"path\"\r\n let-field=\"field\"\r\n let-tpl=\"tpl\"\r\n let-parent=\"parent\">\r\n <div\r\n role=\"treeitem\"\r\n [style.--level]=\"treeItemLevel(node)\"\r\n [attr.aria-selected]=\"node.$selected ?? false\"\r\n [attr.aria-expanded]=\"\r\n node.hasChildren !== false ? (node.$opened ?? false) : null\r\n \"\r\n [attr.aria-disabled]=\"node.count === 0 ? 'true' : null\"\r\n [attr.disabled]=\"node.count === 0 ? 'disabled' : null\">\r\n <a\r\n [attr.aria-label]=\"treeItemName(node) | syslang\"\r\n [class]=\"\r\n cn(\r\n 'flex grow items-center gap-2 p-1 leading-7',\r\n node.count === 0 && 'disabled pointer-events-none',\r\n (node.$selected || node.$selectedVisually) && ''\r\n )\r\n \"\r\n (click)=\"selectTreeItem(node, parent, $event, true)\">\r\n <button\r\n variant=\"none\"\r\n [iconOnly]=\"true\"\r\n (click)=\"toggleTreeNode($event, node)\"\r\n class=\"transition-transform ease-in hover:scale-125\"\r\n aria-label=\"Open\">\r\n <ChevronRight\r\n [class]=\"\r\n cn(\r\n 'size-4 translate-x-1',\r\n node.$opened && 'rotate-90',\r\n !node.hasChildren && 'hidden'\r\n )\r\n \"\r\n width=\"16\"\r\n height=\"16\" />\r\n </button>\r\n <input\r\n type=\"checkbox\"\r\n role=\"checkbox\"\r\n value=\"{{ node.value }}\"\r\n [attr.disabled]=\"node.count === 0 ? true : null\"\r\n [attr.aria-disabled]=\"node.count === 0\"\r\n (keydown.enter)=\"selectTreeItem(node, parent, $event)\"\r\n [checked]=\"node.$selected || node.$selectedVisually\" />\r\n @let icon = node.icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"self-center justify-self-center\" />\r\n }\r\n <span\r\n [class]=\"\r\n cn(\r\n 'line-clamp-1 break-all text-ellipsis',\r\n quickFilter() && 'hover:underline'\r\n )\r\n \"\r\n [title]=\"\r\n quickFilter()\r\n ? ((isTreeItemFiltered(node, field)\r\n ? 'filters.removeFilter'\r\n : 'filters.addFilter'\r\n ) | transloco) +\r\n ': ' +\r\n (treeItemName(node) | syslang)\r\n : (treeItemName(node) | syslang)\r\n \"\r\n (click)=\"onTreeItemTextClick(node, parent, $event)\">\r\n @for (\r\n chunk of (treeItemName(node) | syslang) ?? \"\"\r\n | highlightWord: searchText() : 10;\r\n track $index\r\n ) {\r\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\r\n chunk.text\r\n }}</span>\r\n }\r\n </span>\r\n @if (showCount() && node.count > 0) {\r\n <span class=\"ml-auto px-1 text-xs empty:hidden\" aria-hidden=\"true\">{{\r\n node.count\r\n }}</span>\r\n }\r\n </a>\r\n @if (node.hasChildren && node.$opened) {\r\n <div role=\"group\">\r\n @for (child of node.items; track $index) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"tpl\"\r\n [ngTemplateOutletContext]=\"{\r\n node: child,\r\n path: treeChildrenPath(node, path),\r\n field: field,\r\n tpl: tpl,\r\n parent: node\r\n }\">\r\n </ng-container>\r\n }\r\n </div>\r\n }\r\n </div>\r\n</ng-template>\r\n", styles: ["div[role=treeitem]:has(+div[role=treeitem]){margin-bottom:var(--agg-item-gap, 0)}div[role=treeitem]{display:block;-webkit-user-select:none;user-select:none}div[role=treeitem] a{padding-left:calc(var(--agg-tree-indent, .5rem) * var(--level, 0));line-height:var(--agg-item-height, inherit)}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: ButtonComponent, selector: "button", inputs: ["class", "variant", "decoration", "scheme", "iconOnly", "size", "solid"] }, { kind: "component", type: ChevronRightIcon, selector: "chevron-right, ChevronRight, chevronright, ChevronRightIcon, chevron-right-icon, chevronrighticon", inputs: ["class"] }, { kind: "component", type: FaIconComponent, selector: "fa-icon, FaIcon", inputs: ["faClass", "class"] }, { kind: "component", type: TriangleAlertIcon, selector: "triangle-alert-icon, TriangleAlertIcon", inputs: ["class"] }, { kind: "component", type: AggregationPanelComponent, selector: "AggregationPanel, aggregation-panel", inputs: ["id", "collapsible", "collapsed", "isDate", "isEmpty", "aggregation", "showFiltersCount", "filtersCount", "hasFilters", "selection", "isAllSelected", "searchText", "itemsLength", "hasMore", "searchedItemsLength"], outputs: ["searchTextChange", "cleared", "applied", "allSelected", "allUnselected", "loadedMore"] }, { kind: "pipe", type: SyslangPipe, name: "syslang" }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }, { kind: "pipe", type: HighlightWordPipe, name: "highlightWord" }] });
13805
13435
  }
13806
13436
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AggregationTreeComponent, decorators: [{
13807
13437
  type: Component,
13808
13438
  args: [{ selector: "AggregationTree, aggregation-tree, aggregationtree", imports: [
13439
+ NgTemplateOutlet,
13809
13440
  FormsModule,
13810
13441
  ReactiveFormsModule,
13811
13442
  ButtonComponent,
13812
- AggregationTreeItemComponent,
13443
+ ChevronRightIcon,
13813
13444
  SyslangPipe,
13814
13445
  TranslocoPipe,
13815
- BadgeComponent,
13816
- ChevronRightIcon,
13817
- InputGroupInput,
13818
- InputGroupComponent,
13819
- InputGroupAddonComponent,
13820
- SearchIcon,
13821
- FilterIcon,
13822
13446
  FaIconComponent,
13823
13447
  TriangleAlertIcon,
13824
- FilterXIcon,
13825
- SquareCheckIcon,
13826
- SquareIcon,
13827
- XMarkIcon,
13828
- IconButtonComponent
13448
+ HighlightWordPipe,
13449
+ AggregationPanelComponent,
13450
+ ], standalone: true, host: {
13451
+ "[class]": 'cn("block h-[inherit] max-h-[inherit] w-[inherit]", class())',
13452
+ }, template: "@if (!aggregation()?.isTree) {\r\n <div class=\"p-2 text-sm text-red-500\">\r\n <triangle-alert-icon class=\"mr-1\" />\r\n The aggregationTree component does not support list aggregations. Please use\r\n the &lt;Aggregation /&gt; component instead.\r\n </div>\r\n}\r\n<aggregation-panel\r\n [id]=\"id()\"\r\n [collapsible]=\"collapsible()\"\r\n [collapsed]=\"collapsed()\"\r\n [isEmpty]=\"isEmpty()\"\r\n [isDate]=\"isDate()\"\r\n [aggregation]=\"aggregation()\"\r\n [showFiltersCount]=\"showFiltersCount()\"\r\n [filtersCount]=\"filtersCount()\"\r\n [hasFilters]=\"hasFilters()\"\r\n [selection]=\"selection()\"\r\n [isAllSelected]=\"isAllSelected()\"\r\n [(searchText)]=\"searchText\"\r\n [itemsLength]=\"items().length\"\r\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\r\n [searchedItemsLength]=\"searchedItems().length\"\r\n (cleared)=\"clear()\"\r\n (applied)=\"apply()\"\r\n (allSelected)=\"selectAll()\"\r\n (allUnselected)=\"unselectAll()\"\r\n (loadedMore)=\"loadMore()\">\r\n <div\r\n #scrollElement\r\n class=\"scrollbar-thin max-h-(--scroll-height,20rem) w-full overflow-auto\">\r\n <div\r\n class=\"relative w-full\"\r\n [style.height]=\"virtualizer.getTotalSize() + 'px'\"\r\n role=\"tree\"\r\n [attr.aria-label]=\"aggregation()?.display | syslang | transloco\">\r\n <div\r\n class=\"absolute top-0 left-0 w-full\"\r\n [style.transform]=\"\r\n 'translateY(' +\r\n (virtualizer.getVirtualItems()[0]\r\n ? virtualizer.getVirtualItems()[0].start\r\n : 0) +\r\n 'px)'\r\n \">\r\n @for (vItem of virtualizer.getVirtualItems(); track vItem.index) {\r\n @let item = items()[vItem.index];\r\n <div #virtualItem [attr.data-index]=\"vItem.index\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"treeItemTpl\"\r\n [ngTemplateOutletContext]=\"{\r\n node: item,\r\n path: [],\r\n field: aggregation()?.column,\r\n tpl: treeItemTpl,\r\n parent: null\r\n }\">\r\n </ng-container>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n</aggregation-panel>\r\n\r\n<ng-template\r\n #treeItemTpl\r\n let-node=\"node\"\r\n let-path=\"path\"\r\n let-field=\"field\"\r\n let-tpl=\"tpl\"\r\n let-parent=\"parent\">\r\n <div\r\n role=\"treeitem\"\r\n [style.--level]=\"treeItemLevel(node)\"\r\n [attr.aria-selected]=\"node.$selected ?? false\"\r\n [attr.aria-expanded]=\"\r\n node.hasChildren !== false ? (node.$opened ?? false) : null\r\n \"\r\n [attr.aria-disabled]=\"node.count === 0 ? 'true' : null\"\r\n [attr.disabled]=\"node.count === 0 ? 'disabled' : null\">\r\n <a\r\n [attr.aria-label]=\"treeItemName(node) | syslang\"\r\n [class]=\"\r\n cn(\r\n 'flex grow items-center gap-2 p-1 leading-7',\r\n node.count === 0 && 'disabled pointer-events-none',\r\n (node.$selected || node.$selectedVisually) && ''\r\n )\r\n \"\r\n (click)=\"selectTreeItem(node, parent, $event, true)\">\r\n <button\r\n variant=\"none\"\r\n [iconOnly]=\"true\"\r\n (click)=\"toggleTreeNode($event, node)\"\r\n class=\"transition-transform ease-in hover:scale-125\"\r\n aria-label=\"Open\">\r\n <ChevronRight\r\n [class]=\"\r\n cn(\r\n 'size-4 translate-x-1',\r\n node.$opened && 'rotate-90',\r\n !node.hasChildren && 'hidden'\r\n )\r\n \"\r\n width=\"16\"\r\n height=\"16\" />\r\n </button>\r\n <input\r\n type=\"checkbox\"\r\n role=\"checkbox\"\r\n value=\"{{ node.value }}\"\r\n [attr.disabled]=\"node.count === 0 ? true : null\"\r\n [attr.aria-disabled]=\"node.count === 0\"\r\n (keydown.enter)=\"selectTreeItem(node, parent, $event)\"\r\n [checked]=\"node.$selected || node.$selectedVisually\" />\r\n @let icon = node.icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"self-center justify-self-center\" />\r\n }\r\n <span\r\n [class]=\"\r\n cn(\r\n 'line-clamp-1 break-all text-ellipsis',\r\n quickFilter() && 'hover:underline'\r\n )\r\n \"\r\n [title]=\"\r\n quickFilter()\r\n ? ((isTreeItemFiltered(node, field)\r\n ? 'filters.removeFilter'\r\n : 'filters.addFilter'\r\n ) | transloco) +\r\n ': ' +\r\n (treeItemName(node) | syslang)\r\n : (treeItemName(node) | syslang)\r\n \"\r\n (click)=\"onTreeItemTextClick(node, parent, $event)\">\r\n @for (\r\n chunk of (treeItemName(node) | syslang) ?? \"\"\r\n | highlightWord: searchText() : 10;\r\n track $index\r\n ) {\r\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\r\n chunk.text\r\n }}</span>\r\n }\r\n </span>\r\n @if (showCount() && node.count > 0) {\r\n <span class=\"ml-auto px-1 text-xs empty:hidden\" aria-hidden=\"true\">{{\r\n node.count\r\n }}</span>\r\n }\r\n </a>\r\n @if (node.hasChildren && node.$opened) {\r\n <div role=\"group\">\r\n @for (child of node.items; track $index) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"tpl\"\r\n [ngTemplateOutletContext]=\"{\r\n node: child,\r\n path: treeChildrenPath(node, path),\r\n field: field,\r\n tpl: tpl,\r\n parent: node\r\n }\">\r\n </ng-container>\r\n }\r\n </div>\r\n }\r\n </div>\r\n</ng-template>\r\n", styles: ["div[role=treeitem]:has(+div[role=treeitem]){margin-bottom:var(--agg-item-gap, 0)}div[role=treeitem]{display:block;-webkit-user-select:none;user-select:none}div[role=treeitem] a{padding-left:calc(var(--agg-tree-indent, .5rem) * var(--level, 0));line-height:var(--agg-item-height, inherit)}\n"] }]
13453
+ }], ctorParameters: () => [], propDecorators: { class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }], column: [{ type: i0.Input, args: [{ isSignal: true, alias: "column", required: true }] }], collapsible: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsible", required: false }] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], showFiltersCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFiltersCount", required: false }] }], expandedLevel: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandedLevel", required: false }] }], onSelect: [{ type: i0.Output, args: ["onSelect"] }], onApply: [{ type: i0.Output, args: ["onApply"] }], onClear: [{ type: i0.Output, args: ["onClear"] }], virtualItems: [{ type: i0.ViewChildren, args: ["virtualItem", { isSignal: true }] }], scrollElement: [{ type: i0.ViewChild, args: ["scrollElement", { isSignal: true }] }], searchText: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchText", required: false }] }, { type: i0.Output, args: ["searchTextChange"] }] } });
13454
+
13455
+ class AggregationListComponent {
13456
+ cn = cn;
13457
+ /* inputs */
13458
+ class = input("", ...(ngDevMode ? [{ debugName: "class" }] : []));
13459
+ id = input(null, ...(ngDevMode ? [{ debugName: "id" }] : []));
13460
+ name = input.required(...(ngDevMode ? [{ debugName: "name" }] : []));
13461
+ column = input.required(...(ngDevMode ? [{ debugName: "column" }] : []));
13462
+ collapsible = input(false, ...(ngDevMode ? [{ debugName: "collapsible" }] : []));
13463
+ collapsed = input(false, ...(ngDevMode ? [{ debugName: "collapsed" }] : []));
13464
+ searchable = input(undefined, ...(ngDevMode ? [{ debugName: "searchable" }] : []));
13465
+ showFiltersCount = input(null, ...(ngDevMode ? [{ debugName: "showFiltersCount", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
13466
+ /* outputs */
13467
+ onSelect = output();
13468
+ onApply = output();
13469
+ onClear = output();
13470
+ /* view queries */
13471
+ scrollElement = viewChild("scrollElement", ...(ngDevMode ? [{ debugName: "scrollElement" }] : []));
13472
+ /* selection state */
13473
+ selection = signal(false, ...(ngDevMode ? [{ debugName: "selection" }] : []));
13474
+ isAllSelected = signal(false, ...(ngDevMode ? [{ debugName: "isAllSelected" }] : []));
13475
+ /* search state */
13476
+ searchText = model("", ...(ngDevMode ? [{ debugName: "searchText" }] : []));
13477
+ /* composable — injects stores/services, wires search effects, provides shared methods */
13478
+ base = injectAggregationBase({
13479
+ name: this.name,
13480
+ column: this.column,
13481
+ searchText: this.searchText,
13482
+ });
13483
+ /* spread from base */
13484
+ aggregationsStore = this.base.aggregationsStore;
13485
+ queryParamsStore = this.base.queryParamsStore;
13486
+ appStore = this.base.appStore;
13487
+ aggregationsService = this.base.aggregationsService;
13488
+ injector = this.base.injector;
13489
+ destroyRef = this.base.destroyRef;
13490
+ suggests = this.base.suggests;
13491
+ hasFilters = this.base.hasFilters;
13492
+ filtersCount = this.base.filtersCount;
13493
+ query = this.base.query;
13494
+ filters = this.base.filters;
13495
+ /* features from appStore */
13496
+ showCount = computed(() => this.appStore.general()?.features?.showAggregationItemCount ?? false, ...(ngDevMode ? [{ debugName: "showCount" }] : []));
13497
+ quickFilter = computed(() => this.appStore.general()?.features?.quickFilter, ...(ngDevMode ? [{ debugName: "quickFilter" }] : []));
13498
+ isDate = computed(() => this.appStore.isDateColumn(this.aggregation()?.column || ""), ...(ngDevMode ? [{ debugName: "isDate" }] : []));
13499
+ /* virtualizer */
13500
+ cdr = inject(ChangeDetectorRef);
13501
+ virtualizer = injectVirtualizer(() => ({
13502
+ count: this.items().length,
13503
+ estimateSize: () => 32,
13504
+ scrollElement: this.scrollElement(),
13505
+ }));
13506
+ aggregation = computed(() => {
13507
+ getState(this.aggregationsStore);
13508
+ const name = this.name();
13509
+ const column = this.column();
13510
+ if (name !== null) {
13511
+ const agg = this.aggregationsService.processAggregation(name, column);
13512
+ if (agg) {
13513
+ if (agg.isTree) {
13514
+ error("The aggregation component does not support tree aggregations. Please use the <AggregationTree /> component instead.");
13515
+ }
13516
+ agg.searchable = this.searchable() ?? agg.searchable;
13517
+ return agg;
13518
+ }
13519
+ }
13520
+ return null;
13521
+ }, ...(ngDevMode ? [{ debugName: "aggregation" }] : []));
13522
+ items = computed(() => {
13523
+ getState(this.aggregationsStore);
13524
+ const agg = this.aggregation();
13525
+ const searchedItems = this.searchedItems();
13526
+ if (searchedItems.length > 0)
13527
+ return searchedItems;
13528
+ if (!agg?.items)
13529
+ return [];
13530
+ // Reset $selected so items removed from the filter are deselected.
13531
+ // Then return a new array reference so Angular's signal equality check
13532
+ // detects the change and re-renders all component instances.
13533
+ agg.items.forEach(item => { item.$selected = false; });
13534
+ return [...this.addCurrentFiltersToItems()];
13535
+ }, ...(ngDevMode ? [{ debugName: "items" }] : []));
13536
+ isEmpty = computed(() => this.items().length === 0, ...(ngDevMode ? [{ debugName: "isEmpty" }] : []));
13537
+ searchedItems = computed(() => {
13538
+ if (!this.suggests())
13539
+ return [];
13540
+ return this.suggests()?.map((suggest) => {
13541
+ const column = this.appStore.getColumn(suggest.category);
13542
+ const item = {
13543
+ name: this.name(),
13544
+ value: suggest.normalized || suggest.display || "",
13545
+ display: suggest.display,
13546
+ column: column?.name ?? suggest.category,
13547
+ count: Number(suggest.frequency),
13548
+ $selected: false,
13549
+ };
13550
+ if (column?.eType === EngineType.bool) {
13551
+ item.value = Boolean(item.value);
13552
+ }
13553
+ return item;
13554
+ });
13555
+ }, ...(ngDevMode ? [{ debugName: "searchedItems" }] : []));
13556
+ constructor() {
13557
+ this.destroyRef.onDestroy(() => {
13558
+ if (this.selection()) {
13559
+ this.aggregation()?.items?.forEach((item) => {
13560
+ item.$selected = undefined;
13561
+ });
13562
+ }
13563
+ });
13564
+ }
13565
+ clear() {
13566
+ const agg = this.aggregation();
13567
+ if (agg) {
13568
+ this.queryParamsStore.removeFilterByName(agg.name, agg.column);
13569
+ this.selection.set(false);
13570
+ this.isAllSelected.set(false);
13571
+ }
13572
+ this.onSelect.emit([]);
13573
+ this.onClear.emit();
13574
+ }
13575
+ selectAll() {
13576
+ if (this.items().length) {
13577
+ this.base.selectItems(this.items(), true);
13578
+ this.selection.set(true);
13579
+ this.isAllSelected.set(true);
13580
+ }
13581
+ }
13582
+ unselectAll() {
13583
+ if (this.items().length) {
13584
+ this.base.selectItems(this.items(), false);
13585
+ this.select();
13586
+ this.isAllSelected.set(false);
13587
+ }
13588
+ }
13589
+ apply() {
13590
+ const agg = this.aggregation();
13591
+ if (!agg)
13592
+ return;
13593
+ this.base.applyFilters(this.getFilters(), agg, () => this.clear());
13594
+ this.searchText.set("");
13595
+ this.selection.set(false);
13596
+ this.onApply.emit();
13597
+ }
13598
+ loadMore() {
13599
+ const q = this.queryParamsStore.getQuery();
13600
+ this.aggregationsService.loadMore(q, this.aggregation()).subscribe((aggregation) => {
13601
+ this.aggregationsStore.updateAggregation(aggregation);
13602
+ this.cdr.detectChanges();
13603
+ });
13604
+ }
13605
+ select() {
13606
+ const selectedItems = this.items().filter((item) => item.$selected);
13607
+ this.onSelect.emit(selectedItems);
13608
+ this.selection.set(selectedItems.length > 0 || this.hasFilters());
13609
+ }
13610
+ /* item-level methods — called from the ng-template */
13611
+ listItemName(item) {
13612
+ const value = item.display || item.value;
13613
+ return typeof value === "string" ? value : `${value}`;
13614
+ }
13615
+ isListItemFiltered(item, field) {
13616
+ const filters = this.queryParamsStore.getFilter({ field: field ?? undefined, name: this.listItemName(item) });
13617
+ if (!filters)
13618
+ return false;
13619
+ const values = [item.value];
13620
+ return (values.some((v) => v === filters.value) ||
13621
+ !!(filters.values?.length && filters.values.some((value) => values.some((v) => v === value))));
13622
+ }
13623
+ selectListItem(item, e) {
13624
+ e?.stopImmediatePropagation();
13625
+ item.$selected = !item.$selected;
13626
+ this.select();
13627
+ }
13628
+ onListItemTextClick(item, event) {
13629
+ if (this.quickFilter()) {
13630
+ this.selectListItem(item, event);
13631
+ this.apply();
13632
+ }
13633
+ }
13634
+ getFilters() {
13635
+ const items = this.items().filter((item) => item.$selected);
13636
+ const searchedItems = this.searchedItems().filter((item) => item.$selected);
13637
+ const currentItems = [...items, ...searchedItems];
13638
+ const { column, name, isDistribution = false } = this.aggregation() || {};
13639
+ return currentItems.map((item) => this.aggregationsService.toFilter(item, column, name, isDistribution));
13640
+ }
13641
+ addCurrentFiltersToItems() {
13642
+ return this.base.addCurrentFiltersToItems(this.aggregation());
13643
+ }
13644
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AggregationListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
13645
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: AggregationListComponent, isStandalone: true, selector: "AggregationList, aggregation-list, aggregationlist", inputs: { class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null }, column: { classPropertyName: "column", publicName: "column", isSignal: true, isRequired: true, transformFunction: null }, collapsible: { classPropertyName: "collapsible", publicName: "collapsible", isSignal: true, isRequired: false, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, searchable: { classPropertyName: "searchable", publicName: "searchable", isSignal: true, isRequired: false, transformFunction: null }, showFiltersCount: { classPropertyName: "showFiltersCount", publicName: "showFiltersCount", isSignal: true, isRequired: false, transformFunction: null }, searchText: { classPropertyName: "searchText", publicName: "searchText", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSelect: "onSelect", onApply: "onApply", onClear: "onClear", searchText: "searchTextChange" }, host: { properties: { "class": "cn(\"block h-[inherit] max-h-[inherit]\", class())" } }, viewQueries: [{ propertyName: "scrollElement", first: true, predicate: ["scrollElement"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (aggregation()?.isTree) {\r\n <div class=\"p-2 text-sm text-red-500\">\r\n <triangle-alert-icon class=\"mr-1\" />\r\n The aggregation component no longer supports tree aggregations. Please use\r\n the &lt;AggregationTree /&gt; component instead.\r\n </div>\r\n}\r\n<aggregation-panel\r\n [id]=\"id()\"\r\n [collapsible]=\"collapsible()\"\r\n [collapsed]=\"collapsed()\"\r\n [isEmpty]=\"isEmpty()\"\r\n [isDate]=\"isDate()\"\r\n [aggregation]=\"aggregation()\"\r\n [showFiltersCount]=\"showFiltersCount()\"\r\n [filtersCount]=\"filtersCount()\"\r\n [hasFilters]=\"hasFilters()\"\r\n [selection]=\"selection()\"\r\n [isAllSelected]=\"isAllSelected()\"\r\n [(searchText)]=\"searchText\"\r\n [itemsLength]=\"items().length\"\r\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\r\n [searchedItemsLength]=\"searchedItems().length\"\r\n (cleared)=\"clear()\"\r\n (applied)=\"apply()\"\r\n (allSelected)=\"selectAll()\"\r\n (allUnselected)=\"unselectAll()\"\r\n (loadedMore)=\"loadMore()\">\r\n <div\r\n #scrollElement\r\n class=\"scrollbar-thin max-h-(--scroll-height,20rem) w-full overflow-auto\">\r\n <div\r\n class=\"relative w-full\"\r\n [style.height]=\"virtualizer.getTotalSize() + 'px'\"\r\n role=\"listbox\"\r\n aria-multiselectable=\"true\"\r\n [attr.aria-label]=\"aggregation()?.display | syslang | transloco\">\r\n @for (vItem of virtualizer.getVirtualItems(); track vItem.index) {\r\n @let item = items()[vItem.index];\r\n <div\r\n class=\"absolute w-full\"\r\n [style.transform]=\"'translateY(' + vItem.start + 'px)'\"\r\n role=\"option\"\r\n [attr.aria-selected]=\"item.$selected ?? false\"\r\n [attr.aria-disabled]=\"item.count === 0 ? 'true' : null\"\r\n [attr.disabled]=\"item.count === 0 ? 'disabled' : null\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"listItemTpl\"\r\n [ngTemplateOutletContext]=\"{\r\n $implicit: item,\r\n field: aggregation()?.column\r\n }\">\r\n </ng-container>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n</aggregation-panel>\r\n\r\n<ng-template #listItemTpl let-item let-field=\"field\">\r\n <a\r\n [attr.aria-label]=\"listItemName(item) | syslang\"\r\n [class]=\"\r\n cn(\r\n 'flex grow items-center gap-2 p-1 leading-7',\r\n item.count === 0 && 'disabled pointer-events-none'\r\n )\r\n \"\r\n (click)=\"selectListItem(item, $event)\">\r\n <input\r\n type=\"checkbox\"\r\n role=\"checkbox\"\r\n value=\"{{ item.value }}\"\r\n [attr.disabled]=\"item.count === 0 ? true : null\"\r\n [attr.aria-disabled]=\"item.count === 0\"\r\n (keydown.enter)=\"selectListItem(item, $event)\"\r\n [checked]=\"item.$selected\" />\r\n @let icon = item.icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"self-center justify-self-center\" />\r\n }\r\n <span\r\n [class]=\"\r\n cn(\r\n 'line-clamp-1 break-all text-ellipsis',\r\n quickFilter() && 'hover:underline'\r\n )\r\n \"\r\n [title]=\"\r\n quickFilter()\r\n ? ((isListItemFiltered(item, field)\r\n ? 'filters.removeFilter'\r\n : 'filters.addFilter'\r\n ) | transloco) +\r\n ': ' +\r\n (listItemName(item) | syslang)\r\n : (listItemName(item) | syslang)\r\n \"\r\n (click)=\"onListItemTextClick(item, $event)\">\r\n @for (\r\n chunk of (listItemName(item) | syslang) ?? \"\"\r\n | highlightWord: searchText() : 10;\r\n track $index\r\n ) {\r\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\r\n chunk.text\r\n }}</span>\r\n }\r\n </span>\r\n @if (showCount() && item.count > 0) {\r\n <span class=\"ml-auto px-1 text-xs empty:hidden\" aria-hidden=\"true\">{{\r\n item.count\r\n }}</span>\r\n }\r\n </a>\r\n</ng-template>\r\n", styles: ["div[role=option]{display:block;-webkit-user-select:none;user-select:none}div[role=option] a{padding-left:var(--agg-tree-indent, .5rem);line-height:var(--agg-item-height, inherit)}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "component", type: FaIconComponent, selector: "fa-icon, FaIcon", inputs: ["faClass", "class"] }, { kind: "component", type: TriangleAlertIcon, selector: "triangle-alert-icon, TriangleAlertIcon", inputs: ["class"] }, { kind: "component", type: AggregationPanelComponent, selector: "AggregationPanel, aggregation-panel", inputs: ["id", "collapsible", "collapsed", "isDate", "isEmpty", "aggregation", "showFiltersCount", "filtersCount", "hasFilters", "selection", "isAllSelected", "searchText", "itemsLength", "hasMore", "searchedItemsLength"], outputs: ["searchTextChange", "cleared", "applied", "allSelected", "allUnselected", "loadedMore"] }, { kind: "pipe", type: SyslangPipe, name: "syslang" }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }, { kind: "pipe", type: HighlightWordPipe, name: "highlightWord" }] });
13646
+ }
13647
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AggregationListComponent, decorators: [{
13648
+ type: Component,
13649
+ args: [{ selector: "AggregationList, aggregation-list, aggregationlist", imports: [
13650
+ NgTemplateOutlet,
13651
+ FormsModule,
13652
+ ReactiveFormsModule,
13653
+ SyslangPipe,
13654
+ TranslocoPipe,
13655
+ FaIconComponent,
13656
+ TriangleAlertIcon,
13657
+ HighlightWordPipe,
13658
+ AggregationPanelComponent,
13829
13659
  ], standalone: true, host: {
13830
- "[class]": 'cn("block h-[inherit] max-h-[inherit] w-[inherit]",class())'
13831
- }, template: "@if (!aggregation()?.isTree) {\r\n <div class=\"p-2 text-sm text-red-500\">\r\n <triangle-alert-icon class=\"mr-1\" />\r\n The aggregationTree component does not support list aggregations. Please use\r\n the &lt;Aggregation /&gt; component instead.\r\n </div>\r\n}\r\n<details\r\n [attr.open]=\"expanded()\"\r\n [attr.name]=\"id()\"\r\n class=\"group space-y-2\"\r\n (toggle)=\"onToggle($event)\">\r\n <summary\r\n [class.cursor-pointer]=\"collapsible() && !isEmpty()\"\r\n [class.text-muted-foreground]=\"isEmpty()\"\r\n class=\"m-0 mt-1 flex h-8 w-full items-center gap-1 pl-1 font-semibold select-none\"\r\n (click)=\"onHeaderClick($event)\">\r\n <ng-content select=\"label\">\r\n @let icon = aggregation()?.icon;\r\n @if (icon) {\r\n <fa-icon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\r\n }\r\n <span class=\"grow truncate\">{{\r\n aggregation()?.display | syslang | transloco\r\n }}</span>\r\n </ng-content>\r\n\r\n @if (showFiltersCount() && filtersCount() > 0) {\r\n <!-- count -->\r\n <Badge size=\"xs\" class=\"ml-1\">\r\n {{ filtersCount() }}\r\n </Badge>\r\n }\r\n <!-- apply filter block -->\r\n @if (!isCollapsed()) {\r\n @if (hasFilters()) {\r\n @let label = \"filters.clearFilters\" | transloco;\r\n <button\r\n variant=\"none\"\r\n icon-button\r\n [aria-label]=\"label\"\r\n (click)=\"$event.stopPropagation(); clear()\">\r\n <filter-x-icon />\r\n </button>\r\n }\r\n @if (selection()) {\r\n @let label = \"filters.apply\" | transloco;\r\n <button\r\n variant=\"accent\"\r\n size=\"sm\"\r\n [aria-label]=\"label\"\r\n (click)=\"$event.stopPropagation(); apply()\">\r\n <FilterIcon />\r\n {{ label }}\r\n </button>\r\n }\r\n\r\n <!-- select / unselect all -->\r\n @if (isAllSelected()) {\r\n @let label = \"filters.unselectAllFilters\" | transloco;\r\n <button\r\n variant=\"none\"\r\n icon-button\r\n [aria-label]=\"label\"\r\n (click)=\"$event.stopPropagation(); unselectAll()\">\r\n <square-check-icon />\r\n </button>\r\n } @else {\r\n @let label = \"filters.selectAllFilters\" | transloco;\r\n <button\r\n variant=\"none\"\r\n icon-button\r\n [aria-label]=\"label\"\r\n (click)=\"$event.stopPropagation(); selectAll()\">\r\n <square-icon />\r\n </button>\r\n }\r\n }\r\n\r\n @if (collapsible()) {\r\n <icon-button\r\n title=\"Open/Close\"\r\n class=\"cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\r\n <chevronright />\r\n <span class=\"sr-only\">{{ \"filters.toggle\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n </summary>\r\n\r\n <!-- content wrapper -->\r\n @if (aggregation()?.searchable && items().length) {\r\n <InputGroup class=\"group/item mt-1\">\r\n <input\r\n #searchInput\r\n input-group\r\n id=\"aggregation-input-{{ column() }}\"\r\n type=\"text\"\r\n [attr.placeholder]=\"'search' | transloco\"\r\n [(ngModel)]=\"searchText\"\r\n class=\"mt-1\" />\r\n <InputGroupAddon>\r\n <SearchIcon\r\n class=\"text-foreground size-4 rotate-0 transition-[rotate] duration-500 group-focus-within/item:rotate-90\" />\r\n </InputGroupAddon>\r\n <InputGroupAddon align=\"inline-end\" class=\"gap-0.5!\">\r\n <icon-button\r\n size=\"sm\"\r\n [class]=\"\r\n searchText().length > 0\r\n ? 'rotate-90 cursor-pointer opacity-100 transition-[rotate,opacity] duration-500'\r\n : 'pointer-events-none rotate-0 opacity-0 transition-[rotate,opacity] duration-500'\r\n \"\r\n aria-label=\"Clear search\"\r\n [tabindex]=\"searchText().length > 0 ? 0 : -1\"\r\n (keydown.enter)=\"clearSearch($event)\"\r\n (click)=\"clearSearch($event)\">\r\n <XMarkIcon />\r\n </icon-button>\r\n <ng-content />\r\n </InputGroupAddon>\r\n </InputGroup>\r\n }\r\n\r\n <div\r\n #scrollElement\r\n class=\"scrollbar-thin max-h-[calc(var(--height,100%)-100px)] w-full overflow-auto\">\r\n <div\r\n class=\"relative w-full\"\r\n [style.height]=\"virtualizer.getTotalSize() + 'px'\"\r\n role=\"list\"\r\n [attr.aria-label]=\"aggregation()?.display | syslang | transloco\">\r\n <div\r\n class=\"absolute top-0 left-0 w-full\"\r\n [style.transform]=\"\r\n 'translateY(' +\r\n (virtualizer.getVirtualItems()[0]\r\n ? virtualizer.getVirtualItems()[0].start\r\n : 0) +\r\n 'px)'\r\n \"\r\n role=\"listitem\">\r\n @for (vItem of virtualizer.getVirtualItems(); track vItem.index) {\r\n @let item = items()[vItem.index];\r\n <div #virtualItem [attr.data-index]=\"vItem.index\">\r\n <AggregationTreeItem\r\n [node]=\"item\"\r\n [path]=\"[]\"\r\n [field]=\"aggregation()?.column\"\r\n (onSelect)=\"select()\"\r\n (onOpen)=\"open($event)\"\r\n (onFilter)=\"apply()\" />\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n @if (aggregation()?.$hasMore && this.searchedItems().length === 0) {\r\n <button\r\n class=\"mt-1 flex w-full justify-center\"\r\n [attr.aria-label]=\"'loadMore' | transloco\"\r\n (click)=\"loadMore()\">\r\n {{ \"loadMore\" | transloco }}\r\n </button>\r\n }\r\n</details>\r\n", styles: ["AggregationTreeItem:has(+AggregationTreeItem){margin-bottom:var(--agg-item-gap, 0)}\n"] }]
13832
- }], ctorParameters: () => [], propDecorators: { virtualItems: [{ type: i0.ViewChildren, args: ['virtualItem', { isSignal: true }] }], scrollElement: [{ type: i0.ViewChild, args: ["scrollElement", { isSignal: true }] }], searchInput: [{ type: i0.ViewChild, args: ["searchInput", { isSignal: true }] }], class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }], column: [{ type: i0.Input, args: [{ isSignal: true, alias: "column", required: true }] }], expandedLevel: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandedLevel", required: false }] }], onSelect: [{ type: i0.Output, args: ["onSelect"] }], onApply: [{ type: i0.Output, args: ["onApply"] }], onClear: [{ type: i0.Output, args: ["onClear"] }], collapsible: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsible", required: false }] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], showFiltersCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFiltersCount", required: false }] }], searchText: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchText", required: false }] }, { type: i0.Output, args: ["searchTextChange"] }] } });
13660
+ "[class]": 'cn("block h-[inherit] max-h-[inherit]", class())',
13661
+ }, template: "@if (aggregation()?.isTree) {\r\n <div class=\"p-2 text-sm text-red-500\">\r\n <triangle-alert-icon class=\"mr-1\" />\r\n The aggregation component no longer supports tree aggregations. Please use\r\n the &lt;AggregationTree /&gt; component instead.\r\n </div>\r\n}\r\n<aggregation-panel\r\n [id]=\"id()\"\r\n [collapsible]=\"collapsible()\"\r\n [collapsed]=\"collapsed()\"\r\n [isEmpty]=\"isEmpty()\"\r\n [isDate]=\"isDate()\"\r\n [aggregation]=\"aggregation()\"\r\n [showFiltersCount]=\"showFiltersCount()\"\r\n [filtersCount]=\"filtersCount()\"\r\n [hasFilters]=\"hasFilters()\"\r\n [selection]=\"selection()\"\r\n [isAllSelected]=\"isAllSelected()\"\r\n [(searchText)]=\"searchText\"\r\n [itemsLength]=\"items().length\"\r\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\r\n [searchedItemsLength]=\"searchedItems().length\"\r\n (cleared)=\"clear()\"\r\n (applied)=\"apply()\"\r\n (allSelected)=\"selectAll()\"\r\n (allUnselected)=\"unselectAll()\"\r\n (loadedMore)=\"loadMore()\">\r\n <div\r\n #scrollElement\r\n class=\"scrollbar-thin max-h-(--scroll-height,20rem) w-full overflow-auto\">\r\n <div\r\n class=\"relative w-full\"\r\n [style.height]=\"virtualizer.getTotalSize() + 'px'\"\r\n role=\"listbox\"\r\n aria-multiselectable=\"true\"\r\n [attr.aria-label]=\"aggregation()?.display | syslang | transloco\">\r\n @for (vItem of virtualizer.getVirtualItems(); track vItem.index) {\r\n @let item = items()[vItem.index];\r\n <div\r\n class=\"absolute w-full\"\r\n [style.transform]=\"'translateY(' + vItem.start + 'px)'\"\r\n role=\"option\"\r\n [attr.aria-selected]=\"item.$selected ?? false\"\r\n [attr.aria-disabled]=\"item.count === 0 ? 'true' : null\"\r\n [attr.disabled]=\"item.count === 0 ? 'disabled' : null\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"listItemTpl\"\r\n [ngTemplateOutletContext]=\"{\r\n $implicit: item,\r\n field: aggregation()?.column\r\n }\">\r\n </ng-container>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n</aggregation-panel>\r\n\r\n<ng-template #listItemTpl let-item let-field=\"field\">\r\n <a\r\n [attr.aria-label]=\"listItemName(item) | syslang\"\r\n [class]=\"\r\n cn(\r\n 'flex grow items-center gap-2 p-1 leading-7',\r\n item.count === 0 && 'disabled pointer-events-none'\r\n )\r\n \"\r\n (click)=\"selectListItem(item, $event)\">\r\n <input\r\n type=\"checkbox\"\r\n role=\"checkbox\"\r\n value=\"{{ item.value }}\"\r\n [attr.disabled]=\"item.count === 0 ? true : null\"\r\n [attr.aria-disabled]=\"item.count === 0\"\r\n (keydown.enter)=\"selectListItem(item, $event)\"\r\n [checked]=\"item.$selected\" />\r\n @let icon = item.icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"self-center justify-self-center\" />\r\n }\r\n <span\r\n [class]=\"\r\n cn(\r\n 'line-clamp-1 break-all text-ellipsis',\r\n quickFilter() && 'hover:underline'\r\n )\r\n \"\r\n [title]=\"\r\n quickFilter()\r\n ? ((isListItemFiltered(item, field)\r\n ? 'filters.removeFilter'\r\n : 'filters.addFilter'\r\n ) | transloco) +\r\n ': ' +\r\n (listItemName(item) | syslang)\r\n : (listItemName(item) | syslang)\r\n \"\r\n (click)=\"onListItemTextClick(item, $event)\">\r\n @for (\r\n chunk of (listItemName(item) | syslang) ?? \"\"\r\n | highlightWord: searchText() : 10;\r\n track $index\r\n ) {\r\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\r\n chunk.text\r\n }}</span>\r\n }\r\n </span>\r\n @if (showCount() && item.count > 0) {\r\n <span class=\"ml-auto px-1 text-xs empty:hidden\" aria-hidden=\"true\">{{\r\n item.count\r\n }}</span>\r\n }\r\n </a>\r\n</ng-template>\r\n", styles: ["div[role=option]{display:block;-webkit-user-select:none;user-select:none}div[role=option] a{padding-left:var(--agg-tree-indent, .5rem);line-height:var(--agg-item-height, inherit)}\n"] }]
13662
+ }], ctorParameters: () => [], propDecorators: { class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }], column: [{ type: i0.Input, args: [{ isSignal: true, alias: "column", required: true }] }], collapsible: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsible", required: false }] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], showFiltersCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFiltersCount", required: false }] }], onSelect: [{ type: i0.Output, args: ["onSelect"] }], onApply: [{ type: i0.Output, args: ["onApply"] }], onClear: [{ type: i0.Output, args: ["onClear"] }], scrollElement: [{ type: i0.ViewChild, args: ["scrollElement", { isSignal: true }] }], searchText: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchText", required: false }] }, { type: i0.Output, args: ["searchTextChange"] }] } });
13833
13663
 
13834
13664
  /**
13835
13665
  * The `AggregationComponent` is responsible for rendering the appropriate aggregation component based on the type of aggregation (date, tree, or list). It uses the `AggregationsService` to retrieve the aggregation data and determines which component to display based on the column type and whether it is a tree structure. The component also provides inputs for configuring its behavior, such as collapsibility, searchability, and showing filter counts.
@@ -13986,7 +13816,7 @@ class AggregationComponent {
13986
13816
  (onClear)="onClear.emit($event)"
13987
13817
  />
13988
13818
  }
13989
- `, isInline: true, dependencies: [{ kind: "component", type: AggregationListComponent, selector: "AggregationList, aggregation-list, aggregationlist", inputs: ["class", "id", "name", "column", "collapsible", "collapsed", "searchable", "showFiltersCount", "searchText"], outputs: ["onSelect", "onApply", "onClear", "searchTextChange"] }, { kind: "component", type: AggregationTreeComponent, selector: "AggregationTree, aggregation-tree, aggregationtree", inputs: ["class", "id", "name", "column", "expandedLevel", "collapsible", "collapsed", "searchable", "showFiltersCount", "searchText"], outputs: ["onSelect", "onApply", "onClear", "searchTextChange"] }, { kind: "component", type: AggregationDateComponent, selector: "aggregation-date, AggregationDate, aggregationdate", inputs: ["title", "displayEmptyDistributionIntervals", "name"] }] });
13819
+ `, isInline: true, dependencies: [{ kind: "component", type: AggregationListComponent, selector: "AggregationList, aggregation-list, aggregationlist", inputs: ["class", "id", "name", "column", "collapsible", "collapsed", "searchable", "showFiltersCount", "searchText"], outputs: ["onSelect", "onApply", "onClear", "searchTextChange"] }, { kind: "component", type: AggregationTreeComponent, selector: "AggregationTree, aggregation-tree, aggregationtree", inputs: ["class", "id", "name", "column", "collapsible", "collapsed", "searchable", "showFiltersCount", "expandedLevel", "searchText"], outputs: ["onSelect", "onApply", "onClear", "searchTextChange"] }, { kind: "component", type: AggregationDateComponent, selector: "aggregation-date, AggregationDate, aggregationdate", inputs: ["name", "column", "id", "collapsible", "collapsed", "searchable", "showFiltersCount", "title", "displayEmptyDistributionIntervals", "searchText"], outputs: ["onSelect", "onApply", "onClear", "searchTextChange"] }] });
13990
13820
  }
13991
13821
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AggregationComponent, decorators: [{
13992
13822
  type: Component,
@@ -14302,6 +14132,7 @@ class MoreComponent {
14302
14132
  includedFilters = input([], ...(ngDevMode ? [{ debugName: "includedFilters" }] : []));
14303
14133
  excludedFilters = input([], ...(ngDevMode ? [{ debugName: "excludedFilters" }] : []));
14304
14134
  aggregations = input(...(ngDevMode ? [undefined, { debugName: "aggregations" }] : []));
14135
+ homepage = input(false, ...(ngDevMode ? [{ debugName: "homepage", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
14305
14136
  appStore = inject(AppStore);
14306
14137
  aggregationsStore = inject(AggregationsStore);
14307
14138
  queryParamsStore = inject(QueryParamsStore);
@@ -14329,7 +14160,7 @@ class MoreComponent {
14329
14160
  effect(() => {
14330
14161
  const count = this.count();
14331
14162
  const authorizedFilters = this.aggregationsService
14332
- .getAuthorizedFilters(this.aggregations(), this.includedFilters(), this.excludedFilters())
14163
+ .getAuthorizedFilters(this.aggregations(), this.includedFilters(), this.excludedFilters(), this.homepage())
14333
14164
  .toSpliced(0, count);
14334
14165
  const f = authorizedFilters.map((agg) => {
14335
14166
  const { icon = "far fa-list", hidden = false } = this.appStore.getAggregationCustomization(agg.column, agg.name) || {};
@@ -14380,7 +14211,7 @@ class MoreComponent {
14380
14211
  return count > 0;
14381
14212
  }
14382
14213
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: MoreComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
14383
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: MoreComponent, isStandalone: true, selector: "more, More", inputs: { count: { classPropertyName: "count", publicName: "count", isSignal: true, isRequired: false, transformFunction: null }, includedFilters: { classPropertyName: "includedFilters", publicName: "includedFilters", isSignal: true, isRequired: false, transformFunction: null }, excludedFilters: { classPropertyName: "excludedFilters", publicName: "excludedFilters", isSignal: true, isRequired: false, transformFunction: null }, aggregations: { classPropertyName: "aggregations", publicName: "aggregations", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "divide-y divide-muted-foreground/18" }, ngImport: i0, template: `
14214
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: MoreComponent, isStandalone: true, selector: "more, More", inputs: { count: { classPropertyName: "count", publicName: "count", isSignal: true, isRequired: false, transformFunction: null }, includedFilters: { classPropertyName: "includedFilters", publicName: "includedFilters", isSignal: true, isRequired: false, transformFunction: null }, excludedFilters: { classPropertyName: "excludedFilters", publicName: "excludedFilters", isSignal: true, isRequired: false, transformFunction: null }, aggregations: { classPropertyName: "aggregations", publicName: "aggregations", isSignal: true, isRequired: false, transformFunction: null }, homepage: { classPropertyName: "homepage", publicName: "homepage", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "divide-y divide-muted-foreground/18" }, ngImport: i0, template: `
14384
14215
  @for (filter of visibleFilters(); track $index) {
14385
14216
  <Aggregation
14386
14217
  class="w-60 max-w-80 px-1 [--height:15lh]"
@@ -14411,7 +14242,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
14411
14242
  `, host: {
14412
14243
  class: "divide-y divide-muted-foreground/18"
14413
14244
  }, styles: [":host{scrollbar-width:none}\n"] }]
14414
- }], ctorParameters: () => [], propDecorators: { count: [{ type: i0.Input, args: [{ isSignal: true, alias: "count", required: false }] }], includedFilters: [{ type: i0.Input, args: [{ isSignal: true, alias: "includedFilters", required: false }] }], excludedFilters: [{ type: i0.Input, args: [{ isSignal: true, alias: "excludedFilters", required: false }] }], aggregations: [{ type: i0.Input, args: [{ isSignal: true, alias: "aggregations", required: false }] }] } });
14245
+ }], ctorParameters: () => [], propDecorators: { count: [{ type: i0.Input, args: [{ isSignal: true, alias: "count", required: false }] }], includedFilters: [{ type: i0.Input, args: [{ isSignal: true, alias: "includedFilters", required: false }] }], excludedFilters: [{ type: i0.Input, args: [{ isSignal: true, alias: "excludedFilters", required: false }] }], aggregations: [{ type: i0.Input, args: [{ isSignal: true, alias: "aggregations", required: false }] }], homepage: [{ type: i0.Input, args: [{ isSignal: true, alias: "homepage", required: false }] }] } });
14415
14246
 
14416
14247
  class MoreButtonComponent {
14417
14248
  appStore = inject(AppStore);
@@ -14423,10 +14254,11 @@ class MoreButtonComponent {
14423
14254
  includedFilters = input([], ...(ngDevMode ? [{ debugName: "includedFilters" }] : []));
14424
14255
  excludedFilters = input([], ...(ngDevMode ? [{ debugName: "excludedFilters" }] : []));
14425
14256
  aggregations = input(...(ngDevMode ? [undefined, { debugName: "aggregations" }] : []));
14257
+ homepage = input(false, ...(ngDevMode ? [{ debugName: "homepage", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
14426
14258
  totalFiltersCount = computed(() => {
14427
14259
  const count = this.count();
14428
14260
  const authorizedFilters = this.aggregationsService
14429
- .getAuthorizedFilters(this.aggregations(), this.includedFilters(), this.excludedFilters())
14261
+ .getAuthorizedFilters(this.aggregations(), this.includedFilters(), this.excludedFilters(), this.homepage())
14430
14262
  .toSpliced(0, count);
14431
14263
  const total = authorizedFilters.reduce((acc, filter) => {
14432
14264
  const f = this.queryParamsStore.getFilter(filter);
@@ -14436,7 +14268,7 @@ class MoreButtonComponent {
14436
14268
  return total;
14437
14269
  }, ...(ngDevMode ? [{ debugName: "totalFiltersCount" }] : []));
14438
14270
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: MoreButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
14439
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: MoreButtonComponent, isStandalone: true, selector: "more-button, MoreButton", inputs: { count: { classPropertyName: "count", publicName: "count", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, includedFilters: { classPropertyName: "includedFilters", publicName: "includedFilters", isSignal: true, isRequired: false, transformFunction: null }, excludedFilters: { classPropertyName: "excludedFilters", publicName: "excludedFilters", isSignal: true, isRequired: false, transformFunction: null }, aggregations: { classPropertyName: "aggregations", publicName: "aggregations", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
14271
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: MoreButtonComponent, isStandalone: true, selector: "more-button, MoreButton", inputs: { count: { classPropertyName: "count", publicName: "count", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, includedFilters: { classPropertyName: "includedFilters", publicName: "includedFilters", isSignal: true, isRequired: false, transformFunction: null }, excludedFilters: { classPropertyName: "excludedFilters", publicName: "excludedFilters", isSignal: true, isRequired: false, transformFunction: null }, aggregations: { classPropertyName: "aggregations", publicName: "aggregations", isSignal: true, isRequired: false, transformFunction: null }, homepage: { classPropertyName: "homepage", publicName: "homepage", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
14440
14272
  <Popover class="group/more">
14441
14273
  <button
14442
14274
  variant="ghost"
@@ -14454,11 +14286,11 @@ class MoreButtonComponent {
14454
14286
 
14455
14287
  <PopoverContent #contentRef="popoverContent" [position]="position()" class="min-w-max">
14456
14288
  @if(contentRef.isVisible) {
14457
- <More [count]="count()" [includedFilters]="includedFilters()" [excludedFilters]="excludedFilters()" [aggregations]="aggregations()" class="block h-full w-full max-w-80 [--height:55vh] min-w-40 overflow-hidden" />
14289
+ <More [count]="count()" [includedFilters]="includedFilters()" [excludedFilters]="excludedFilters()" [aggregations]="aggregations()" [homepage]="homepage()" class="block h-full w-full max-w-80 [--height:55vh] min-w-40 overflow-hidden" />
14458
14290
  }
14459
14291
  </PopoverContent>
14460
14292
  </Popover>
14461
- `, isInline: true, dependencies: [{ kind: "directive", type: ButtonComponent, selector: "button", inputs: ["class", "variant", "decoration", "scheme", "iconOnly", "size", "solid"] }, { kind: "component", type: PopoverComponent, selector: "popover, Popover", inputs: ["disabled", "closeOnScroll"], outputs: ["closed"] }, { kind: "directive", type: PopoverContentComponent, selector: "popover-content, PopoverContent, popovercontent", inputs: ["class", "position", "keepOpen", "offset", "strategy"], exportAs: ["popoverContent"] }, { kind: "component", type: MoreComponent, selector: "more, More", inputs: ["count", "includedFilters", "excludedFilters", "aggregations"] }, { kind: "directive", type: BadgeComponent, selector: "badge, Badge", inputs: ["class", "variant", "scheme", "size"] }, { kind: "component", type: ListFilterIcon, selector: "list-filter-icon, ListFilterIcon", inputs: ["class"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
14293
+ `, isInline: true, dependencies: [{ kind: "directive", type: ButtonComponent, selector: "button", inputs: ["class", "variant", "decoration", "scheme", "iconOnly", "size", "solid"] }, { kind: "component", type: PopoverComponent, selector: "popover, Popover", inputs: ["disabled", "closeOnScroll"], outputs: ["closed"] }, { kind: "directive", type: PopoverContentComponent, selector: "popover-content, PopoverContent, popovercontent", inputs: ["class", "position", "keepOpen", "offset", "strategy"], exportAs: ["popoverContent"] }, { kind: "component", type: MoreComponent, selector: "more, More", inputs: ["count", "includedFilters", "excludedFilters", "aggregations", "homepage"] }, { kind: "directive", type: BadgeComponent, selector: "badge, Badge", inputs: ["class", "variant", "scheme", "size"] }, { kind: "component", type: ListFilterIcon, selector: "list-filter-icon, ListFilterIcon", inputs: ["class"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
14462
14294
  }
14463
14295
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: MoreButtonComponent, decorators: [{
14464
14296
  type: Component,
@@ -14484,13 +14316,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
14484
14316
 
14485
14317
  <PopoverContent #contentRef="popoverContent" [position]="position()" class="min-w-max">
14486
14318
  @if(contentRef.isVisible) {
14487
- <More [count]="count()" [includedFilters]="includedFilters()" [excludedFilters]="excludedFilters()" [aggregations]="aggregations()" class="block h-full w-full max-w-80 [--height:55vh] min-w-40 overflow-hidden" />
14319
+ <More [count]="count()" [includedFilters]="includedFilters()" [excludedFilters]="excludedFilters()" [aggregations]="aggregations()" [homepage]="homepage()" class="block h-full w-full max-w-80 [--height:55vh] min-w-40 overflow-hidden" />
14488
14320
  }
14489
14321
  </PopoverContent>
14490
14322
  </Popover>
14491
14323
  `
14492
14324
  }]
14493
- }], propDecorators: { count: [{ type: i0.Input, args: [{ isSignal: true, alias: "count", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], includedFilters: [{ type: i0.Input, args: [{ isSignal: true, alias: "includedFilters", required: false }] }], excludedFilters: [{ type: i0.Input, args: [{ isSignal: true, alias: "excludedFilters", required: false }] }], aggregations: [{ type: i0.Input, args: [{ isSignal: true, alias: "aggregations", required: false }] }] } });
14325
+ }], propDecorators: { count: [{ type: i0.Input, args: [{ isSignal: true, alias: "count", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], includedFilters: [{ type: i0.Input, args: [{ isSignal: true, alias: "includedFilters", required: false }] }], excludedFilters: [{ type: i0.Input, args: [{ isSignal: true, alias: "excludedFilters", required: false }] }], aggregations: [{ type: i0.Input, args: [{ isSignal: true, alias: "aggregations", required: false }] }], homepage: [{ type: i0.Input, args: [{ isSignal: true, alias: "homepage", required: false }] }] } });
14494
14326
 
14495
14327
  class FiltersBarComponent {
14496
14328
  class = input(...(ngDevMode ? [undefined, { debugName: "class" }] : []));
@@ -14528,6 +14360,15 @@ class FiltersBarComponent {
14528
14360
  * @default true
14529
14361
  */
14530
14362
  showMoreFiltersButton = input(true, ...(ngDevMode ? [{ debugName: "showMoreFiltersButton", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
14363
+ /**
14364
+ * When enabled, only the filters flagged with `homepage: true` in the "filters" custom JSON
14365
+ * are displayed. If no filter is flagged, the bar shows no filters.
14366
+ *
14367
+ * Accepts a boolean value or a string that can be transformed to a boolean.
14368
+ *
14369
+ * @default false
14370
+ */
14371
+ homepage = input(false, ...(ngDevMode ? [{ debugName: "homepage", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
14531
14372
  direction = input("horizontal", ...(ngDevMode ? [{ debugName: "direction" }] : []));
14532
14373
  /**
14533
14374
  * The distance in pixels between the popover and its trigger element.
@@ -14577,50 +14418,76 @@ class FiltersBarComponent {
14577
14418
  return this.aggregationsStore.aggregations().length > 0;
14578
14419
  return false;
14579
14420
  }, ...(ngDevMode ? [{ debugName: "hasAggregations" }] : []));
14421
+ /**
14422
+ * The full list of authorized filters, NOT capped by `filtersCount`.
14423
+ *
14424
+ * This computed signal performs the following operations:
14425
+ * 1. Retrieves aggregations from either the component's aggregations input or the app store
14426
+ * 2. Filters aggregations based on the route's filter criteria configuration
14427
+ * 3. Excludes filters specified in the `excludeFilters` list
14428
+ * 4. If `includeFilters` is not empty, only includes filters present in that list
14429
+ * 5. Maps the filtered aggregations to objects containing only `name` and `column` properties
14430
+ */
14431
+ allAuthorizedFilters = computed(() => {
14432
+ return this.aggregationsService
14433
+ .getAuthorizedFilters(this.aggregations(), this.includeFilters(), this.excludeFilters(), this.homepage())
14434
+ .map((f) => ({ name: f.name, column: f.column }));
14435
+ }, ...(ngDevMode ? [{ debugName: "allAuthorizedFilters" }] : []));
14580
14436
  /**
14581
14437
  * Computes the list of additional filters that can be displayed in the "more filters" popover.
14582
14438
  *
14583
- * This computed property filters the authorized filters from the AppStore, excluding those
14584
- * specified in the `excludeFilters` input. It then maps these filters to their corresponding
14585
- * aggregations from the AggregationsStore, limited to the number defined by `moreFilterCount`.
14439
+ * Derived from the FULL authorized list (not the one capped by `filtersCount`), so the
14440
+ * filters beyond `filtersCount` which are never rendered in the bar — are still counted.
14441
+ * Otherwise, when every rendered filter fits in the container, this list would be empty and
14442
+ * the "more" button would be hidden even though more filters exist beyond the cap.
14586
14443
  *
14587
- * This property manages the visibility and content of the "more filters" popover in the UI.
14444
+ * This property manages the visibility of the "more filters" button in the UI.
14588
14445
  *
14589
14446
  * @returns An array of Aggregation objects representing the additional filters available.
14590
14447
  */
14591
14448
  hasMoreFilters = computed(() => {
14592
- const moreFiltersAggregations = this.authorizedFilters()
14593
- .filter((f) => !this.excludeFilters().includes(f.name)) // filter out the excluded filters
14594
- .filter((f) => !this.includeFilters().length || this.includeFilters().includes(f.name)) // exclude filters not included in includeFilters if not empty
14595
- .map((f) => ({ column: f.column, name: f.name }))
14449
+ const moreFiltersAggregations = this.allAuthorizedFilters()
14596
14450
  .toSpliced(0, this.visibleFiltersCount())
14597
14451
  .map((f) => this.aggregationsStore.getAggregation(f.column, "column"));
14598
14452
  return moreFiltersAggregations;
14599
14453
  }, ...(ngDevMode ? [{ debugName: "hasMoreFilters" }] : []));
14600
14454
  /**
14601
- * Computed property that returns a filtered and processed list of authorized filters.
14602
- *
14603
- * This computed signal performs the following operations:
14604
- * 1. Retrieves aggregations from either the component's aggregations input or the app store
14605
- * 2. Filters aggregations based on the route's filter criteria configuration
14606
- * 3. Excludes filters specified in the `excludeFilters` list
14607
- * 4. If `includeFilters` is not empty, only includes filters present in that list
14608
- * 5. Maps the filtered aggregations to objects containing only `name` and `column` properties
14609
- * 6. Limits the result to the number specified by `filtersCount`
14455
+ * The authorized filters rendered as buttons in the bar, limited to the number
14456
+ * specified by `filtersCount`.
14610
14457
  *
14611
14458
  * @returns An array of authorized filter objects, each containing `name` and `column` properties
14612
14459
  */
14613
14460
  authorizedFilters = computed(() => {
14614
- const authorizedFilters = this.aggregationsService
14615
- .getAuthorizedFilters(this.aggregations(), this.includeFilters(), this.excludeFilters())
14616
- .map((f) => ({ name: f.name, column: f.column }))
14617
- .toSpliced(this.filtersCount());
14618
- return authorizedFilters;
14461
+ return this.allAuthorizedFilters().toSpliced(this.filtersCount());
14619
14462
  }, ...(ngDevMode ? [{ debugName: "authorizedFilters" }] : []));
14463
+ /**
14464
+ * Whether some authorized filters exist beyond the `filtersCount` cap.
14465
+ *
14466
+ * Those filters are never rendered in the bar and are only reachable through
14467
+ * the "more" button, which is therefore permanently visible: the overflow
14468
+ * manager must always reserve its space so the last filter button never
14469
+ * overlaps it (`reserveStop`).
14470
+ */
14471
+ hasCappedFilters = computed(() => this.allAuthorizedFilters().length > this.filtersCount(), ...(ngDevMode ? [{ debugName: "hasCappedFilters" }] : []));
14620
14472
  constructor() {
14621
14473
  this.transloco.events$
14622
14474
  .pipe(takeUntilDestroyed(this.destroyRef), debounceTime(100))
14623
14475
  .subscribe(() => this.overflowManagerRef()?.countItems());
14476
+ // Recount the overflow whenever the applied filters or basket change (e.g.
14477
+ // a filter modified or removed from the "more filters" popover). A
14478
+ // FilterButton hidden by the overflow manager (display: none) emits no
14479
+ // resize notification when its natural width changes, so it could fit in
14480
+ // the bar again without the manager knowing. afterRenderEffect guarantees
14481
+ // the DOM already reflects the new state when we measure.
14482
+ afterRenderEffect({
14483
+ read: () => {
14484
+ // track filters and basket changes (getState is reactive here)
14485
+ const { filters, basket } = getState(this.queryParamsStore);
14486
+ void filters;
14487
+ void basket;
14488
+ this.overflowManagerRef()?.countItems();
14489
+ }
14490
+ });
14624
14491
  }
14625
14492
  /**
14626
14493
  * Clears all filters (included baskets) by invoking the clearFilters method on the queryParamsStore.
@@ -14669,8 +14536,8 @@ class FiltersBarComponent {
14669
14536
  });
14670
14537
  }
14671
14538
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: FiltersBarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
14672
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: FiltersBarComponent, isStandalone: true, selector: "filters-bar, FiltersBar, filtersbar", inputs: { class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, morePosition: { classPropertyName: "morePosition", publicName: "morePosition", isSignal: true, isRequired: false, transformFunction: null }, aggregations: { classPropertyName: "aggregations", publicName: "aggregations", isSignal: true, isRequired: false, transformFunction: null }, includeFilters: { classPropertyName: "includeFilters", publicName: "includeFilters", isSignal: true, isRequired: false, transformFunction: null }, excludeFilters: { classPropertyName: "excludeFilters", publicName: "excludeFilters", isSignal: true, isRequired: false, transformFunction: null }, filtersCount: { classPropertyName: "filtersCount", publicName: "filtersCount", isSignal: true, isRequired: false, transformFunction: null }, showMoreFiltersButton: { classPropertyName: "showMoreFiltersButton", publicName: "showMoreFiltersButton", isSignal: true, isRequired: false, transformFunction: null }, direction: { classPropertyName: "direction", publicName: "direction", isSignal: true, isRequired: false, transformFunction: null }, offset: { classPropertyName: "offset", publicName: "offset", isSignal: true, isRequired: false, transformFunction: null }, expandedLevel: { classPropertyName: "expandedLevel", publicName: "expandedLevel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onClearFilters: "onClearFilters", onClearBasket: "onClearBasket" }, host: { listeners: { "click": "handleClick($event)" }, properties: { "class": "cn('block relative', class())" } }, providers: [provideTranslocoScope("filters")], viewQueries: [{ propertyName: "moreButtonRef", first: true, predicate: MoreButtonComponent, descendants: true, isSignal: true }, { propertyName: "filterButtonRefs", predicate: FilterButtonComponent, descendants: true, isSignal: true }, { propertyName: "overflowManagerRef", first: true, predicate: OverflowManagerDirective, descendants: true, isSignal: true }], ngImport: i0, template: `
14673
- <div overflowManager [direction]="direction()" (count)="adjustFiltersCount($event)" class="flex items-end gap-2 rounded-[inherit] bg-inherit">
14539
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: FiltersBarComponent, isStandalone: true, selector: "filters-bar, FiltersBar, filtersbar", inputs: { class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, morePosition: { classPropertyName: "morePosition", publicName: "morePosition", isSignal: true, isRequired: false, transformFunction: null }, aggregations: { classPropertyName: "aggregations", publicName: "aggregations", isSignal: true, isRequired: false, transformFunction: null }, includeFilters: { classPropertyName: "includeFilters", publicName: "includeFilters", isSignal: true, isRequired: false, transformFunction: null }, excludeFilters: { classPropertyName: "excludeFilters", publicName: "excludeFilters", isSignal: true, isRequired: false, transformFunction: null }, filtersCount: { classPropertyName: "filtersCount", publicName: "filtersCount", isSignal: true, isRequired: false, transformFunction: null }, showMoreFiltersButton: { classPropertyName: "showMoreFiltersButton", publicName: "showMoreFiltersButton", isSignal: true, isRequired: false, transformFunction: null }, homepage: { classPropertyName: "homepage", publicName: "homepage", isSignal: true, isRequired: false, transformFunction: null }, direction: { classPropertyName: "direction", publicName: "direction", isSignal: true, isRequired: false, transformFunction: null }, offset: { classPropertyName: "offset", publicName: "offset", isSignal: true, isRequired: false, transformFunction: null }, expandedLevel: { classPropertyName: "expandedLevel", publicName: "expandedLevel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onClearFilters: "onClearFilters", onClearBasket: "onClearBasket" }, host: { listeners: { "click": "handleClick($event)" }, properties: { "class": "cn('block relative min-w-0', class())" } }, providers: [provideTranslocoScope("filters")], viewQueries: [{ propertyName: "moreButtonRef", first: true, predicate: MoreButtonComponent, descendants: true, isSignal: true }, { propertyName: "filterButtonRefs", predicate: FilterButtonComponent, descendants: true, isSignal: true }, { propertyName: "overflowManagerRef", first: true, predicate: OverflowManagerDirective, descendants: true, isSignal: true }], ngImport: i0, template: `
14540
+ <div overflowManager [direction]="direction()" [reserveStop]="hasCappedFilters()" (count)="adjustFiltersCount($event)" class="flex items-end gap-2 rounded-[inherit] bg-inherit">
14674
14541
  @if (hasFilters()) {
14675
14542
  <button
14676
14543
  variant="destructive"
@@ -14717,11 +14584,12 @@ class FiltersBarComponent {
14717
14584
  [position]="morePosition()"
14718
14585
  [includedFilters]="includeFilters()"
14719
14586
  [excludedFilters]="excludeFilters()"
14720
- [aggregations]="aggregations()" />
14587
+ [aggregations]="aggregations()"
14588
+ [homepage]="homepage()" />
14721
14589
  }
14722
14590
  }
14723
14591
  </div>
14724
- `, isInline: true, dependencies: [{ kind: "directive", type: ButtonComponent, selector: "button", inputs: ["class", "variant", "decoration", "scheme", "iconOnly", "size", "solid"] }, { kind: "component", type: MoreButtonComponent, selector: "more-button, MoreButton", inputs: ["count", "position", "includedFilters", "excludedFilters", "aggregations"] }, { kind: "component", type: FilterButtonComponent, selector: "filter-button, FilterButton", inputs: ["name", "column", "position", "offset", "expandedLevel"] }, { kind: "directive", type: OverflowManagerDirective, selector: "[overflowManager]", inputs: ["target", "margin", "direction"], outputs: ["count"] }, { kind: "directive", type: OverflowItemDirective, selector: "[overflowItem]" }, { kind: "directive", type: OverflowStopDirective, selector: "[overflowStop]" }, { kind: "component", type: TrashCanIcon, selector: "trash-can-icon, TrashCanIcon", inputs: ["class"] }, { kind: "component", type: InboxIcon, selector: "inbox-icon, InboxIcon", inputs: ["class"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
14592
+ `, isInline: true, dependencies: [{ kind: "directive", type: ButtonComponent, selector: "button", inputs: ["class", "variant", "decoration", "scheme", "iconOnly", "size", "solid"] }, { kind: "component", type: MoreButtonComponent, selector: "more-button, MoreButton", inputs: ["count", "position", "includedFilters", "excludedFilters", "aggregations", "homepage"] }, { kind: "component", type: FilterButtonComponent, selector: "filter-button, FilterButton", inputs: ["name", "column", "position", "offset", "expandedLevel"] }, { kind: "directive", type: OverflowManagerDirective, selector: "[overflowManager]", inputs: ["target", "margin", "direction", "reserveStop"], outputs: ["count"] }, { kind: "directive", type: OverflowItemDirective, selector: "[overflowItem]" }, { kind: "directive", type: OverflowStopDirective, selector: "[overflowStop]" }, { kind: "component", type: TrashCanIcon, selector: "trash-can-icon, TrashCanIcon", inputs: ["class"] }, { kind: "component", type: InboxIcon, selector: "inbox-icon, InboxIcon", inputs: ["class"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
14725
14593
  }
14726
14594
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: FiltersBarComponent, decorators: [{
14727
14595
  type: Component,
@@ -14741,7 +14609,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
14741
14609
  ],
14742
14610
  providers: [provideTranslocoScope("filters")],
14743
14611
  template: `
14744
- <div overflowManager [direction]="direction()" (count)="adjustFiltersCount($event)" class="flex items-end gap-2 rounded-[inherit] bg-inherit">
14612
+ <div overflowManager [direction]="direction()" [reserveStop]="hasCappedFilters()" (count)="adjustFiltersCount($event)" class="flex items-end gap-2 rounded-[inherit] bg-inherit">
14745
14613
  @if (hasFilters()) {
14746
14614
  <button
14747
14615
  variant="destructive"
@@ -14788,17 +14656,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
14788
14656
  [position]="morePosition()"
14789
14657
  [includedFilters]="includeFilters()"
14790
14658
  [excludedFilters]="excludeFilters()"
14791
- [aggregations]="aggregations()" />
14659
+ [aggregations]="aggregations()"
14660
+ [homepage]="homepage()" />
14792
14661
  }
14793
14662
  }
14794
14663
  </div>
14795
14664
  `,
14796
14665
  host: {
14797
- "[class]": "cn('block relative', class())",
14666
+ "[class]": "cn('block relative min-w-0', class())",
14798
14667
  "(click)": "handleClick($event)"
14799
14668
  }
14800
14669
  }]
14801
- }], ctorParameters: () => [], propDecorators: { class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], morePosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "morePosition", required: false }] }], aggregations: [{ type: i0.Input, args: [{ isSignal: true, alias: "aggregations", required: false }] }], includeFilters: [{ type: i0.Input, args: [{ isSignal: true, alias: "includeFilters", required: false }] }], excludeFilters: [{ type: i0.Input, args: [{ isSignal: true, alias: "excludeFilters", required: false }] }], filtersCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "filtersCount", required: false }] }], showMoreFiltersButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "showMoreFiltersButton", required: false }] }], direction: [{ type: i0.Input, args: [{ isSignal: true, alias: "direction", required: false }] }], offset: [{ type: i0.Input, args: [{ isSignal: true, alias: "offset", required: false }] }], expandedLevel: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandedLevel", required: false }] }], onClearFilters: [{ type: i0.Output, args: ["onClearFilters"] }], onClearBasket: [{ type: i0.Output, args: ["onClearBasket"] }], moreButtonRef: [{ type: i0.ViewChild, args: [i0.forwardRef(() => MoreButtonComponent), { isSignal: true }] }], filterButtonRefs: [{ type: i0.ViewChildren, args: [i0.forwardRef(() => FilterButtonComponent), { isSignal: true }] }], overflowManagerRef: [{ type: i0.ViewChild, args: [i0.forwardRef(() => OverflowManagerDirective), { isSignal: true }] }] } });
14670
+ }], ctorParameters: () => [], propDecorators: { class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], morePosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "morePosition", required: false }] }], aggregations: [{ type: i0.Input, args: [{ isSignal: true, alias: "aggregations", required: false }] }], includeFilters: [{ type: i0.Input, args: [{ isSignal: true, alias: "includeFilters", required: false }] }], excludeFilters: [{ type: i0.Input, args: [{ isSignal: true, alias: "excludeFilters", required: false }] }], filtersCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "filtersCount", required: false }] }], showMoreFiltersButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "showMoreFiltersButton", required: false }] }], homepage: [{ type: i0.Input, args: [{ isSignal: true, alias: "homepage", required: false }] }], direction: [{ type: i0.Input, args: [{ isSignal: true, alias: "direction", required: false }] }], offset: [{ type: i0.Input, args: [{ isSignal: true, alias: "offset", required: false }] }], expandedLevel: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandedLevel", required: false }] }], onClearFilters: [{ type: i0.Output, args: ["onClearFilters"] }], onClearBasket: [{ type: i0.Output, args: ["onClearBasket"] }], moreButtonRef: [{ type: i0.ViewChild, args: [i0.forwardRef(() => MoreButtonComponent), { isSignal: true }] }], filterButtonRefs: [{ type: i0.ViewChildren, args: [i0.forwardRef(() => FilterButtonComponent), { isSignal: true }] }], overflowManagerRef: [{ type: i0.ViewChild, args: [i0.forwardRef(() => OverflowManagerDirective), { isSignal: true }] }] } });
14802
14671
 
14803
14672
  class LabelService {
14804
14673
  appStore = inject(AppStore);
@@ -16187,5 +16056,5 @@ const queryNameResolver = () => {
16187
16056
  * Generated bundle index. Do not edit.
16188
16057
  */
16189
16058
 
16190
- export { AGGREGATIONS_NAMES, AGGREGATIONS_NAMES_PRESET_DEFAULT, APP_FEATURES, AdvancedFiltersComponent, AdvancedSearch, AdvancedSearchComponent, AggregationComponent, AggregationDateComponent, AggregationDateRangeDialogComponent, AggregationListComponent, AggregationTreeComponent, AggregationsService, AggregationsStore, Alert, AlertDialog, AlertsComponent, AppService, AppStore, ApplicationService, ApplicationStore, ArticleEntities, ArticleExtracts, ArticleLabels, ArticleSimilarDocuments, AsideFiltersComponent, AuditFeedbackType, AuditService, AuthGuard, AuthPageComponent, AutocompleteService, BOOKMARKS_CONFIG, BOOKMARKS_OPTIONS, BackdropComponent, BackdropService, BookmarkButtonComponent, BookmarksComponent, COLLECTIONS_CONFIG, COLLECTIONS_OPTIONS, COMPONENTS_FOR_DOCUMENT_TYPE, ChangePasswordComponent, ChildMarkerDirective, CollectionsComponent, CollectionsDialog, DRAWER_COMPONENT, DRAWER_STACK_MAX_COUNT, DateComponent, DeleteCollectionDialog, DidYouMeanComponent, DocumentLocatorComponent, DrawerAdvancedFiltersComponent, DrawerComponent, DrawerNavbarComponent, DrawerPreviewComponent, DrawerService, DrawerStackComponent, DrawerStackService, DropdownInputComponent, DropdownListComponent, ErrorComponent, ExportDialog, ExportService, FILTERS_BREAKPOINT, FILTER_DATE_ALLOW_CUSTOM_RANGE, FeedbackDialogComponent, FileSizePipe, FilterButtonComponent, FiltersBarComponent, HIGHLIGHTS, HighlightWordPipe, InfinityScrollDirective, InlineWorker, JsonMethodPluginService, KeyboardNavigatorDirective, LabelService, LabelsEditDialog, LoadingComponent, MetadataComponent, MissingTermsComponent, MoreButtonComponent, MoreComponent, MultiSelectLabelsComponent, MultiSelectionToolbarComponent, NON_SEARCHABLE_COLUMNS, NON_SEARCHABLE_DEFAULTS, NavbarTabsComponent, NavigationService, NoResultComponent, OpenArticleOnCtrlEnterDirective, OperatorPipe, OverflowItemDirective, OverflowManagerDirective, OverflowStopDirective, OverrideUserDialogComponent, PREVIEW_CONFIG, PagerComponent, PreviewNavigator, PreviewService, PrincipalService, PrincipalStore, QueryParamsStore, QueryService, RECENT_SEARCHES_CONFIG, RECENT_SEARCHES_OPTIONS, ROUTE_COMPONENTS, RecentSearchesComponent, ResetUserSettingsDialogComponent, SAVED_SEARCHES_CONFIG, SAVED_SEARCHES_OPTIONS, SavedSearchDialog, SavedSearchesComponent, SavedSearchesService, SearchFeedbackComponent, SearchInputFooter, SearchService, SelectArticleDirective, SelectArticleOnClickDirective, SelectionHistoryService, SelectionService, SelectionStore, ShowBookmarkDirective, SignInComponent, SortSelectorComponent, SourceComponent, SourceIconPipe, SponsoredResultsComponent, SyslangPipe, THEMES, TextChunkService, ThemeProviderDirective, ThemeSelectorComponent, ThemeStore, ThemeToggleComponent, TranslocoDateImpurePipe, UserProfileDialog, UserProfileFormComponent, UserProfileService, UserSettingsStore, applyThemeToNativeElement, auditInterceptorFn, authInterceptorFn, bodyInterceptorFn, buildQuery, debouncedSignal, errorInterceptorFn, getCurrentPath, getCurrentQueryName, getQueryNameFromRoute, processCssVars, queryNameResolver, signIn, themeColorNameToCssVariable, themeColorsToCssVariables, toastInterceptorFn, withAggregationsFeatures, withAlertsFeatures, withAppFeatures, withApplicationFeatures, withAssistantFeatures, withBasketsFeatures, withBookmarkFeatures, withBootstrapApp, withExtractsFeatures, withFetch, withMultiSelectionFeatures, withPrincipalFeatures, withQueryParamsFeatures, withRecentSearchesFeatures, withSavedSearchesFeatures, withSelectionFeatures, withThemeBodyHook, withThemes, withThemesFeatures, withUserSettingsFeatures };
16059
+ export { AGGREGATIONS_NAMES, AGGREGATIONS_NAMES_PRESET_DEFAULT, APP_FEATURES, AdvancedFiltersComponent, AdvancedSearch, AdvancedSearchComponent, AggregationComponent, AggregationDateComponent, AggregationDateRangeDialogComponent, AggregationListComponent, AggregationPanelComponent, AggregationTreeComponent, AggregationsService, AggregationsStore, Alert, AlertDialog, AlertsComponent, AppService, AppStore, ApplicationService, ApplicationStore, ArticleEntities, ArticleExtracts, ArticleLabels, ArticleSimilarDocuments, AsideFiltersComponent, AuditFeedbackType, AuditService, AuthGuard, AuthPageComponent, AutocompleteService, BOOKMARKS_CONFIG, BOOKMARKS_OPTIONS, BackdropComponent, BackdropService, BookmarkButtonComponent, BookmarksComponent, COLLECTIONS_CONFIG, COLLECTIONS_OPTIONS, COMPONENTS_FOR_DOCUMENT_TYPE, ChangePasswordComponent, ChildMarkerDirective, CollectionsComponent, CollectionsDialog, DRAWER_COMPONENT, DRAWER_STACK_MAX_COUNT, DateComponent, DeleteCollectionDialog, DidYouMeanComponent, DocumentLocatorComponent, DrawerAdvancedFiltersComponent, DrawerComponent, DrawerNavbarComponent, DrawerPreviewComponent, DrawerService, DrawerStackComponent, DrawerStackService, DropdownInputComponent, DropdownListComponent, ErrorComponent, ExportDialog, ExportService, FILTERS_BREAKPOINT, FILTER_DATE_ALLOW_CUSTOM_RANGE, FeedbackDialogComponent, FileSizePipe, FilterButtonComponent, FiltersBarComponent, HIGHLIGHTS, HighlightWordPipe, InfinityScrollDirective, InlineWorker, JsonMethodPluginService, KeyboardNavigatorDirective, LabelService, LabelsEditDialog, LoadingComponent, MetadataComponent, MissingTermsComponent, MoreButtonComponent, MoreComponent, MultiSelectLabelsComponent, MultiSelectionToolbarComponent, NON_SEARCHABLE_COLUMNS, NON_SEARCHABLE_DEFAULTS, NavbarTabsComponent, NavigationService, NoResultComponent, OpenArticleOnCtrlEnterDirective, OperatorPipe, OverflowItemDirective, OverflowManagerDirective, OverflowStopDirective, OverrideUserDialogComponent, PREVIEW_CONFIG, PagerComponent, PreviewNavigator, PreviewService, PrincipalService, PrincipalStore, QueryParamsStore, QueryService, RECENT_SEARCHES_CONFIG, RECENT_SEARCHES_OPTIONS, ROUTE_COMPONENTS, RecentSearchesComponent, ResetUserSettingsDialogComponent, SAVED_SEARCHES_CONFIG, SAVED_SEARCHES_OPTIONS, SavedSearchDialog, SavedSearchesComponent, SavedSearchesService, SearchFeedbackComponent, SearchInputFooter, SearchService, SelectArticleDirective, SelectArticleOnClickDirective, SelectionHistoryService, SelectionService, SelectionStore, ShowBookmarkDirective, SidebarNavComponent, SignInComponent, SortSelectorComponent, SourceComponent, SourceIconPipe, SponsoredResultsComponent, SyslangPipe, THEMES, TextChunkService, ThemeProviderDirective, ThemeSelectorComponent, ThemeStore, ThemeToggleComponent, TranslocoDateImpurePipe, UserProfileDialog, UserProfileFormComponent, UserProfileService, UserSettingsStore, applyThemeToNativeElement, auditInterceptorFn, authInterceptorFn, bodyInterceptorFn, buildQuery, debouncedSignal, errorInterceptorFn, getCurrentPath, getCurrentQueryName, getQueryNameFromRoute, injectRouteNavigation, processCssVars, queryNameResolver, signIn, themeColorNameToCssVariable, themeColorsToCssVariables, toastInterceptorFn, withAggregationsFeatures, withAlertsFeatures, withAppFeatures, withApplicationFeatures, withAssistantFeatures, withBasketsFeatures, withBookmarkFeatures, withBootstrapApp, withExtractsFeatures, withFetch, withMultiSelectionFeatures, withPrincipalFeatures, withQueryParamsFeatures, withRecentSearchesFeatures, withSavedSearchesFeatures, withSelectionFeatures, withThemeBodyHook, withThemes, withThemesFeatures, withUserSettingsFeatures };
16191
16060
  //# sourceMappingURL=sinequa-atomic-angular.mjs.map