@sinequa/atomic-angular 1.2.5 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,152 @@ 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 (Array.isArray(currentFilters.values) && currentFilters.values.length) {
8715
+ // multiple values stored as a string array, e.g. { values: ["alice_martin", "caroline_dubois"] }
8716
+ // (no `filters` sub-array and no single `value`) — mark each matching item as selected
8717
+ currentFilters.values.forEach((value) => {
8718
+ const found = currentItems.find((item) => item.value?.toString().toLocaleLowerCase() === value?.toString().toLocaleLowerCase());
8719
+ if (!found) {
8720
+ currentItems.unshift({ value, display: value, $selected: true });
8721
+ }
8722
+ else {
8723
+ found.$selected = true;
8724
+ }
8725
+ });
8726
+ }
8727
+ else if (currentFilters.value) {
8728
+ const found = currentItems.find((item) => item.value?.toString().toLocaleLowerCase() === currentFilters.value?.toLocaleLowerCase());
8729
+ if (!found) {
8730
+ currentItems.push({ value: currentFilters.value, display: currentFilters.display, $selected: true });
8731
+ }
8732
+ else {
8733
+ found.$selected = true;
8734
+ }
8735
+ }
8736
+ }
8737
+ }
8738
+ return currentItems;
8739
+ }
8740
+ function applyFilters(appliedFilters, agg, clearFn) {
8741
+ const { name: aggName, column: field } = agg;
8742
+ if (appliedFilters.length > 1) {
8743
+ const display = appliedFilters[0].display;
8744
+ if (agg.isDistribution) {
8745
+ queryParamsStore.updateFilter({
8746
+ operator: "or",
8747
+ filters: appliedFilters,
8748
+ name: aggName,
8749
+ field,
8750
+ display,
8751
+ });
8752
+ }
8753
+ else {
8754
+ const values = appliedFilters.map((f) => f.value);
8755
+ queryParamsStore.updateFilter({
8756
+ operator: "in",
8757
+ name: aggName,
8758
+ field,
8759
+ values,
8760
+ display,
8761
+ filters: appliedFilters,
8762
+ });
8763
+ }
8764
+ }
8765
+ else if (appliedFilters.length === 1) {
8766
+ queryParamsStore.updateFilter(appliedFilters[0]);
8767
+ }
8768
+ else {
8769
+ clearFn();
8770
+ }
8771
+ }
8772
+ return {
8773
+ aggregationsStore,
8774
+ queryParamsStore,
8775
+ appStore,
8776
+ aggregationsService,
8777
+ injector,
8778
+ destroyRef,
8779
+ debouncedSearchText,
8780
+ normalizedSearchText,
8781
+ suggests,
8782
+ hasFilters,
8783
+ filtersCount,
8784
+ query,
8785
+ filters,
8786
+ clearSearch,
8787
+ selectItems,
8788
+ addCurrentFiltersToItems,
8789
+ applyFilters,
8790
+ };
8791
+ }
8792
+
8450
8793
  /**
8451
8794
  * Injection token that indicates whether custom date ranges are allowed.
8452
8795
  *
@@ -8466,507 +8809,67 @@ const FILTER_DATE_ALLOW_CUSTOM_RANGE = new InjectionToken("date allow custom ran
8466
8809
  factory: () => true
8467
8810
  });
8468
8811
 
8469
- class AggregationListItemComponent {
8812
+ const options = {
8813
+ year: 'numeric',
8814
+ month: '2-digit',
8815
+ day: '2-digit'
8816
+ };
8817
+ class AggregationDateComponent {
8470
8818
  cn = cn;
8471
- get disabled() {
8472
- return this.node().count === 0 ? "disabled" : null;
8473
- }
8474
- 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" }] : []));
8819
+ /* view queries */
8820
+ dateRangeDialog = viewChild(AggregationDateRangeDialogComponent, ...(ngDevMode ? [{ debugName: "dateRangeDialog" }] : []));
8821
+ /* inputs */
8822
+ name = input(null, ...(ngDevMode ? [{ debugName: "name" }] : []));
8823
+ column = input.required(...(ngDevMode ? [{ debugName: "column" }] : []));
8824
+ id = input(null, ...(ngDevMode ? [{ debugName: "id" }] : []));
8825
+ collapsible = input(false, ...(ngDevMode ? [{ debugName: "collapsible" }] : []));
8826
+ collapsed = input(false, ...(ngDevMode ? [{ debugName: "collapsed" }] : []));
8827
+ searchable = input(undefined, ...(ngDevMode ? [{ debugName: "searchable" }] : []));
8828
+ showFiltersCount = input(false, ...(ngDevMode ? [{ debugName: "showFiltersCount", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
8829
+ title = input({ label: "Date", icon: "far fa-calendar-day" }, ...(ngDevMode ? [{ debugName: "title" }] : []));
8830
+ displayEmptyDistributionIntervals = input(false, ...(ngDevMode ? [{ debugName: "displayEmptyDistributionIntervals" }] : []));
8831
+ /* outputs */
8549
8832
  onSelect = output();
8550
8833
  onApply = output();
8551
8834
  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
- */
8835
+ /* collapse state */
8573
8836
  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
8837
  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" }] : []));
8838
+ /* search state — unused by date component but required by AggregationBaseRefs */
8839
+ searchText = model("", ...(ngDevMode ? [{ debugName: "searchText" }] : []));
8840
+ searchInput = signal(undefined, ...(ngDevMode ? [{ debugName: "searchInput" }] : []));
8841
+ /* composable injects stores/services, wires search effects, provides shared methods */
8842
+ base = injectAggregationBase({
8843
+ name: this.name,
8844
+ column: this.column,
8845
+ searchText: this.searchText,
8846
+ searchInput: this.searchInput,
8847
+ expanded: this.expanded,
8848
+ });
8849
+ /* spread from base */
8850
+ aggregationsService = this.base.aggregationsService;
8851
+ queryParamsStore = this.base.queryParamsStore;
8852
+ hasFilters = this.base.hasFilters;
8853
+ destroyRef = this.base.destroyRef;
8854
+ /* injected services */
8855
+ allowCustomRange = inject(FILTER_DATE_ALLOW_CUSTOM_RANGE);
8856
+ transloco = inject(TranslocoService);
8857
+ /* state */
8588
8858
  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 */
8859
+ validSelection = signal(false, ...(ngDevMode ? [{ debugName: "validSelection" }] : []));
8595
8860
  aggregation = computed(() => {
8596
- // when the aggegation store updates, we need to check if the aggregation is still valid
8597
- getState(this.aggregationsStore);
8598
8861
  const name = this.name();
8599
- const column = this.column();
8600
8862
  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
- }
8863
+ const agg = this.aggregationsService.processAggregation(name, this.column());
8864
+ return {
8865
+ ...agg,
8866
+ items: agg?.items?.filter((item) => item.display !== "custom-range") ?? []
8867
+ };
8610
8868
  }
8611
8869
  return null;
8612
8870
  }, ...(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" }] : []));
8871
+ isEmpty = computed(() => this.aggregation() === null, ...(ngDevMode ? [{ debugName: "isEmpty" }] : []));
8872
+ items = computed(() => this.aggregation()?.items ?? [], ...(ngDevMode ? [{ debugName: "items" }] : []));
8970
8873
  dateOptions = computed(() => translateAggregationToDateOptions(this.aggregation(), this.displayEmptyDistributionIntervals()), ...(ngDevMode ? [{ debugName: "dateOptions" }] : []));
8971
8874
  form = new FormGroup({
8972
8875
  option: new FormControl(null),
@@ -8977,7 +8880,6 @@ class AggregationDateComponent extends AggregationListComponent {
8977
8880
  });
8978
8881
  today = new Date();
8979
8882
  lang = signal(this.transloco.getActiveLang(), ...(ngDevMode ? [{ debugName: "lang" }] : []));
8980
- validSelection = signal(false, ...(ngDevMode ? [{ debugName: "validSelection" }] : []));
8981
8883
  formValue = toSignal(this.form.valueChanges, { initialValue: this.form.value });
8982
8884
  customRangeFrom = computed(() => {
8983
8885
  const from = this.formValue().customRange?.from;
@@ -8988,7 +8890,6 @@ class AggregationDateComponent extends AggregationListComponent {
8988
8890
  return to ? new Date(to).toLocaleDateString(this.lang()) : "";
8989
8891
  }, ...(ngDevMode ? [{ debugName: "customRangeTo" }] : []));
8990
8892
  constructor() {
8991
- super();
8992
8893
  this.transloco.langChanges$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((lang) => {
8993
8894
  this.lang.set(lang);
8994
8895
  });
@@ -9007,17 +8908,6 @@ class AggregationDateComponent extends AggregationListComponent {
9007
8908
  (changes.option !== "custom-range" || changes.customRange?.from !== null || changes.customRange?.to !== null));
9008
8909
  });
9009
8910
  }
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
8911
  select() {
9022
8912
  this.selection.set(true);
9023
8913
  }
@@ -9049,6 +8939,15 @@ class AggregationDateComponent extends AggregationListComponent {
9049
8939
  this.onClear.emit();
9050
8940
  }
9051
8941
  }
8942
+ onHeaderClick(event) {
8943
+ event.preventDefault();
8944
+ const isDate = this.aggregationsService.appStore.isDateColumn(this.aggregation()?.column || "");
8945
+ if (!isDate && this.isEmpty())
8946
+ return;
8947
+ if (this.collapsible()) {
8948
+ this.isCollapsed.update((value) => !value);
8949
+ }
8950
+ }
9052
8951
  selectAndOpenDialog() {
9053
8952
  this.select();
9054
8953
  this.dateRangeDialog()?.open();
@@ -9174,7 +9073,7 @@ class AggregationDateComponent extends AggregationListComponent {
9174
9073
  throw new Error("filters.filterInvalid");
9175
9074
  }
9176
9075
  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" }] });
9076
+ 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
9077
  }
9179
9078
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AggregationDateComponent, decorators: [{
9180
9079
  type: Component,
@@ -9192,8 +9091,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
9192
9091
  IconButtonComponent
9193
9092
  ], host: {
9194
9093
  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 }] }] } });
9094
+ }, 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"] }]
9095
+ }], 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
9096
 
9198
9097
  /**
9199
9098
  * Component that allows users to select a date or a date range for filtering search results.
@@ -9202,7 +9101,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
9202
9101
  */
9203
9102
  class DateComponent extends AggregationDateComponent {
9204
9103
  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" }] });
9104
+ 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
9105
  }
9207
9106
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: DateComponent, decorators: [{
9208
9107
  type: Component,
@@ -9219,7 +9118,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
9219
9118
  FilterXIcon
9220
9119
  ], host: {
9221
9120
  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"] }]
9121
+ }, 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
9122
  }] });
9224
9123
 
9225
9124
  class ArticleEntities {
@@ -9862,6 +9761,8 @@ class AlertDialog {
9862
9761
  if (!this.alert)
9863
9762
  return;
9864
9763
  const q = this.alert.query;
9764
+ // `q.filters` widened to `Filter[] | LegacyFilter[]` upstream, but the
9765
+ // query-params store deliberately deals in `LegacyFilter[]` only.
9865
9766
  const filters = Array.isArray(q.filters) ? q.filters : undefined;
9866
9767
  this.queryParamsStore.patch({ text: q.text, tab: q.tab, basket: q.basket, sort: q.sort, filters, name: q.name });
9867
9768
  this.dialog()?.close();
@@ -10654,6 +10555,15 @@ class SignInComponent {
10654
10555
  destroyRef;
10655
10556
  cn = cn;
10656
10557
  config = globalConfig;
10558
+ /**
10559
+ * True when authentication is handled outside the credentials form — i.e. by the
10560
+ * browser/proxy (`useSSO`) or by an auto-configured OAuth/SAML provider. In those
10561
+ * modes this screen shows a loader instead of a login form and initiates the
10562
+ * handshake automatically by calling `handleLogin()`.
10563
+ */
10564
+ externalAuth = !!(globalConfig.useSSO ||
10565
+ globalConfig.autoOAuthProvider ||
10566
+ globalConfig.autoSAMLProvider);
10657
10567
  class = input(...(ngDevMode ? [undefined, { debugName: "class" }] : []));
10658
10568
  forgotPassword = output();
10659
10569
  username = model("", ...(ngDevMode ? [{ debugName: "username" }] : []));
@@ -10675,6 +10585,30 @@ class SignInComponent {
10675
10585
  expiresSoonNotified = signal(false, ...(ngDevMode ? [{ debugName: "expiresSoonNotified" }] : []));
10676
10586
  constructor(destroyRef) {
10677
10587
  this.destroyRef = destroyRef;
10588
+ // If the user is already authenticated when landing here (e.g. page refresh on
10589
+ // /login, or an external handshake completed before this screen was created),
10590
+ // don't sit on the loader: go straight to the returnUrl.
10591
+ if (this.authenticated()) {
10592
+ const url = this.route.snapshot.queryParams["returnUrl"] || "/";
10593
+ this.router.navigateByUrl(url);
10594
+ }
10595
+ // When authentication is delegated to the browser/proxy (SSO) or an OAuth/SAML
10596
+ // provider, no credentials form is shown: this screen shows a loader and initiates
10597
+ // the handshake automatically by calling `handleLogin()`. If the handshake never
10598
+ // completes, fall back to /error after 5s; the fallback is cancelled as soon as
10599
+ // the login succeeds (the `authenticated` event then drives navigation).
10600
+ if (this.externalAuth && !this.authenticated()) {
10601
+ const timeout = setTimeout(() => {
10602
+ this.router.navigate(["/error"], {
10603
+ queryParams: { returnUrl: this.route.snapshot.queryParams["returnUrl"] }
10604
+ });
10605
+ }, 5000);
10606
+ destroyRef.onDestroy(() => clearTimeout(timeout));
10607
+ this.handleLogin().then(result => {
10608
+ if (result)
10609
+ clearTimeout(timeout);
10610
+ });
10611
+ }
10678
10612
  effect(() => {
10679
10613
  const principal = getState(this.principalStore);
10680
10614
  if (this.authenticated() && principal && !this.expiresSoonNotified()) {
@@ -10720,14 +10654,16 @@ class SignInComponent {
10720
10654
  this.router.navigate(["/login"]);
10721
10655
  }
10722
10656
  async handleLogin() {
10723
- login().then((result) => {
10657
+ return login().then((result) => {
10724
10658
  if (result) {
10725
10659
  this.auditService.notifyLogin();
10726
10660
  }
10661
+ return result;
10727
10662
  }).catch(error => {
10728
10663
  warn("An error occurred while logging in", error);
10729
10664
  this.auditService.notify({ type: 'Login_Denied' });
10730
10665
  this.router.navigate(["error"]);
10666
+ return false;
10731
10667
  });
10732
10668
  }
10733
10669
  async handleLoginWithCredentials() {
@@ -10770,7 +10706,7 @@ class SignInComponent {
10770
10706
  }
10771
10707
  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
10708
  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()) {
10709
+ @if (!authenticated() && !externalAuth) {
10774
10710
  <Card
10775
10711
  hover="no"
10776
10712
  cdkTrapFocus
@@ -10781,60 +10717,54 @@ class SignInComponent {
10781
10717
  </CardHeader>
10782
10718
 
10783
10719
  <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
- }
10720
+ <!-- authentication using credentials -->
10721
+ <div class="grid gap-2">
10722
+ <label class="text-sm font-medium" for="username">{{
10723
+ "login.username" | transloco
10724
+ }}</label>
10725
+ <input
10726
+ id="username"
10727
+ type="text"
10728
+ required
10729
+ [(ngModel)]="username"
10730
+ (keydown.enter)="handleLoginWithCredentials()" />
10731
+ </div>
10732
+
10733
+ <div class="grid gap-2">
10734
+ <label class="text-sm font-medium" for="password">{{
10735
+ "login.password" | transloco
10736
+ }}</label>
10737
+ <input
10738
+ id="password"
10739
+ type="password"
10740
+ required
10741
+ [(ngModel)]="password"
10742
+ (keydown.enter)="handleLoginWithCredentials()" />
10743
+ </div>
10744
+
10745
+ <span
10746
+ class="text-muted-foreground cursor-pointer justify-self-start text-xs hover:underline"
10747
+ role="button"
10748
+ tabindex="0"
10749
+ (click)="forgotPassword.emit()"
10750
+ (keydown.enter)="forgotPassword.emit()">
10751
+ {{ "login.forgotPassword" | transloco }}
10752
+ </span>
10753
+ <button variant="primary"
10754
+ [disabled]="!isValid()"
10755
+ (click)="handleLoginWithCredentials()">
10756
+ {{ "login.connect" | transloco }}
10757
+ </button>
10832
10758
  </CardContent>
10833
10759
  </Card>
10834
10760
  } @else {
10835
- <app-wait />
10761
+ <div class="flex h-dvh w-full items-center justify-center">
10762
+ <div class="flex flex-col items-center space-y-4">
10763
+ <span class="loader"></span>
10764
+ </div>
10765
+ </div>
10836
10766
  }
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" }] });
10767
+ `, 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
10768
  }
10839
10769
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: SignInComponent, decorators: [{
10840
10770
  type: Component,
@@ -10847,10 +10777,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
10847
10777
  ButtonComponent,
10848
10778
  CardComponent,
10849
10779
  CardHeaderComponent,
10850
- CardContentComponent,
10851
- LoadingComponent
10780
+ CardContentComponent
10852
10781
  ], providers: [provideTranslocoScope("login")], template: `
10853
- @if (!authenticated()) {
10782
+ @if (!authenticated() && !externalAuth) {
10854
10783
  <Card
10855
10784
  hover="no"
10856
10785
  cdkTrapFocus
@@ -10861,62 +10790,56 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
10861
10790
  </CardHeader>
10862
10791
 
10863
10792
  <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
- }
10793
+ <!-- authentication using credentials -->
10794
+ <div class="grid gap-2">
10795
+ <label class="text-sm font-medium" for="username">{{
10796
+ "login.username" | transloco
10797
+ }}</label>
10798
+ <input
10799
+ id="username"
10800
+ type="text"
10801
+ required
10802
+ [(ngModel)]="username"
10803
+ (keydown.enter)="handleLoginWithCredentials()" />
10804
+ </div>
10805
+
10806
+ <div class="grid gap-2">
10807
+ <label class="text-sm font-medium" for="password">{{
10808
+ "login.password" | transloco
10809
+ }}</label>
10810
+ <input
10811
+ id="password"
10812
+ type="password"
10813
+ required
10814
+ [(ngModel)]="password"
10815
+ (keydown.enter)="handleLoginWithCredentials()" />
10816
+ </div>
10817
+
10818
+ <span
10819
+ class="text-muted-foreground cursor-pointer justify-self-start text-xs hover:underline"
10820
+ role="button"
10821
+ tabindex="0"
10822
+ (click)="forgotPassword.emit()"
10823
+ (keydown.enter)="forgotPassword.emit()">
10824
+ {{ "login.forgotPassword" | transloco }}
10825
+ </span>
10826
+ <button variant="primary"
10827
+ [disabled]="!isValid()"
10828
+ (click)="handleLoginWithCredentials()">
10829
+ {{ "login.connect" | transloco }}
10830
+ </button>
10912
10831
  </CardContent>
10913
10832
  </Card>
10914
10833
  } @else {
10915
- <app-wait />
10834
+ <div class="flex h-dvh w-full items-center justify-center">
10835
+ <div class="flex flex-col items-center space-y-4">
10836
+ <span class="loader"></span>
10837
+ </div>
10838
+ </div>
10916
10839
  }
10917
10840
  `, host: {
10918
10841
  "[class]": "cn('grid h-dvh w-full place-content-center', class())"
10919
- }, styles: ["input{background-color:var(--background)}\n"] }]
10842
+ }, 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
10843
  }], 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
10844
 
10922
10845
  class AuthPageComponent {
@@ -11308,7 +11231,7 @@ class OverrideUserDialogComponent {
11308
11231
  }
11309
11232
  }
11310
11233
  handleOverrideUser(username, domain) {
11311
- const { useSSO, createRoutes } = globalConfig;
11234
+ const { useSSO, createRoutes, useCredentials } = globalConfig;
11312
11235
  if (username === undefined || domain === undefined) {
11313
11236
  setGlobalConfig({ userOverrideActive: false, userOverride: undefined });
11314
11237
  }
@@ -11316,7 +11239,7 @@ class OverrideUserDialogComponent {
11316
11239
  setGlobalConfig({ userOverrideActive: true, userOverride: { username, domain } });
11317
11240
  }
11318
11241
  // Login with the new user
11319
- if (useSSO) {
11242
+ if (useSSO && !useCredentials) {
11320
11243
  this.appService
11321
11244
  .initialize(createRoutes)
11322
11245
  .then(() => {
@@ -11341,6 +11264,7 @@ class OverrideUserDialogComponent {
11341
11264
  })
11342
11265
  .catch((err) => {
11343
11266
  error("An error occured while overriding (initialize)", err);
11267
+ notify.error(err.message, { duration: 2000 });
11344
11268
  setGlobalConfig({ userOverrideActive: false, userOverride: undefined });
11345
11269
  });
11346
11270
  }
@@ -11898,11 +11822,16 @@ class DrawerAdvancedFiltersComponent extends DrawerComponent {
11898
11822
  text = "";
11899
11823
  constructor() {
11900
11824
  super();
11901
- this.getFirstPageQuery();
11825
+ effect(() => {
11826
+ getState(this.appStore);
11827
+ const query = this.appStore.getDefaultQuery();
11828
+ if (query?.name) {
11829
+ this.getFirstPageQuery(query?.name);
11830
+ }
11831
+ });
11902
11832
  }
11903
- async getFirstPageQuery() {
11904
- const query = this.appStore.getDefaultQuery() || { name: "_default" };
11905
- const response = await fetchQuery({ isFirstPage: true, name: query.name });
11833
+ async getFirstPageQuery(queryName) {
11834
+ const response = await fetchQuery({ isFirstPage: true, name: queryName });
11906
11835
  this.aggregations.set(response.aggregations);
11907
11836
  }
11908
11837
  onTabChange(tab) {
@@ -13037,201 +12966,156 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
13037
12966
  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
12967
  }], 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
12968
 
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);
12969
+ class AggregationPanelComponent {
12970
+ /* collapse */
12971
+ id = input(null, ...(ngDevMode ? [{ debugName: "id" }] : []));
12972
+ collapsible = input(false, ...(ngDevMode ? [{ debugName: "collapsible" }] : []));
12973
+ collapsed = input(false, ...(ngDevMode ? [{ debugName: "collapsed" }] : []));
12974
+ isDate = input(false, ...(ngDevMode ? [{ debugName: "isDate" }] : []));
12975
+ isEmpty = input(false, ...(ngDevMode ? [{ debugName: "isEmpty" }] : []));
12976
+ /* aggregation data — used for label, search input id and load-more condition */
12977
+ aggregation = input(null, ...(ngDevMode ? [{ debugName: "aggregation" }] : []));
12978
+ /* header button state (driven by parent) */
12979
+ showFiltersCount = input(false, ...(ngDevMode ? [{ debugName: "showFiltersCount", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
12980
+ filtersCount = input(0, ...(ngDevMode ? [{ debugName: "filtersCount" }] : []));
12981
+ hasFilters = input(false, ...(ngDevMode ? [{ debugName: "hasFilters" }] : []));
12982
+ selection = input(false, ...(ngDevMode ? [{ debugName: "selection" }] : []));
12983
+ isAllSelected = input(false, ...(ngDevMode ? [{ debugName: "isAllSelected" }] : []));
12984
+ /* search */
12985
+ searchText = model("", ...(ngDevMode ? [{ debugName: "searchText" }] : []));
12986
+ itemsLength = input(0, ...(ngDevMode ? [{ debugName: "itemsLength" }] : []));
12987
+ /* load more */
12988
+ hasMore = input(false, ...(ngDevMode ? [{ debugName: "hasMore" }] : []));
12989
+ searchedItemsLength = input(0, ...(ngDevMode ? [{ debugName: "searchedItemsLength" }] : []));
12990
+ /* outputs — parent reacts to user actions */
12991
+ cleared = output();
12992
+ applied = output();
12993
+ allSelected = output();
12994
+ allUnselected = output();
12995
+ loadedMore = output();
12996
+ /* internal collapse state */
12997
+ isCollapsed = linkedSignal(() => this.collapsed(), ...(ngDevMode ? [{ debugName: "isCollapsed" }] : []));
12998
+ expanded = computed(() => (this.isCollapsed() ? null : ""), ...(ngDevMode ? [{ debugName: "expanded" }] : []));
12999
+ searchInput = viewChild("searchInput", ...(ngDevMode ? [{ debugName: "searchInput" }] : []));
13000
+ isInPopover = !!(inject(PopoverContentComponent, { optional: true }) ||
13001
+ inject(DropdownContentComponent, { optional: true }) ||
13002
+ inject(DropdownDirective, { optional: true }));
13003
+ constructor() {
13004
+ effect(() => {
13005
+ if (this.isInPopover && this.searchInput()?.nativeElement && this.expanded() !== null) {
13006
+ setTimeout(() => this.searchInput()?.nativeElement.focus(), 0);
13099
13007
  }
13100
13008
  });
13101
13009
  }
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;
13010
+ onHeaderClick(event) {
13011
+ if (!this.isDate() && this.isEmpty()) {
13012
+ event.preventDefault();
13112
13013
  return;
13113
13014
  }
13114
- this.onOpen.emit(node);
13115
- }
13116
- onTextClick(event) {
13117
- if (this.quickFilter()) {
13118
- this.select(this.node(), event, true);
13119
- this.onFilter.emit();
13015
+ if (this.collapsible()) {
13016
+ this.isCollapsed.update((v) => !v);
13120
13017
  }
13018
+ event.preventDefault();
13121
13019
  }
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());
13020
+ onToggle(event) {
13021
+ const e = event;
13022
+ this.isCollapsed.set(e.newState === "closed");
13023
+ }
13024
+ clearSearch(e) {
13025
+ e.stopImmediatePropagation();
13026
+ this.searchText.set("");
13138
13027
  }
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" }] });
13028
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AggregationPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
13029
+ 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
13030
  }
13142
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AggregationTreeItemComponent, decorators: [{
13031
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AggregationPanelComponent, decorators: [{
13143
13032
  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 }] }] } });
13033
+ args: [{ selector: "AggregationPanel, aggregation-panel", standalone: true, imports: [
13034
+ FormsModule,
13035
+ ButtonComponent,
13036
+ SyslangPipe,
13037
+ TranslocoPipe,
13038
+ BadgeComponent,
13039
+ ChevronRightIcon,
13040
+ InputGroupInput,
13041
+ InputGroupComponent,
13042
+ InputGroupAddonComponent,
13043
+ SearchIcon,
13044
+ FilterIcon,
13045
+ FaIconComponent,
13046
+ FilterXIcon,
13047
+ SquareCheckIcon,
13048
+ SquareIcon,
13049
+ XMarkIcon,
13050
+ IconButtonComponent,
13051
+ ], 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" }]
13052
+ }], 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
13053
 
13150
13054
  class AggregationTreeComponent {
13151
13055
  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);
13056
+ /* inputs */
13172
13057
  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
13058
  id = input(null, ...(ngDevMode ? [{ debugName: "id" }] : []));
13178
13059
  name = input.required(...(ngDevMode ? [{ debugName: "name" }] : []));
13179
13060
  column = input.required(...(ngDevMode ? [{ debugName: "column" }] : []));
13061
+ collapsible = input(false, ...(ngDevMode ? [{ debugName: "collapsible" }] : []));
13062
+ collapsed = input(false, ...(ngDevMode ? [{ debugName: "collapsed" }] : []));
13063
+ searchable = input(undefined, ...(ngDevMode ? [{ debugName: "searchable" }] : []));
13064
+ showFiltersCount = input(false, ...(ngDevMode ? [{ debugName: "showFiltersCount", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
13180
13065
  expandedLevel = input(undefined, ...(ngDevMode ? [{ debugName: "expandedLevel", transform: (v) => {
13181
13066
  const n = numberAttribute(v);
13182
13067
  return Number.isNaN(n) ? undefined : n;
13183
- } }] : [{ transform: (v) => {
13068
+ } }] : [{
13069
+ transform: (v) => {
13184
13070
  const n = numberAttribute(v);
13185
13071
  return Number.isNaN(n) ? undefined : n;
13186
- } }]));
13072
+ },
13073
+ }]));
13074
+ /* outputs */
13187
13075
  onSelect = output();
13188
13076
  onApply = output();
13189
13077
  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" }] : []));
13078
+ /* view queries */
13079
+ virtualItems = viewChildren("virtualItem", ...(ngDevMode ? [{ debugName: "virtualItems" }] : []));
13080
+ scrollElement = viewChild("scrollElement", ...(ngDevMode ? [{ debugName: "scrollElement" }] : []));
13081
+ /* selection state */
13226
13082
  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 */
13083
+ isAllSelected = signal(false, ...(ngDevMode ? [{ debugName: "isAllSelected" }] : []));
13084
+ /* search state */
13085
+ searchText = model("", ...(ngDevMode ? [{ debugName: "searchText" }] : []));
13086
+ /* composable — injects stores/services, wires search effects, provides shared methods */
13087
+ base = injectAggregationBase({
13088
+ name: this.name,
13089
+ column: this.column,
13090
+ searchText: this.searchText,
13091
+ });
13092
+ /* spread from base */
13093
+ aggregationsStore = this.base.aggregationsStore;
13094
+ queryParamsStore = this.base.queryParamsStore;
13095
+ appStore = this.base.appStore;
13096
+ aggregationsService = this.base.aggregationsService;
13097
+ injector = this.base.injector;
13098
+ destroyRef = this.base.destroyRef;
13099
+ debouncedSearchText = this.base.debouncedSearchText;
13100
+ normalizedSearchText = this.base.normalizedSearchText;
13101
+ suggests = this.base.suggests;
13102
+ hasFilters = this.base.hasFilters;
13103
+ filtersCount = this.base.filtersCount;
13104
+ query = this.base.query;
13105
+ filters = this.base.filters;
13106
+ /* features from appStore */
13107
+ showCount = computed(() => this.appStore.general()?.features?.showAggregationItemCount ?? false, ...(ngDevMode ? [{ debugName: "showCount" }] : []));
13108
+ quickFilter = computed(() => this.appStore.general()?.features?.quickFilter, ...(ngDevMode ? [{ debugName: "quickFilter" }] : []));
13109
+ isDate = computed(() => this.appStore.isDateColumn(this.aggregation()?.column || ""), ...(ngDevMode ? [{ debugName: "isDate" }] : []));
13110
+ /* virtualizer */
13111
+ virtualizer = injectVirtualizer(() => ({
13112
+ count: this.items().length,
13113
+ estimateSize: () => 32,
13114
+ scrollElement: this.scrollElement(),
13115
+ }));
13116
+ #measureItems = effect(() => this.virtualItems().forEach((el) => this.virtualizer.measureElement(el.nativeElement)), ...(ngDevMode ? [{ debugName: "#measureItems" }] : []));
13117
+ linkChildren = computed(() => this.appStore.general()?.features?.filterLinkChildren, ...(ngDevMode ? [{ debugName: "linkChildren" }] : []));
13233
13118
  aggregation = computed(() => {
13234
- // when the aggegation store updates, we need to check if the aggregation is still valid
13235
13119
  getState(this.aggregationsStore);
13236
13120
  const name = this.name();
13237
13121
  const column = this.column();
@@ -13241,21 +13125,17 @@ class AggregationTreeComponent {
13241
13125
  if (!agg.isTree) {
13242
13126
  error("The aggregation tree component does not support list aggregations. Please use the <Aggregation /> component instead.");
13243
13127
  }
13244
- // overrides "expandedLevel" from custom JSON file
13245
13128
  const expandedLevel = this.expandedLevel() ?? agg.expandedLevel;
13246
13129
  if (expandedLevel) {
13247
13130
  this.expandItems(agg.items, expandedLevel);
13248
13131
  }
13249
- // overrides "searchable" properties with the input if any
13250
13132
  agg.searchable = this.searchable() ?? agg.searchable;
13251
13133
  return agg;
13252
13134
  }
13253
13135
  }
13254
13136
  return null;
13255
13137
  }, ...(ngDevMode ? [{ debugName: "aggregation" }] : []));
13256
- /* items of the aggregation */
13257
13138
  items = computed(() => {
13258
- // when the aggegation store updates, we need to check if the aggregation is still valid
13259
13139
  getState(this.aggregationsStore);
13260
13140
  const agg = this.aggregation();
13261
13141
  const searchedItems = this.searchedItems();
@@ -13266,57 +13146,17 @@ class AggregationTreeComponent {
13266
13146
  else if (agg?.items) {
13267
13147
  res = this.addCurrentFiltersToItems();
13268
13148
  }
13269
- // use session storage to keep the selected statuses
13270
13149
  const sessionAggItem = sessionStorage.getItem(`agg-${agg?.column}`);
13271
13150
  const sessionAgg = JSON.parse(sessionAggItem || "[]");
13272
13151
  return this.processAggregations(sessionAgg.length ? this.setSelected(res, sessionAgg) : res);
13273
13152
  }, ...(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
13153
  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
13154
  searchedItems = computed(() => {
13313
13155
  if (!this.suggests())
13314
13156
  return [];
13315
- // if the aggregation is a tree, we transform the suggestions into tree nodes
13316
13157
  if (this.aggregation()?.isTree) {
13317
13158
  return suggestionsToTreeAggregationNodes(this.suggests(), this.searchText());
13318
13159
  }
13319
- // if the aggregation is not a tree, we return the suggestions as is
13320
13160
  return this.suggests()?.map((suggest) => ({
13321
13161
  name: this.name(),
13322
13162
  value: suggest.normalized || suggest.display || "",
@@ -13324,41 +13164,18 @@ class AggregationTreeComponent {
13324
13164
  column: suggest.category,
13325
13165
  count: Number(suggest.frequency),
13326
13166
  $selected: false,
13327
- items: []
13167
+ items: [],
13328
13168
  }));
13329
13169
  }, ...(ngDevMode ? [{ debugName: "searchedItems" }] : []));
13330
- linkChildren = computed(() => this.appStore.general()?.features?.filterLinkChildren, ...(ngDevMode ? [{ debugName: "linkChildren" }] : []));
13331
- query;
13332
- filters = signal([], ...(ngDevMode ? [{ debugName: "filters" }] : []));
13333
13170
  constructor() {
13334
- this.query = buildQuery();
13335
13171
  effect(() => {
13336
- // if the aggregation store changes, remove previous session storage
13337
13172
  getState(this.aggregationsStore);
13338
13173
  sessionStorage.removeItem(`agg-${this.column()}`);
13339
13174
  });
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
13175
  effect(() => {
13358
13176
  this.filters.set(this.getFilters());
13359
13177
  });
13360
13178
  this.destroyRef.onDestroy(() => {
13361
- // If the popover is closed with unapplied selections, reset state so it doesn't persist when reopening
13362
13179
  sessionStorage.removeItem(`agg-${this.aggregation()?.column}`);
13363
13180
  if (this.selection()) {
13364
13181
  const unselect = (items) => {
@@ -13373,40 +13190,6 @@ class AggregationTreeComponent {
13373
13190
  }
13374
13191
  });
13375
13192
  }
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
13193
  clear() {
13411
13194
  const agg = this.aggregation();
13412
13195
  if (agg) {
@@ -13426,72 +13209,25 @@ class AggregationTreeComponent {
13426
13209
  this.onClear.emit();
13427
13210
  }
13428
13211
  }
13429
- /**
13430
- * Select all filters for the aggregation column.
13431
- */
13432
13212
  selectAll() {
13433
13213
  if (this.items().length) {
13434
- this.selectItems(this.items(), true);
13214
+ this.base.selectItems(this.items(), true, true);
13435
13215
  this.selection.set(true);
13436
13216
  this.isAllSelected.set(true);
13437
13217
  }
13438
13218
  }
13439
- /**
13440
- * Unselect all filters for the aggregation column.
13441
- */
13442
13219
  unselectAll() {
13443
13220
  if (this.items().length) {
13444
- this.selectItems(this.items(), false);
13221
+ this.base.selectItems(this.items(), false, true);
13445
13222
  this.select();
13446
13223
  this.isAllSelected.set(false);
13447
13224
  }
13448
13225
  }
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
13226
  apply(overrideFilters) {
13460
13227
  sessionStorage.setItem(`agg-${this.aggregation()?.column}`, JSON.stringify([...this.items()]));
13461
13228
  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
- }
13229
+ const agg = this.aggregation();
13230
+ this.base.applyFilters(filters, agg, () => this.clear());
13495
13231
  this.searchText.set("");
13496
13232
  this.selection.set(false);
13497
13233
  this.onApply.emit();
@@ -13504,214 +13240,118 @@ class AggregationTreeComponent {
13504
13240
  }
13505
13241
  async open(node) {
13506
13242
  const q = this.queryParamsStore.getQuery();
13507
- delete q.filters; // remove filters to get all items
13243
+ delete q.filters;
13508
13244
  const agg = await firstValueFrom(this.aggregationsService.open(q, this.aggregation(), node));
13509
13245
  node.$opened = true;
13510
13246
  this.aggregationsStore.updateAggregation(agg);
13511
13247
  }
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
13248
  select() {
13524
- const selectedItems = this.getFlattenTreeItems().filter(item => item.$selected);
13249
+ const selectedItems = this.getFlattenTreeItems().filter((item) => item.$selected);
13525
13250
  this.onSelect.emit(selectedItems);
13526
- // Keep apply visible if items are selected, or if active filters exist (user may be deselecting to clear)
13527
13251
  this.selection.set(selectedItems.length > 0 || this.hasFilters());
13528
13252
  this.verifySelected();
13529
13253
  sessionStorage.setItem(`agg-${this.aggregation()?.column}`, JSON.stringify([...this.items()]));
13530
13254
  }
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();
13255
+ /* item-level methods — called from the ng-template */
13256
+ treeItemName(item) {
13257
+ const value = item.display || item.value;
13258
+ return typeof value === "string" ? value : `${value}`;
13259
+ }
13260
+ isTreeItemFiltered(item, field) {
13261
+ const filters = this.queryParamsStore.getFilter({ field: field ?? undefined, name: this.treeItemName(item) });
13262
+ if (!filters)
13263
+ return false;
13264
+ const values = [item.value, `/${item.$path}/*`];
13265
+ return (values.some((v) => v === filters.value) ||
13266
+ !!(filters.values?.length && filters.values.some((value) => values.some((v) => v === value))));
13267
+ }
13268
+ treeItemLevel(item) {
13269
+ const level = (item.$level ?? 0) - 1 + (!item.hasChildren ? 1 : 0);
13270
+ return item.hasChildren === false ? level + 1 : level;
13271
+ }
13272
+ treeChildrenPath(item, parentPath) {
13273
+ return parentPath.concat(`/${item.$path}/*`);
13274
+ }
13275
+ selectTreeItem(node, parent, e, updateChildren = false) {
13276
+ e?.stopImmediatePropagation();
13277
+ const selected = !node.$selected && !node.$selectedVisually;
13278
+ node.$selected = selected;
13279
+ node.$selectedVisually = false;
13280
+ if (updateChildren)
13281
+ this.selectTreeItemChildren(node.items, node.$selected);
13282
+ if (parent)
13283
+ this.handleTreeChildSelect(parent, node);
13284
+ this.select();
13285
+ }
13286
+ toggleTreeNode(e, node) {
13287
+ e.preventDefault();
13288
+ e.stopImmediatePropagation();
13289
+ if (node.items && node.$opened) {
13290
+ node.$opened = false;
13539
13291
  return;
13540
13292
  }
13541
- if (this.collapsible()) {
13542
- this.isCollapsed.update((value) => !value);
13293
+ if (node.items && !node.$opened) {
13294
+ node.$opened = true;
13295
+ return;
13296
+ }
13297
+ this.open(node);
13298
+ }
13299
+ onTreeItemTextClick(node, parent, event) {
13300
+ if (this.quickFilter()) {
13301
+ this.selectTreeItem(node, parent, event, true);
13302
+ this.apply();
13543
13303
  }
13544
- event.preventDefault();
13545
13304
  }
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
13305
  getFilters() {
13555
13306
  if (this.aggregation()?.isTree) {
13556
13307
  return this.getFiltersForTree();
13557
13308
  }
13558
13309
  return this.getFiltersForList();
13559
13310
  }
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
13311
  getFiltersForTree() {
13570
13312
  const { name, column: field } = this.aggregation() || {};
13571
- if (!name || !field) {
13313
+ if (!name || !field)
13572
13314
  return [];
13573
- }
13574
13315
  const items = this.getFlattenTreeItems()
13575
13316
  .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
- }
13317
+ .map((item) => (item.$path ? `/${item.$path}/*` : ""));
13318
+ const notSelectedItems = this.getFlattenTreeItems()
13319
+ .filter((item) => !item.$selected)
13320
+ .map((item) => (item.$path ? `/${item.$path}/*` : ""));
13321
+ const currentFilters = this.queryParamsStore.getFilter({ field, name })?.values || [];
13322
+ if (currentFilters) {
13323
+ const filteredCurrentFilters = currentFilters.filter((filter) => !notSelectedItems.includes(filter));
13324
+ if (filteredCurrentFilters.length > 0) {
13325
+ items.push(...filteredCurrentFilters);
13682
13326
  }
13683
13327
  }
13684
- return currentItems;
13328
+ const uniqueItems = Array.from(new Set(items));
13329
+ if (uniqueItems.length === 0)
13330
+ return [];
13331
+ return [{ operator: "in", name, field, values: uniqueItems, display: uniqueItems[0] }];
13685
13332
  }
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
- });
13333
+ getFiltersForList() {
13334
+ const items = this.addCurrentFiltersToItems().filter((item) => item.$selected);
13335
+ const searchedItems = this.searchedItems().filter((item) => item.$selected);
13336
+ const currentItems = [...items, ...searchedItems];
13337
+ const { column, name, isDistribution = false } = this.aggregation() || {};
13338
+ return currentItems.map((item) => this.aggregationsService.toFilter(item, column, name, isDistribution));
13339
+ }
13340
+ getFlattenTreeItems() {
13341
+ const flattenItems = (items) => items.reduce((flat, item) => flat.concat(item, item.items ? flattenItems(item.items) : []), []);
13342
+ const searchedItemsFiltered = (this.searchedItems() || []).filter((item) => "items" in item);
13343
+ return [
13344
+ ...flattenItems(searchedItemsFiltered),
13345
+ ...flattenItems(this.aggregation()?.items || []),
13346
+ ];
13347
+ }
13348
+ addCurrentFiltersToItems() {
13349
+ return this.base.addCurrentFiltersToItems(this.aggregation());
13702
13350
  }
13703
- /**
13704
- * Check whether all items are selected and update isAllSelected accordingly
13705
- */
13706
13351
  verifySelected() {
13707
- const someItemsUnselected = (items) => {
13708
- return items.some((item) => !item.$selected || (item.items?.length && someItemsUnselected(item.items)));
13709
- };
13352
+ const someItemsUnselected = (items) => items.some((item) => !item.$selected || (item.items?.length && someItemsUnselected(item.items)));
13710
13353
  this.isAllSelected.set(!someItemsUnselected(this.items()));
13711
13354
  }
13712
- /**
13713
- * set @items $selected and $selectedVisually to the values from @savedItems
13714
- */
13715
13355
  setSelected(items, savedItems) {
13716
13356
  return items.map((item) => {
13717
13357
  const savedItem = savedItems.find((i) => i.value === item.value);
@@ -13729,9 +13369,8 @@ class AggregationTreeComponent {
13729
13369
  if (!this.linkChildren())
13730
13370
  return items;
13731
13371
  items.forEach((item) => {
13732
- if (item.items?.length) {
13372
+ if (item.items?.length)
13733
13373
  this.selectVisually(item.items, item.$selected || false);
13734
- }
13735
13374
  });
13736
13375
  return items;
13737
13376
  }
@@ -13742,39 +13381,49 @@ class AggregationTreeComponent {
13742
13381
  this.selectVisually(item.items, item.$selected || item.$selectedVisually);
13743
13382
  });
13744
13383
  }
13745
- onToggle(event) {
13746
- const e = event;
13747
- this.isCollapsed.set(e.newState === "closed");
13384
+ selectTreeItemChildren(items, select) {
13385
+ if (!this.linkChildren() || !items?.length)
13386
+ return;
13387
+ items.forEach((item) => {
13388
+ item.$selectedVisually = select;
13389
+ if (select)
13390
+ item.$selected = false;
13391
+ if (item.items?.length)
13392
+ this.selectTreeItemChildren(item.items, select);
13393
+ });
13394
+ }
13395
+ handleTreeChildSelect(parent, child) {
13396
+ if (this.linkChildren() && !child.$selected && parent.items.some((i) => i.$selectedVisually)) {
13397
+ parent.items.forEach((i) => {
13398
+ if (i !== child) {
13399
+ i.$selectedVisually = false;
13400
+ i.$selected = true;
13401
+ }
13402
+ });
13403
+ }
13404
+ if (this.linkChildren() && parent.items.some((i) => !i.$selectedVisually && !i.$selected)) {
13405
+ parent.$selected = false;
13406
+ parent.$selectedVisually = false;
13407
+ }
13748
13408
  }
13749
13409
  expandItems(items, expandedLevel) {
13750
- this.traverse(items, (lineage, node, level) => {
13751
- if (!node.$opened && node.items?.length >= 0 && level < expandedLevel) {
13410
+ this.traverse(items, (_lineage, node, level) => {
13411
+ if (!node.$opened && node.items?.length > 0 && level < expandedLevel) {
13752
13412
  node.$opened = true;
13753
13413
  }
13754
- // if(node.$opened && level >= expandedLevel) {
13755
- // node.$opened = false;
13756
- // }
13757
13414
  return false;
13758
13415
  });
13759
13416
  }
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
13417
  traverse(nodes, callback) {
13766
- if (!nodes || nodes.length === 0) {
13418
+ if (!nodes || nodes.length === 0)
13767
13419
  return false;
13768
- }
13769
- if (!callback) {
13420
+ if (!callback)
13770
13421
  return false;
13771
- }
13772
13422
  const lineage = [];
13773
13423
  const stack = [];
13774
13424
  let _i = nodes.length;
13775
- while (_i--) {
13425
+ while (_i--)
13776
13426
  stack.push(nodes[_i]);
13777
- }
13778
13427
  while (stack.length) {
13779
13428
  const node = stack.pop();
13780
13429
  if (!node) {
@@ -13782,54 +13431,248 @@ class AggregationTreeComponent {
13782
13431
  }
13783
13432
  else {
13784
13433
  lineage.push(node);
13785
- if (callback(lineage, node, lineage.length - 1)) {
13434
+ if (callback(lineage, node, lineage.length - 1))
13786
13435
  return true;
13787
- }
13788
13436
  stack.push(undefined);
13789
13437
  if (node.items && node.items.length > 0) {
13790
13438
  _i = node.items.length;
13791
- while (_i--) {
13439
+ while (_i--)
13792
13440
  stack.push(node.items[_i]);
13793
- }
13794
13441
  }
13795
13442
  }
13796
13443
  }
13797
13444
  return false;
13798
13445
  }
13799
- clearSearch(e) {
13800
- e.stopImmediatePropagation();
13801
- this.searchText.set("");
13802
- }
13803
13446
  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" }] });
13447
+ 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
13448
  }
13806
13449
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AggregationTreeComponent, decorators: [{
13807
13450
  type: Component,
13808
13451
  args: [{ selector: "AggregationTree, aggregation-tree, aggregationtree", imports: [
13452
+ NgTemplateOutlet,
13809
13453
  FormsModule,
13810
13454
  ReactiveFormsModule,
13811
13455
  ButtonComponent,
13812
- AggregationTreeItemComponent,
13456
+ ChevronRightIcon,
13813
13457
  SyslangPipe,
13814
13458
  TranslocoPipe,
13815
- BadgeComponent,
13816
- ChevronRightIcon,
13817
- InputGroupInput,
13818
- InputGroupComponent,
13819
- InputGroupAddonComponent,
13820
- SearchIcon,
13821
- FilterIcon,
13822
13459
  FaIconComponent,
13823
13460
  TriangleAlertIcon,
13824
- FilterXIcon,
13825
- SquareCheckIcon,
13826
- SquareIcon,
13827
- XMarkIcon,
13828
- IconButtonComponent
13461
+ HighlightWordPipe,
13462
+ AggregationPanelComponent,
13463
+ ], standalone: true, host: {
13464
+ "[class]": 'cn("block h-[inherit] max-h-[inherit] w-[inherit]", class())',
13465
+ }, 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"] }]
13466
+ }], 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"] }] } });
13467
+
13468
+ class AggregationListComponent {
13469
+ cn = cn;
13470
+ /* inputs */
13471
+ class = input("", ...(ngDevMode ? [{ debugName: "class" }] : []));
13472
+ id = input(null, ...(ngDevMode ? [{ debugName: "id" }] : []));
13473
+ name = input.required(...(ngDevMode ? [{ debugName: "name" }] : []));
13474
+ column = input.required(...(ngDevMode ? [{ debugName: "column" }] : []));
13475
+ collapsible = input(false, ...(ngDevMode ? [{ debugName: "collapsible" }] : []));
13476
+ collapsed = input(false, ...(ngDevMode ? [{ debugName: "collapsed" }] : []));
13477
+ searchable = input(undefined, ...(ngDevMode ? [{ debugName: "searchable" }] : []));
13478
+ showFiltersCount = input(null, ...(ngDevMode ? [{ debugName: "showFiltersCount", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
13479
+ /* outputs */
13480
+ onSelect = output();
13481
+ onApply = output();
13482
+ onClear = output();
13483
+ /* view queries */
13484
+ scrollElement = viewChild("scrollElement", ...(ngDevMode ? [{ debugName: "scrollElement" }] : []));
13485
+ /* selection state */
13486
+ selection = signal(false, ...(ngDevMode ? [{ debugName: "selection" }] : []));
13487
+ isAllSelected = signal(false, ...(ngDevMode ? [{ debugName: "isAllSelected" }] : []));
13488
+ /* search state */
13489
+ searchText = model("", ...(ngDevMode ? [{ debugName: "searchText" }] : []));
13490
+ /* composable — injects stores/services, wires search effects, provides shared methods */
13491
+ base = injectAggregationBase({
13492
+ name: this.name,
13493
+ column: this.column,
13494
+ searchText: this.searchText,
13495
+ });
13496
+ /* spread from base */
13497
+ aggregationsStore = this.base.aggregationsStore;
13498
+ queryParamsStore = this.base.queryParamsStore;
13499
+ appStore = this.base.appStore;
13500
+ aggregationsService = this.base.aggregationsService;
13501
+ injector = this.base.injector;
13502
+ destroyRef = this.base.destroyRef;
13503
+ suggests = this.base.suggests;
13504
+ hasFilters = this.base.hasFilters;
13505
+ filtersCount = this.base.filtersCount;
13506
+ query = this.base.query;
13507
+ filters = this.base.filters;
13508
+ /* features from appStore */
13509
+ showCount = computed(() => this.appStore.general()?.features?.showAggregationItemCount ?? false, ...(ngDevMode ? [{ debugName: "showCount" }] : []));
13510
+ quickFilter = computed(() => this.appStore.general()?.features?.quickFilter, ...(ngDevMode ? [{ debugName: "quickFilter" }] : []));
13511
+ isDate = computed(() => this.appStore.isDateColumn(this.aggregation()?.column || ""), ...(ngDevMode ? [{ debugName: "isDate" }] : []));
13512
+ /* virtualizer */
13513
+ cdr = inject(ChangeDetectorRef);
13514
+ virtualizer = injectVirtualizer(() => ({
13515
+ count: this.items().length,
13516
+ estimateSize: () => 32,
13517
+ scrollElement: this.scrollElement(),
13518
+ }));
13519
+ aggregation = computed(() => {
13520
+ getState(this.aggregationsStore);
13521
+ const name = this.name();
13522
+ const column = this.column();
13523
+ if (name !== null) {
13524
+ const agg = this.aggregationsService.processAggregation(name, column);
13525
+ if (agg) {
13526
+ if (agg.isTree) {
13527
+ error("The aggregation component does not support tree aggregations. Please use the <AggregationTree /> component instead.");
13528
+ }
13529
+ agg.searchable = this.searchable() ?? agg.searchable;
13530
+ return agg;
13531
+ }
13532
+ }
13533
+ return null;
13534
+ }, ...(ngDevMode ? [{ debugName: "aggregation" }] : []));
13535
+ items = computed(() => {
13536
+ getState(this.aggregationsStore);
13537
+ const agg = this.aggregation();
13538
+ const searchedItems = this.searchedItems();
13539
+ if (searchedItems.length > 0)
13540
+ return searchedItems;
13541
+ if (!agg?.items)
13542
+ return [];
13543
+ // Reset $selected so items removed from the filter are deselected.
13544
+ // Then return a new array reference so Angular's signal equality check
13545
+ // detects the change and re-renders all component instances.
13546
+ agg.items.forEach(item => { item.$selected = false; });
13547
+ return [...this.addCurrentFiltersToItems()];
13548
+ }, ...(ngDevMode ? [{ debugName: "items" }] : []));
13549
+ isEmpty = computed(() => this.items().length === 0, ...(ngDevMode ? [{ debugName: "isEmpty" }] : []));
13550
+ searchedItems = computed(() => {
13551
+ if (!this.suggests())
13552
+ return [];
13553
+ return this.suggests()?.map((suggest) => {
13554
+ const column = this.appStore.getColumn(suggest.category);
13555
+ const item = {
13556
+ name: this.name(),
13557
+ value: suggest.normalized || suggest.display || "",
13558
+ display: suggest.display,
13559
+ column: column?.name ?? suggest.category,
13560
+ count: Number(suggest.frequency),
13561
+ $selected: false,
13562
+ };
13563
+ if (column?.eType === EngineType.bool) {
13564
+ item.value = Boolean(item.value);
13565
+ }
13566
+ return item;
13567
+ });
13568
+ }, ...(ngDevMode ? [{ debugName: "searchedItems" }] : []));
13569
+ constructor() {
13570
+ this.destroyRef.onDestroy(() => {
13571
+ if (this.selection()) {
13572
+ this.aggregation()?.items?.forEach((item) => {
13573
+ item.$selected = undefined;
13574
+ });
13575
+ }
13576
+ });
13577
+ }
13578
+ clear() {
13579
+ const agg = this.aggregation();
13580
+ if (agg) {
13581
+ this.queryParamsStore.removeFilterByName(agg.name, agg.column);
13582
+ this.selection.set(false);
13583
+ this.isAllSelected.set(false);
13584
+ }
13585
+ this.onSelect.emit([]);
13586
+ this.onClear.emit();
13587
+ }
13588
+ selectAll() {
13589
+ if (this.items().length) {
13590
+ this.base.selectItems(this.items(), true);
13591
+ this.selection.set(true);
13592
+ this.isAllSelected.set(true);
13593
+ }
13594
+ }
13595
+ unselectAll() {
13596
+ if (this.items().length) {
13597
+ this.base.selectItems(this.items(), false);
13598
+ this.select();
13599
+ this.isAllSelected.set(false);
13600
+ }
13601
+ }
13602
+ apply() {
13603
+ const agg = this.aggregation();
13604
+ if (!agg)
13605
+ return;
13606
+ this.base.applyFilters(this.getFilters(), agg, () => this.clear());
13607
+ this.searchText.set("");
13608
+ this.selection.set(false);
13609
+ this.onApply.emit();
13610
+ }
13611
+ loadMore() {
13612
+ const q = this.queryParamsStore.getQuery();
13613
+ this.aggregationsService.loadMore(q, this.aggregation()).subscribe((aggregation) => {
13614
+ this.aggregationsStore.updateAggregation(aggregation);
13615
+ this.cdr.detectChanges();
13616
+ });
13617
+ }
13618
+ select() {
13619
+ const selectedItems = this.items().filter((item) => item.$selected);
13620
+ this.onSelect.emit(selectedItems);
13621
+ this.selection.set(selectedItems.length > 0 || this.hasFilters());
13622
+ }
13623
+ /* item-level methods — called from the ng-template */
13624
+ listItemName(item) {
13625
+ const value = item.display || item.value;
13626
+ return typeof value === "string" ? value : `${value}`;
13627
+ }
13628
+ isListItemFiltered(item, field) {
13629
+ const filters = this.queryParamsStore.getFilter({ field: field ?? undefined, name: this.listItemName(item) });
13630
+ if (!filters)
13631
+ return false;
13632
+ const values = [item.value];
13633
+ return (values.some((v) => v === filters.value) ||
13634
+ !!(filters.values?.length && filters.values.some((value) => values.some((v) => v === value))));
13635
+ }
13636
+ selectListItem(item, e) {
13637
+ e?.stopImmediatePropagation();
13638
+ item.$selected = !item.$selected;
13639
+ this.select();
13640
+ }
13641
+ onListItemTextClick(item, event) {
13642
+ if (this.quickFilter()) {
13643
+ this.selectListItem(item, event);
13644
+ this.apply();
13645
+ }
13646
+ }
13647
+ getFilters() {
13648
+ const items = this.items().filter((item) => item.$selected);
13649
+ const searchedItems = this.searchedItems().filter((item) => item.$selected);
13650
+ const currentItems = [...items, ...searchedItems];
13651
+ const { column, name, isDistribution = false } = this.aggregation() || {};
13652
+ return currentItems.map((item) => this.aggregationsService.toFilter(item, column, name, isDistribution));
13653
+ }
13654
+ addCurrentFiltersToItems() {
13655
+ return this.base.addCurrentFiltersToItems(this.aggregation());
13656
+ }
13657
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AggregationListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
13658
+ 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" }] });
13659
+ }
13660
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AggregationListComponent, decorators: [{
13661
+ type: Component,
13662
+ args: [{ selector: "AggregationList, aggregation-list, aggregationlist", imports: [
13663
+ NgTemplateOutlet,
13664
+ FormsModule,
13665
+ ReactiveFormsModule,
13666
+ SyslangPipe,
13667
+ TranslocoPipe,
13668
+ FaIconComponent,
13669
+ TriangleAlertIcon,
13670
+ HighlightWordPipe,
13671
+ AggregationPanelComponent,
13829
13672
  ], 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"] }] } });
13673
+ "[class]": 'cn("block h-[inherit] max-h-[inherit]", class())',
13674
+ }, 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"] }]
13675
+ }], 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
13676
 
13834
13677
  /**
13835
13678
  * 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 +13829,7 @@ class AggregationComponent {
13986
13829
  (onClear)="onClear.emit($event)"
13987
13830
  />
13988
13831
  }
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"] }] });
13832
+ `, 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
13833
  }
13991
13834
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AggregationComponent, decorators: [{
13992
13835
  type: Component,
@@ -14302,6 +14145,7 @@ class MoreComponent {
14302
14145
  includedFilters = input([], ...(ngDevMode ? [{ debugName: "includedFilters" }] : []));
14303
14146
  excludedFilters = input([], ...(ngDevMode ? [{ debugName: "excludedFilters" }] : []));
14304
14147
  aggregations = input(...(ngDevMode ? [undefined, { debugName: "aggregations" }] : []));
14148
+ homepage = input(false, ...(ngDevMode ? [{ debugName: "homepage", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
14305
14149
  appStore = inject(AppStore);
14306
14150
  aggregationsStore = inject(AggregationsStore);
14307
14151
  queryParamsStore = inject(QueryParamsStore);
@@ -14329,7 +14173,7 @@ class MoreComponent {
14329
14173
  effect(() => {
14330
14174
  const count = this.count();
14331
14175
  const authorizedFilters = this.aggregationsService
14332
- .getAuthorizedFilters(this.aggregations(), this.includedFilters(), this.excludedFilters())
14176
+ .getAuthorizedFilters(this.aggregations(), this.includedFilters(), this.excludedFilters(), this.homepage())
14333
14177
  .toSpliced(0, count);
14334
14178
  const f = authorizedFilters.map((agg) => {
14335
14179
  const { icon = "far fa-list", hidden = false } = this.appStore.getAggregationCustomization(agg.column, agg.name) || {};
@@ -14380,7 +14224,7 @@ class MoreComponent {
14380
14224
  return count > 0;
14381
14225
  }
14382
14226
  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: `
14227
+ 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
14228
  @for (filter of visibleFilters(); track $index) {
14385
14229
  <Aggregation
14386
14230
  class="w-60 max-w-80 px-1 [--height:15lh]"
@@ -14411,7 +14255,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
14411
14255
  `, host: {
14412
14256
  class: "divide-y divide-muted-foreground/18"
14413
14257
  }, 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 }] }] } });
14258
+ }], 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
14259
 
14416
14260
  class MoreButtonComponent {
14417
14261
  appStore = inject(AppStore);
@@ -14423,10 +14267,11 @@ class MoreButtonComponent {
14423
14267
  includedFilters = input([], ...(ngDevMode ? [{ debugName: "includedFilters" }] : []));
14424
14268
  excludedFilters = input([], ...(ngDevMode ? [{ debugName: "excludedFilters" }] : []));
14425
14269
  aggregations = input(...(ngDevMode ? [undefined, { debugName: "aggregations" }] : []));
14270
+ homepage = input(false, ...(ngDevMode ? [{ debugName: "homepage", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
14426
14271
  totalFiltersCount = computed(() => {
14427
14272
  const count = this.count();
14428
14273
  const authorizedFilters = this.aggregationsService
14429
- .getAuthorizedFilters(this.aggregations(), this.includedFilters(), this.excludedFilters())
14274
+ .getAuthorizedFilters(this.aggregations(), this.includedFilters(), this.excludedFilters(), this.homepage())
14430
14275
  .toSpliced(0, count);
14431
14276
  const total = authorizedFilters.reduce((acc, filter) => {
14432
14277
  const f = this.queryParamsStore.getFilter(filter);
@@ -14436,7 +14281,7 @@ class MoreButtonComponent {
14436
14281
  return total;
14437
14282
  }, ...(ngDevMode ? [{ debugName: "totalFiltersCount" }] : []));
14438
14283
  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: `
14284
+ 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
14285
  <Popover class="group/more">
14441
14286
  <button
14442
14287
  variant="ghost"
@@ -14454,11 +14299,11 @@ class MoreButtonComponent {
14454
14299
 
14455
14300
  <PopoverContent #contentRef="popoverContent" [position]="position()" class="min-w-max">
14456
14301
  @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" />
14302
+ <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
14303
  }
14459
14304
  </PopoverContent>
14460
14305
  </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" }] });
14306
+ `, 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
14307
  }
14463
14308
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: MoreButtonComponent, decorators: [{
14464
14309
  type: Component,
@@ -14484,13 +14329,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
14484
14329
 
14485
14330
  <PopoverContent #contentRef="popoverContent" [position]="position()" class="min-w-max">
14486
14331
  @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" />
14332
+ <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
14333
  }
14489
14334
  </PopoverContent>
14490
14335
  </Popover>
14491
14336
  `
14492
14337
  }]
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 }] }] } });
14338
+ }], 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
14339
 
14495
14340
  class FiltersBarComponent {
14496
14341
  class = input(...(ngDevMode ? [undefined, { debugName: "class" }] : []));
@@ -14528,6 +14373,15 @@ class FiltersBarComponent {
14528
14373
  * @default true
14529
14374
  */
14530
14375
  showMoreFiltersButton = input(true, ...(ngDevMode ? [{ debugName: "showMoreFiltersButton", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
14376
+ /**
14377
+ * When enabled, only the filters flagged with `homepage: true` in the "filters" custom JSON
14378
+ * are displayed. If no filter is flagged, the bar shows no filters.
14379
+ *
14380
+ * Accepts a boolean value or a string that can be transformed to a boolean.
14381
+ *
14382
+ * @default false
14383
+ */
14384
+ homepage = input(false, ...(ngDevMode ? [{ debugName: "homepage", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
14531
14385
  direction = input("horizontal", ...(ngDevMode ? [{ debugName: "direction" }] : []));
14532
14386
  /**
14533
14387
  * The distance in pixels between the popover and its trigger element.
@@ -14577,50 +14431,76 @@ class FiltersBarComponent {
14577
14431
  return this.aggregationsStore.aggregations().length > 0;
14578
14432
  return false;
14579
14433
  }, ...(ngDevMode ? [{ debugName: "hasAggregations" }] : []));
14434
+ /**
14435
+ * The full list of authorized filters, NOT capped by `filtersCount`.
14436
+ *
14437
+ * This computed signal performs the following operations:
14438
+ * 1. Retrieves aggregations from either the component's aggregations input or the app store
14439
+ * 2. Filters aggregations based on the route's filter criteria configuration
14440
+ * 3. Excludes filters specified in the `excludeFilters` list
14441
+ * 4. If `includeFilters` is not empty, only includes filters present in that list
14442
+ * 5. Maps the filtered aggregations to objects containing only `name` and `column` properties
14443
+ */
14444
+ allAuthorizedFilters = computed(() => {
14445
+ return this.aggregationsService
14446
+ .getAuthorizedFilters(this.aggregations(), this.includeFilters(), this.excludeFilters(), this.homepage())
14447
+ .map((f) => ({ name: f.name, column: f.column }));
14448
+ }, ...(ngDevMode ? [{ debugName: "allAuthorizedFilters" }] : []));
14580
14449
  /**
14581
14450
  * Computes the list of additional filters that can be displayed in the "more filters" popover.
14582
14451
  *
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`.
14452
+ * Derived from the FULL authorized list (not the one capped by `filtersCount`), so the
14453
+ * filters beyond `filtersCount` which are never rendered in the bar — are still counted.
14454
+ * Otherwise, when every rendered filter fits in the container, this list would be empty and
14455
+ * the "more" button would be hidden even though more filters exist beyond the cap.
14586
14456
  *
14587
- * This property manages the visibility and content of the "more filters" popover in the UI.
14457
+ * This property manages the visibility of the "more filters" button in the UI.
14588
14458
  *
14589
14459
  * @returns An array of Aggregation objects representing the additional filters available.
14590
14460
  */
14591
14461
  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 }))
14462
+ const moreFiltersAggregations = this.allAuthorizedFilters()
14596
14463
  .toSpliced(0, this.visibleFiltersCount())
14597
14464
  .map((f) => this.aggregationsStore.getAggregation(f.column, "column"));
14598
14465
  return moreFiltersAggregations;
14599
14466
  }, ...(ngDevMode ? [{ debugName: "hasMoreFilters" }] : []));
14600
14467
  /**
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`
14468
+ * The authorized filters rendered as buttons in the bar, limited to the number
14469
+ * specified by `filtersCount`.
14610
14470
  *
14611
14471
  * @returns An array of authorized filter objects, each containing `name` and `column` properties
14612
14472
  */
14613
14473
  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;
14474
+ return this.allAuthorizedFilters().toSpliced(this.filtersCount());
14619
14475
  }, ...(ngDevMode ? [{ debugName: "authorizedFilters" }] : []));
14476
+ /**
14477
+ * Whether some authorized filters exist beyond the `filtersCount` cap.
14478
+ *
14479
+ * Those filters are never rendered in the bar and are only reachable through
14480
+ * the "more" button, which is therefore permanently visible: the overflow
14481
+ * manager must always reserve its space so the last filter button never
14482
+ * overlaps it (`reserveStop`).
14483
+ */
14484
+ hasCappedFilters = computed(() => this.allAuthorizedFilters().length > this.filtersCount(), ...(ngDevMode ? [{ debugName: "hasCappedFilters" }] : []));
14620
14485
  constructor() {
14621
14486
  this.transloco.events$
14622
14487
  .pipe(takeUntilDestroyed(this.destroyRef), debounceTime(100))
14623
14488
  .subscribe(() => this.overflowManagerRef()?.countItems());
14489
+ // Recount the overflow whenever the applied filters or basket change (e.g.
14490
+ // a filter modified or removed from the "more filters" popover). A
14491
+ // FilterButton hidden by the overflow manager (display: none) emits no
14492
+ // resize notification when its natural width changes, so it could fit in
14493
+ // the bar again without the manager knowing. afterRenderEffect guarantees
14494
+ // the DOM already reflects the new state when we measure.
14495
+ afterRenderEffect({
14496
+ read: () => {
14497
+ // track filters and basket changes (getState is reactive here)
14498
+ const { filters, basket } = getState(this.queryParamsStore);
14499
+ void filters;
14500
+ void basket;
14501
+ this.overflowManagerRef()?.countItems();
14502
+ }
14503
+ });
14624
14504
  }
14625
14505
  /**
14626
14506
  * Clears all filters (included baskets) by invoking the clearFilters method on the queryParamsStore.
@@ -14669,8 +14549,8 @@ class FiltersBarComponent {
14669
14549
  });
14670
14550
  }
14671
14551
  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">
14552
+ 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: `
14553
+ <div overflowManager [direction]="direction()" [reserveStop]="hasCappedFilters()" (count)="adjustFiltersCount($event)" class="flex items-end gap-2 rounded-[inherit] bg-inherit">
14674
14554
  @if (hasFilters()) {
14675
14555
  <button
14676
14556
  variant="destructive"
@@ -14717,11 +14597,12 @@ class FiltersBarComponent {
14717
14597
  [position]="morePosition()"
14718
14598
  [includedFilters]="includeFilters()"
14719
14599
  [excludedFilters]="excludeFilters()"
14720
- [aggregations]="aggregations()" />
14600
+ [aggregations]="aggregations()"
14601
+ [homepage]="homepage()" />
14721
14602
  }
14722
14603
  }
14723
14604
  </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" }] });
14605
+ `, 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
14606
  }
14726
14607
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: FiltersBarComponent, decorators: [{
14727
14608
  type: Component,
@@ -14741,7 +14622,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
14741
14622
  ],
14742
14623
  providers: [provideTranslocoScope("filters")],
14743
14624
  template: `
14744
- <div overflowManager [direction]="direction()" (count)="adjustFiltersCount($event)" class="flex items-end gap-2 rounded-[inherit] bg-inherit">
14625
+ <div overflowManager [direction]="direction()" [reserveStop]="hasCappedFilters()" (count)="adjustFiltersCount($event)" class="flex items-end gap-2 rounded-[inherit] bg-inherit">
14745
14626
  @if (hasFilters()) {
14746
14627
  <button
14747
14628
  variant="destructive"
@@ -14788,17 +14669,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
14788
14669
  [position]="morePosition()"
14789
14670
  [includedFilters]="includeFilters()"
14790
14671
  [excludedFilters]="excludeFilters()"
14791
- [aggregations]="aggregations()" />
14672
+ [aggregations]="aggregations()"
14673
+ [homepage]="homepage()" />
14792
14674
  }
14793
14675
  }
14794
14676
  </div>
14795
14677
  `,
14796
14678
  host: {
14797
- "[class]": "cn('block relative', class())",
14679
+ "[class]": "cn('block relative min-w-0', class())",
14798
14680
  "(click)": "handleClick($event)"
14799
14681
  }
14800
14682
  }]
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 }] }] } });
14683
+ }], 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
14684
 
14803
14685
  class LabelService {
14804
14686
  appStore = inject(AppStore);
@@ -16187,5 +16069,5 @@ const queryNameResolver = () => {
16187
16069
  * Generated bundle index. Do not edit.
16188
16070
  */
16189
16071
 
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 };
16072
+ 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
16073
  //# sourceMappingURL=sinequa-atomic-angular.mjs.map