@sinequa/atomic-angular 1.13.0 → 1.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,12 +2,12 @@ import * as i0 from '@angular/core';
2
2
  import { Injectable, inject, HostBinding, Component, Pipe, InjectionToken, computed, ChangeDetectorRef, DestroyRef, LOCALE_ID, Inject, Optional, input, output, signal, effect, assertInInjectionContext, runInInjectionContext, EnvironmentInjector, 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
3
  import { BehaviorSubject, Subscription, Subject, of, catchError, map, tap, EMPTY, throwError, from, filter as filter$1, shareReplay, switchMap, firstValueFrom, 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, CardComponent, CardHeaderComponent, CardContentComponent, 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, CardFooterComponent, InputGroupInput, InputGroupComponent, InputGroupAddonComponent, EyeSlashIcon, EyeIcon, BookmarkIcon, PopoverContentComponent, UserIcon, FolderIcon, DialogDescriptionComponent, VerticalDividerComponent, CommentIcon, ThumbsUpIcon, ThumbsDownIcon, DropdownDirective, SearchIcon, TriangleAlertIcon, ListFilterIcon, BreakpointObserverService, TrashCanIcon, CircleXIcon, InfoCircleIcon, HorizontalDividerComponent, HistoryIcon, StarIcon, FlagEnglishIcon, FlagFrenchIcon, FlagGermanIcon, EditIcon, UndoIcon, SaveIcon, AvatarComponent, AvatarFallbackComponent, AvatarImageComponent } from '@sinequa/ui';
5
+ import { DropdownComponent, DropdownContentComponent, InputComponent, ButtonComponent, cn, FaIconComponent, EllipsisIcon, ChevronRightIcon, MenuComponent, MenuContentComponent, MenuItemComponent, LinkComponent, CardComponent, CardHeaderComponent, CardContentComponent, 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, CardFooterComponent, InputGroupInput, InputGroupComponent, InputGroupAddonComponent, EyeSlashIcon, EyeIcon, BookmarkIcon, PopoverContentComponent, UserIcon, FolderIcon, DialogDescriptionComponent, VerticalDividerComponent, CommentIcon, ThumbsUpIcon, ThumbsDownIcon, DropdownDirective, SearchIcon, TriangleAlertIcon, BreakpointObserverService, ListFilterIcon, SheetComponent, SheetHeaderComponent, SheetTitleComponent, TrashCanIcon, CircleXIcon, InfoCircleIcon, HorizontalDividerComponent, HistoryIcon, StarIcon, FlagEnglishIcon, FlagFrenchIcon, FlagGermanIcon, EditIcon, UndoIcon, SaveIcon, AvatarComponent, AvatarFallbackComponent, AvatarImageComponent } from '@sinequa/ui';
6
6
  import highlightWords from 'highlight-words';
7
7
  import { ActivatedRoute, Router, NavigationEnd, RouterLink, RouterLinkActive, RouterModule } from '@angular/router';
8
8
  import { withDevtools } from '@angular-architects/ngrx-toolkit';
9
9
  import { signalStore, signalStoreFeature, withState, withMethods, patchState, getState, withComputed } from '@ngrx/signals';
10
- import { EngineType, fetchApp, extraColumns, sysLang, fetchPrincipal, globalConfig, filter, getQueryParamsFromUrl, isRedirectPending, info, clearSessionTokens, login, error, setGlobalConfig, initializeAppConfig, notify, addConcepts, queryParamsFromUrl, patchUserSettings, deleteUserSettings, fetchUserSettings, warn, buildPathsAndLevels, escapeExpr, fetchQuery, isAuthenticated, isExpired, debug, AuditEventType, fetchSuggest, isObject, Audit, getMetadata, bisect, isNotInputEvent, fetchSponsoredLinks, translateAggregationToDateOptions, aggItemRegex, parseValueAndOperatorFromItem, fetchSuggestField, fetchSimilarDocuments, logout, fetchChangePassword, fetchSendPasswordResetEmail, expiresSoon, suggestionsToTreeAggregationNodes, labels, fetchLabels, guid, getRelativeDate, createUserProfile, deleteUserProfileProperty, patchUserProfile, isJsonable, addAuditAdditionalInfo, getToken, setToken, createHeaders } from '@sinequa/atomic';
10
+ import { EngineType, fetchApp, extraColumns, sysLang, fetchPrincipal, globalConfig, filter, getQueryParamsFromUrl, isRedirectPending, info, clearSessionTokens, login, error, setGlobalConfig, initializeAppConfig, notify, addConcepts, queryParamsFromUrl, patchUserSettings, deleteUserSettings, fetchUserSettings, warn, buildPathsAndLevels, escapeExpr, fetchQuery, flattenLegacyFilterValues, isAuthenticated, isExpired, debug, AuditEventType, fetchSuggest, isObject, Audit, getMetadata, bisect, isNotInputEvent, fetchSponsoredLinks, translateAggregationToDateOptions, aggItemRegex, parseValueAndOperatorFromItem, fetchSuggestField, fetchSimilarDocuments, logout, fetchChangePassword, fetchSendPasswordResetEmail, expiresSoon, suggestionsToTreeAggregationNodes, labels, fetchLabels, guid, getRelativeDate, createUserProfile, deleteUserProfileProperty, patchUserProfile, isJsonable, addAuditAdditionalInfo, getToken, setToken, createHeaders } from '@sinequa/atomic';
11
11
  import { takeUntilDestroyed, toObservable, toSignal } from '@angular/core/rxjs-interop';
12
12
  import { DatePipe, DATE_PIPE_DEFAULT_TIMEZONE, DATE_PIPE_DEFAULT_OPTIONS, Location, NgTemplateOutlet, NgStyle, NgClass, NgComponentOutlet } from '@angular/common';
13
13
  import { HttpClient, HttpParams, httpResource, HttpResponse, HttpContextToken, HttpHeaders } from '@angular/common/http';
@@ -3526,7 +3526,7 @@ class AggregationsService {
3526
3526
  else {
3527
3527
  // get current filters for the aggregation
3528
3528
  const filters = this.queryParamsStore.getFilter({ field: agg.column, name: agg.name });
3529
- flattenedValues = filters ? this.flattenFilters([filters]) : [];
3529
+ flattenedValues = filters ? flattenLegacyFilterValues([filters]) : [];
3530
3530
  }
3531
3531
  if (agg.isTree) {
3532
3532
  const selectedAgg = agg.items.map(item => this.selectNode(item, flattenedValues));
@@ -3563,34 +3563,44 @@ class AggregationsService {
3563
3563
  */
3564
3564
  toFilter(item, field, name, isDistribution) {
3565
3565
  if (isDistribution) {
3566
- const res = item.value.match(/.*:\(?([><=\d\-.AND ]+)\)?/);
3567
- if (res?.[1]) {
3568
- const expr = res?.[1].split(" AND ");
3569
- const filters = expr.map(e => {
3570
- const operator = e.indexOf(">=") !== -1 ? "gte" : "lt";
3571
- const value = e.substring(e.indexOf(" ") + 1);
3572
- return { name, field, operator, value, display: item.display || item.value };
3573
- });
3574
- if (filters.length === 2) {
3575
- return {
3576
- operator: "and",
3577
- filters,
3578
- display: filters[0].display || filters[0].value,
3579
- field,
3580
- name
3581
- };
3582
- }
3583
- else if (filters.length === 1) {
3584
- return { ...filters[0], field, name };
3585
- }
3586
- throw new Error("Failed to parse distribution expression");
3566
+ // Parsing lives in @sinequa/atomic — `filter.distribution` is the single implementation, and it
3567
+ // recognises all four bounds where this method only ever produced `gte` or `lt`, reading `>` as
3568
+ // `gte` and `<=` as `lt`. What is added here is what the store needs and a `Filter` has no room
3569
+ // for: the aggregation's `name` on every leaf, which is what makes `updateFilter` replace the
3570
+ // entry rather than merge into it, and the human-readable `display`.
3571
+ const parsed = filter.distribution(field, item.value);
3572
+ if (parsed) {
3573
+ return this.nameLeaves(parsed, field, name, item.display || String(item.value ?? ""));
3587
3574
  }
3575
+ throw new Error("Failed to parse distribution expression");
3588
3576
  }
3589
3577
  if (typeof item.value === "string") {
3590
3578
  return { name, field, value: item.value, operator: "contains", display: item.display };
3591
3579
  }
3592
3580
  return { name, field, value: item.value, display: item.display };
3593
3581
  }
3582
+ /**
3583
+ * Stamps the aggregation's `name` and the item's `display` onto a filter parsed by
3584
+ * `filter.distribution`.
3585
+ *
3586
+ * A `Filter` carries neither, and the store needs both: `name` is what
3587
+ * `QueryParamsStore.updateFilter` matches on to **replace** an aggregation's entry instead of merging
3588
+ * into it — the only way a selection can shrink — and `display` is the label the applied-filter chips
3589
+ * render. A combined filter also keeps `field` at its root, which is how the store finds the entry.
3590
+ */
3591
+ nameLeaves(parsed, field, name, display) {
3592
+ const leaves = parsed.filters;
3593
+ if (leaves?.length) {
3594
+ return {
3595
+ ...parsed,
3596
+ field,
3597
+ name,
3598
+ display,
3599
+ filters: leaves.map(leaf => ({ ...leaf, field, name, display }))
3600
+ };
3601
+ }
3602
+ return { ...parsed, field, name, display };
3603
+ }
3594
3604
  findNode(items, path = "") {
3595
3605
  if (!items || items.length === 0) {
3596
3606
  return undefined;
@@ -3638,27 +3648,6 @@ class AggregationsService {
3638
3648
  }
3639
3649
  return node;
3640
3650
  }
3641
- flattenFilters(filters) {
3642
- const flattenedValues = [];
3643
- function extractValues(filters) {
3644
- for (const filter of filters) {
3645
- if (filter.value) {
3646
- flattenedValues.push(filter.value);
3647
- if (filter.display)
3648
- flattenedValues.push(filter.display);
3649
- // we handle "values" field for trees
3650
- }
3651
- else if (filter.values) {
3652
- flattenedValues.push(...filter.values);
3653
- }
3654
- if (filter.filters) {
3655
- extractValues(filter.filters);
3656
- }
3657
- }
3658
- }
3659
- extractValues(filters);
3660
- return flattenedValues;
3661
- }
3662
3651
  /**
3663
3652
  * Recursively opens parent nodes in a tree structure if any of their child nodes are selected or opened.
3664
3653
  *
@@ -5397,6 +5386,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
5397
5386
 
5398
5387
  class UserProfileService {
5399
5388
  API_URL = `${globalConfig.backendUrl}/api/v2/user-profile`;
5389
+ // Keyed by the userId signal's identity rather than its value: every current caller
5390
+ // passes the same PrincipalStore.userId signal, so sharing the resource here collapses
5391
+ // what would otherwise be one httpResource (and one HTTP request) per caller into one
5392
+ // for the whole app -- and lets a `.set()` from one consumer (e.g. the profile edit
5393
+ // form) show up everywhere else that reads this resource.
5394
+ resources = new WeakMap();
5400
5395
  /**
5401
5396
  * Retrieves the user profile by user ID.
5402
5397
  *
@@ -5404,11 +5399,16 @@ class UserProfileService {
5404
5399
  * @returns An Observable that emits an the UserProfile object.
5405
5400
  */
5406
5401
  getUserProfile(userId) {
5407
- return httpResource(() => (userId() ? `${this.API_URL}/${userId()}` : undefined), {
5408
- parse: (response) => {
5409
- return response;
5410
- }
5411
- });
5402
+ let resource = this.resources.get(userId);
5403
+ if (!resource) {
5404
+ resource = httpResource(() => (userId() ? `${this.API_URL}/${userId()}` : undefined), {
5405
+ parse: response => {
5406
+ return response;
5407
+ }
5408
+ });
5409
+ this.resources.set(userId, resource);
5410
+ }
5411
+ return resource;
5412
5412
  }
5413
5413
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: UserProfileService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
5414
5414
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: UserProfileService, providedIn: "root" });
@@ -9475,41 +9475,57 @@ function injectAggregationBase(refs) {
9475
9475
  }
9476
9476
  function addCurrentFiltersToItems(aggregation) {
9477
9477
  const currentItems = aggregation?.items || [];
9478
- if (!aggregation?.isTree && !aggregation?.isDistribution) {
9479
- const currentFilters = isControlled() ? selectionAsFilter() : queryParamsStore.getFilter({ field: aggregation?.column, name: aggregation?.name });
9480
- if (currentFilters) {
9481
- if (Array.isArray(currentFilters.filters) && currentFilters.filters.length) {
9482
- currentFilters.filters.forEach((filter) => {
9483
- const found = currentItems.find(item => item.value?.toString().toLocaleLowerCase() === filter.value?.toLocaleLowerCase());
9484
- if (!found) {
9485
- currentItems.unshift({ value: filter.value, display: filter.display, $selected: true });
9486
- }
9487
- else {
9488
- found.$selected = true;
9489
- }
9490
- });
9491
- }
9492
- else if (Array.isArray(currentFilters.values) && currentFilters.values.length) {
9493
- // multiple values stored as a string array, e.g. { values: ["alice_martin", "caroline_dubois"] }
9494
- // (no `filters` sub-array and no single `value`) — mark each matching item as selected
9495
- currentFilters.values.forEach(value => {
9496
- const found = currentItems.find(item => item.value?.toString().toLocaleLowerCase() === value?.toString().toLocaleLowerCase());
9497
- if (!found) {
9498
- currentItems.unshift({ value, display: value, $selected: true });
9499
- }
9500
- else {
9501
- found.$selected = true;
9502
- }
9503
- });
9478
+ if (aggregation?.isTree) {
9479
+ return currentItems;
9480
+ }
9481
+ const currentFilters = isControlled() ? selectionAsFilter() : queryParamsStore.getFilter({ field: aggregation?.column, name: aggregation?.name });
9482
+ if (aggregation?.isDistribution) {
9483
+ // A distribution item's `value` is the full backend expression (e.g.
9484
+ // `size`10 Kb to 100 Kb`:(>= 10240 AND < 102400)`), not comparable to what the applied
9485
+ // filter stores — its leaves only ever carry the bucket's numeric thresholds. Flatten the
9486
+ // filter and compare on `display` instead (or `value` when `valuesAreExpressions` is not
9487
+ // set) — same strategy as `AggregationsService.enrichAggregation`.
9488
+ const flattenedValues = currentFilters ? flattenLegacyFilterValues([currentFilters]) : [];
9489
+ currentItems.forEach(item => {
9490
+ const valueToSearch = aggregation.valuesAreExpressions ? item.display : item.value;
9491
+ if (flattenedValues.includes(valueToSearch?.toString() ?? "")) {
9492
+ item.$selected = true;
9504
9493
  }
9505
- else if (currentFilters.value) {
9506
- const found = currentItems.find(item => item.value?.toString().toLocaleLowerCase() === currentFilters.value?.toLocaleLowerCase());
9494
+ });
9495
+ return currentItems;
9496
+ }
9497
+ if (currentFilters) {
9498
+ if (Array.isArray(currentFilters.filters) && currentFilters.filters.length) {
9499
+ currentFilters.filters.forEach((filter) => {
9500
+ const found = currentItems.find(item => item.value?.toString().toLocaleLowerCase() === filter.value?.toLocaleLowerCase());
9501
+ if (!found) {
9502
+ currentItems.unshift({ value: filter.value, display: filter.display, $selected: true });
9503
+ }
9504
+ else {
9505
+ found.$selected = true;
9506
+ }
9507
+ });
9508
+ }
9509
+ else if (Array.isArray(currentFilters.values) && currentFilters.values.length) {
9510
+ // multiple values stored as a string array, e.g. { values: ["alice_martin", "caroline_dubois"] }
9511
+ // (no `filters` sub-array and no single `value`) — mark each matching item as selected
9512
+ currentFilters.values.forEach(value => {
9513
+ const found = currentItems.find(item => item.value?.toString().toLocaleLowerCase() === value?.toString().toLocaleLowerCase());
9507
9514
  if (!found) {
9508
- currentItems.push({ value: currentFilters.value, display: currentFilters.display, $selected: true });
9515
+ currentItems.unshift({ value, display: value, $selected: true });
9509
9516
  }
9510
9517
  else {
9511
9518
  found.$selected = true;
9512
9519
  }
9520
+ });
9521
+ }
9522
+ else if (currentFilters.value) {
9523
+ const found = currentItems.find(item => item.value?.toString().toLocaleLowerCase() === currentFilters.value?.toLocaleLowerCase());
9524
+ if (!found) {
9525
+ currentItems.push({ value: currentFilters.value, display: currentFilters.display, $selected: true });
9526
+ }
9527
+ else {
9528
+ found.$selected = true;
9513
9529
  }
9514
9530
  }
9515
9531
  }
@@ -14500,7 +14516,7 @@ class AggregationPanelComponent {
14500
14516
  this.searchText.set("");
14501
14517
  }
14502
14518
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: AggregationPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
14503
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.29", 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", "aria-label"] }, { 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" }] });
14519
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.29", 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\n [attr.open]=\"expanded()\"\n [attr.name]=\"id()\"\n class=\"group space-y-2\"\n (toggle)=\"onToggle($event)\">\n <summary\n [class.cursor-pointer]=\"collapsible() && !isEmpty()\"\n [class.text-muted-foreground]=\"isEmpty()\"\n class=\"m-0 mt-1 flex h-8 w-full items-center gap-1 pl-1 font-semibold select-none\"\n (click)=\"onHeaderClick($event)\">\n <ng-content select=\"label\">\n @let icon = aggregation()?.icon;\n @if (icon) {\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n\n @if (showFiltersCount() && filtersCount() > 0) {\n <Badge size=\"xs\" class=\"ml-1\">\n {{ filtersCount() }}\n </Badge>\n }\n @if (!isCollapsed()) {\n @if (hasFilters()) {\n @let label = \"filters.clearFilters\" | transloco;\n <button\n variant=\"none\"\n icon-button\n [aria-label]=\"label\"\n (click)=\"$event.stopPropagation(); cleared.emit()\">\n <filter-x-icon />\n </button>\n }\n @if (selection()) {\n @let label = \"filters.apply\" | transloco;\n <button\n variant=\"accent\"\n size=\"sm\"\n [aria-label]=\"label\"\n (click)=\"$event.stopPropagation(); applied.emit()\">\n <FilterIcon />\n {{ label }}\n </button>\n }\n\n @if (isAllSelected()) {\n @let label = \"filters.unselectAllFilters\" | transloco;\n <button\n variant=\"none\"\n icon-button\n [aria-label]=\"label\"\n (click)=\"$event.stopPropagation(); allUnselected.emit()\">\n <square-check-icon />\n </button>\n } @else {\n @let label = \"filters.selectAllFilters\" | transloco;\n <button\n variant=\"none\"\n icon-button\n [aria-label]=\"label\"\n (click)=\"$event.stopPropagation(); allSelected.emit()\">\n <square-icon />\n </button>\n }\n }\n\n @if (collapsible()) {\n <icon-button\n title=\"Open/Close\"\n class=\"cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\n <chevronright />\n <span class=\"sr-only\">{{ \"filters.toggle\" | transloco }}</span>\n </icon-button>\n }\n </summary>\n\n @if (aggregation()?.searchable && itemsLength() > 0) {\n <InputGroup class=\"group/item mt-3\">\n <input\n #searchInput\n input-group\n id=\"aggregation-input-{{ aggregation()?.column }}\"\n type=\"text\"\n [attr.aria-label]=\"'search' | transloco\"\n [attr.placeholder]=\"'search' | transloco\"\n [(ngModel)]=\"searchText\"\n class=\"mt-1\" />\n <InputGroupAddon>\n <SearchIcon\n class=\"text-foreground size-4 rotate-0 transition-[rotate] duration-500 group-focus-within/item:rotate-90\" />\n </InputGroupAddon>\n <InputGroupAddon align=\"inline-end\" class=\"gap-0.5!\">\n <icon-button\n size=\"sm\"\n [class]=\"\n searchText().length > 0\n ? 'rotate-90 cursor-pointer opacity-100 transition-[rotate,opacity] duration-500'\n : 'pointer-events-none rotate-0 opacity-0 transition-[rotate,opacity] duration-500'\n \"\n aria-label=\"Clear search\"\n [tabindex]=\"searchText().length > 0 ? 0 : -1\"\n (keydown.enter)=\"clearSearch($event)\"\n (click)=\"clearSearch($event)\">\n <XMarkIcon />\n </icon-button>\n <ng-content select=\"[search-addon]\" />\n </InputGroupAddon>\n </InputGroup>\n }\n\n <ng-content />\n\n @if (hasMore() && searchedItemsLength() === 0) {\n <button\n class=\"mt-1 flex w-full justify-center\"\n [attr.aria-label]=\"'loadMore' | transloco\"\n (click)=\"loadedMore.emit()\">\n {{ \"loadMore\" | transloco }}\n </button>\n }\n</details>\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", "aria-label"] }, { 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" }] });
14504
14520
  }
14505
14521
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: AggregationPanelComponent, decorators: [{
14506
14522
  type: Component,
@@ -14522,7 +14538,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
14522
14538
  SquareIcon,
14523
14539
  XMarkIcon,
14524
14540
  IconButtonComponent,
14525
- ], 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" }]
14541
+ ], template: "<details\n [attr.open]=\"expanded()\"\n [attr.name]=\"id()\"\n class=\"group space-y-2\"\n (toggle)=\"onToggle($event)\">\n <summary\n [class.cursor-pointer]=\"collapsible() && !isEmpty()\"\n [class.text-muted-foreground]=\"isEmpty()\"\n class=\"m-0 mt-1 flex h-8 w-full items-center gap-1 pl-1 font-semibold select-none\"\n (click)=\"onHeaderClick($event)\">\n <ng-content select=\"label\">\n @let icon = aggregation()?.icon;\n @if (icon) {\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n\n @if (showFiltersCount() && filtersCount() > 0) {\n <Badge size=\"xs\" class=\"ml-1\">\n {{ filtersCount() }}\n </Badge>\n }\n @if (!isCollapsed()) {\n @if (hasFilters()) {\n @let label = \"filters.clearFilters\" | transloco;\n <button\n variant=\"none\"\n icon-button\n [aria-label]=\"label\"\n (click)=\"$event.stopPropagation(); cleared.emit()\">\n <filter-x-icon />\n </button>\n }\n @if (selection()) {\n @let label = \"filters.apply\" | transloco;\n <button\n variant=\"accent\"\n size=\"sm\"\n [aria-label]=\"label\"\n (click)=\"$event.stopPropagation(); applied.emit()\">\n <FilterIcon />\n {{ label }}\n </button>\n }\n\n @if (isAllSelected()) {\n @let label = \"filters.unselectAllFilters\" | transloco;\n <button\n variant=\"none\"\n icon-button\n [aria-label]=\"label\"\n (click)=\"$event.stopPropagation(); allUnselected.emit()\">\n <square-check-icon />\n </button>\n } @else {\n @let label = \"filters.selectAllFilters\" | transloco;\n <button\n variant=\"none\"\n icon-button\n [aria-label]=\"label\"\n (click)=\"$event.stopPropagation(); allSelected.emit()\">\n <square-icon />\n </button>\n }\n }\n\n @if (collapsible()) {\n <icon-button\n title=\"Open/Close\"\n class=\"cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\n <chevronright />\n <span class=\"sr-only\">{{ \"filters.toggle\" | transloco }}</span>\n </icon-button>\n }\n </summary>\n\n @if (aggregation()?.searchable && itemsLength() > 0) {\n <InputGroup class=\"group/item mt-3\">\n <input\n #searchInput\n input-group\n id=\"aggregation-input-{{ aggregation()?.column }}\"\n type=\"text\"\n [attr.aria-label]=\"'search' | transloco\"\n [attr.placeholder]=\"'search' | transloco\"\n [(ngModel)]=\"searchText\"\n class=\"mt-1\" />\n <InputGroupAddon>\n <SearchIcon\n class=\"text-foreground size-4 rotate-0 transition-[rotate] duration-500 group-focus-within/item:rotate-90\" />\n </InputGroupAddon>\n <InputGroupAddon align=\"inline-end\" class=\"gap-0.5!\">\n <icon-button\n size=\"sm\"\n [class]=\"\n searchText().length > 0\n ? 'rotate-90 cursor-pointer opacity-100 transition-[rotate,opacity] duration-500'\n : 'pointer-events-none rotate-0 opacity-0 transition-[rotate,opacity] duration-500'\n \"\n aria-label=\"Clear search\"\n [tabindex]=\"searchText().length > 0 ? 0 : -1\"\n (keydown.enter)=\"clearSearch($event)\"\n (click)=\"clearSearch($event)\">\n <XMarkIcon />\n </icon-button>\n <ng-content select=\"[search-addon]\" />\n </InputGroupAddon>\n </InputGroup>\n }\n\n <ng-content />\n\n @if (hasMore() && searchedItemsLength() === 0) {\n <button\n class=\"mt-1 flex w-full justify-center\"\n [attr.aria-label]=\"'loadMore' | transloco\"\n (click)=\"loadedMore.emit()\">\n {{ \"loadMore\" | transloco }}\n </button>\n }\n</details>\n" }]
14526
14542
  }], 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 }] }] } });
14527
14543
 
14528
14544
  class AggregationTreeComponent {
@@ -15598,6 +15614,15 @@ class AggregationListComponent {
15598
15614
  const filters = this.queryParamsStore.getFilter({ field: field ?? undefined, name: this.listItemName(item) });
15599
15615
  if (!filters)
15600
15616
  return false;
15617
+ const agg = this.aggregation();
15618
+ if (agg?.isDistribution) {
15619
+ // Same reasoning as `injectAggregationBase.addCurrentFiltersToItems`: a distribution item's
15620
+ // `value` is the full backend expression, not comparable to the applied filter's stored
15621
+ // thresholds — flatten it and compare on `display` (or `value` without `valuesAreExpressions`).
15622
+ const flattenedValues = flattenLegacyFilterValues([filters]);
15623
+ const valueToSearch = agg.valuesAreExpressions ? item.display : item.value;
15624
+ return flattenedValues.includes(valueToSearch?.toString() ?? "");
15625
+ }
15601
15626
  const values = [item.value];
15602
15627
  return values.some(v => v === filters.value) || !!(filters.values?.length && filters.values.some((value) => values.some(v => v === value)));
15603
15628
  }
@@ -16238,6 +16263,13 @@ class FilterButtonComponent {
16238
16263
  column = input.required(...(ngDevMode ? [{ debugName: "column" }] : []));
16239
16264
  position = input("bottom-start", ...(ngDevMode ? [{ debugName: "position" }] : []));
16240
16265
  offset = input(8, ...(ngDevMode ? [{ debugName: "offset" }] : []));
16266
+ /**
16267
+ * When true, forces the popover to stay at the exact `position()` placement,
16268
+ * disabling floating-ui's automatic repositioning based on available viewport space.
16269
+ *
16270
+ * @default false
16271
+ */
16272
+ lockPosition = input(false, ...(ngDevMode ? [{ debugName: "lockPosition", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
16241
16273
  expandedLevel = input(undefined, ...(ngDevMode ? [{ debugName: "expandedLevel", transform: numberAttribute }] : [{ transform: numberAttribute }]));
16242
16274
  variant = signal("ghost", ...(ngDevMode ? [{ debugName: "variant" }] : []));
16243
16275
  filter = signal({
@@ -16303,7 +16335,7 @@ class FilterButtonComponent {
16303
16335
  return this.appStore.isDateColumn(column);
16304
16336
  }
16305
16337
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: FilterButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
16306
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.29", type: FilterButtonComponent, isStandalone: true, selector: "filter-button, FilterButton", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, column: { classPropertyName: "column", publicName: "column", isSignal: true, isRequired: true, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", 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 } }, host: { attributes: { "role": "listitem" }, properties: { "class.hidden": "filter().hidden" } }, viewQueries: [{ propertyName: "popoverRef", first: true, predicate: PopoverComponent, descendants: true, isSignal: true }], ngImport: i0, template: `
16338
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.29", type: FilterButtonComponent, isStandalone: true, selector: "filter-button, FilterButton", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, column: { classPropertyName: "column", publicName: "column", isSignal: true, isRequired: true, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, offset: { classPropertyName: "offset", publicName: "offset", isSignal: true, isRequired: false, transformFunction: null }, lockPosition: { classPropertyName: "lockPosition", publicName: "lockPosition", isSignal: true, isRequired: false, transformFunction: null }, expandedLevel: { classPropertyName: "expandedLevel", publicName: "expandedLevel", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "listitem" }, properties: { "class.hidden": "filter().hidden" } }, viewQueries: [{ propertyName: "popoverRef", first: true, predicate: PopoverComponent, descendants: true, isSignal: true }], ngImport: i0, template: `
16307
16339
  <Popover [disabled]="filter().disabled" class="group">
16308
16340
  <button
16309
16341
  [variant]="variant()"
@@ -16336,13 +16368,14 @@ class FilterButtonComponent {
16336
16368
  class="min-w-fit w-max max-w-[90vw]"
16337
16369
  [position]="position()"
16338
16370
  [offset]="offset()"
16371
+ [lockPosition]="lockPosition()"
16339
16372
  >
16340
16373
  @if(contentRef.isVisible) {
16341
16374
  @let name = filter().name;
16342
16375
  @let column = filter().column;
16343
16376
  @if(name) {
16344
16377
  <Aggregation
16345
- class="w-60 [--height:19lh]"
16378
+ class="w-60 [--scroll-height:19lh]"
16346
16379
  id="filter-{{ filter().column }}"
16347
16380
  [name]="name"
16348
16381
  [column]="column"
@@ -16358,7 +16391,7 @@ class FilterButtonComponent {
16358
16391
  }
16359
16392
  </PopoverContent>
16360
16393
  </Popover>
16361
- `, 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: AggregationComponent, selector: "Aggregation, aggregation", inputs: ["class", "id", "name", "column", "showFiltersCount", "collapsible", "collapsed", "searchable", "showCheckbox", "applyOnSelect", "expandedLevel", "syncUrl", "query", "aggregation", "selection", "emitOn", "type", "hideWhenEmpty"], outputs: ["onSelect", "onApply", "onClear", "selectionChange", "filtersChange"] }, { kind: "directive", type: BadgeComponent, selector: "badge, Badge", inputs: ["class", "variant", "scheme", "size", "aria-label"] }, { kind: "component", type: FaIconComponent, selector: "fa-icon, FaIcon", inputs: ["faClass", "class"] }, { kind: "pipe", type: OperatorPipe, name: "operator" }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }, { kind: "pipe", type: SyslangPipe, name: "syslang" }] });
16394
+ `, 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", "lockPosition"], exportAs: ["popoverContent"] }, { kind: "component", type: AggregationComponent, selector: "Aggregation, aggregation", inputs: ["class", "id", "name", "column", "showFiltersCount", "collapsible", "collapsed", "searchable", "showCheckbox", "applyOnSelect", "expandedLevel", "syncUrl", "query", "aggregation", "selection", "emitOn", "type", "hideWhenEmpty"], outputs: ["onSelect", "onApply", "onClear", "selectionChange", "filtersChange"] }, { kind: "directive", type: BadgeComponent, selector: "badge, Badge", inputs: ["class", "variant", "scheme", "size", "aria-label"] }, { kind: "component", type: FaIconComponent, selector: "fa-icon, FaIcon", inputs: ["faClass", "class"] }, { kind: "pipe", type: OperatorPipe, name: "operator" }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }, { kind: "pipe", type: SyslangPipe, name: "syslang" }] });
16362
16395
  }
16363
16396
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: FilterButtonComponent, decorators: [{
16364
16397
  type: Component,
@@ -16409,13 +16442,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
16409
16442
  class="min-w-fit w-max max-w-[90vw]"
16410
16443
  [position]="position()"
16411
16444
  [offset]="offset()"
16445
+ [lockPosition]="lockPosition()"
16412
16446
  >
16413
16447
  @if(contentRef.isVisible) {
16414
16448
  @let name = filter().name;
16415
16449
  @let column = filter().column;
16416
16450
  @if(name) {
16417
16451
  <Aggregation
16418
- class="w-60 [--height:19lh]"
16452
+ class="w-60 [--scroll-height:19lh]"
16419
16453
  id="filter-{{ filter().column }}"
16420
16454
  [name]="name"
16421
16455
  [column]="column"
@@ -16437,11 +16471,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
16437
16471
  role: "listitem"
16438
16472
  }
16439
16473
  }]
16440
- }], ctorParameters: () => [], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], column: [{ type: i0.Input, args: [{ isSignal: true, alias: "column", required: true }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], offset: [{ type: i0.Input, args: [{ isSignal: true, alias: "offset", required: false }] }], expandedLevel: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandedLevel", required: false }] }], popoverRef: [{ type: i0.ViewChild, args: [i0.forwardRef(() => PopoverComponent), { isSignal: true }] }] } });
16474
+ }], ctorParameters: () => [], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], column: [{ type: i0.Input, args: [{ isSignal: true, alias: "column", required: true }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], offset: [{ type: i0.Input, args: [{ isSignal: true, alias: "offset", required: false }] }], lockPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "lockPosition", required: false }] }], expandedLevel: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandedLevel", required: false }] }], popoverRef: [{ type: i0.ViewChild, args: [i0.forwardRef(() => PopoverComponent), { isSignal: true }] }] } });
16441
16475
 
16442
16476
  const FILTERS_BREAKPOINT = new InjectionToken('FILTERS_BREAKPOINT', { factory: () => 5 });
16443
16477
 
16478
+ /**
16479
+ * Below this many overflow filters, the search input adds friction without
16480
+ * helping — scanning a handful of accordion headers is already fast.
16481
+ */
16482
+ const SEARCH_THRESHOLD = 6;
16444
16483
  class MoreComponent {
16484
+ SEARCH_THRESHOLD = SEARCH_THRESHOLD;
16445
16485
  count = input(2, ...(ngDevMode ? [{ debugName: "count" }] : []));
16446
16486
  includedFilters = input([], ...(ngDevMode ? [{ debugName: "includedFilters" }] : []));
16447
16487
  excludedFilters = input([], ...(ngDevMode ? [{ debugName: "excludedFilters" }] : []));
@@ -16457,6 +16497,7 @@ class MoreComponent {
16457
16497
  */
16458
16498
  hideWhenEmpty = input(false, ...(ngDevMode ? [{ debugName: "hideWhenEmpty", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
16459
16499
  aggregationsService = inject(AggregationsService);
16500
+ searchText = model("", ...(ngDevMode ? [{ debugName: "searchText" }] : []));
16460
16501
  /**
16461
16502
  * The overflow filters rendered as accordions: the authorized filters beyond the
16462
16503
  * first `count` (which are already shown in the bar).
@@ -16469,49 +16510,158 @@ class MoreComponent {
16469
16510
  filters = computed(() => this.aggregationsService
16470
16511
  .getAuthorizedFilters(this.aggregations(), this.includedFilters(), this.excludedFilters(), this.homepage())
16471
16512
  .toSpliced(0, this.count()), ...(ngDevMode ? [{ debugName: "filters" }] : []));
16513
+ /** {@link filters}, narrowed to those matching {@link searchText} (case-insensitive, by name). */
16514
+ filteredFilters = computed(() => {
16515
+ const query = this.searchText().trim().toLowerCase();
16516
+ if (!query)
16517
+ return this.filters();
16518
+ return this.filters().filter(filter => filter.name.toLowerCase().includes(query));
16519
+ }, ...(ngDevMode ? [{ debugName: "filteredFilters" }] : []));
16520
+ clearSearch(e) {
16521
+ e.stopImmediatePropagation();
16522
+ this.searchText.set("");
16523
+ }
16472
16524
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: MoreComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
16473
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.29", 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 }, hideWhenEmpty: { classPropertyName: "hideWhenEmpty", publicName: "hideWhenEmpty", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "divide-y divide-muted-foreground/18" }, ngImport: i0, template: `
16474
- @for (filter of filters(); track $index) {
16475
- <Aggregation
16476
- class="w-60 max-w-80 px-1 [--height:15lh]"
16477
- id="more-filters"
16478
- [attr.title]="'filters.openFilter' | transloco: { filter: filter.name }"
16479
- showFiltersCount
16480
- [collapsible]="true"
16481
- [collapsed]="true"
16482
- [hideWhenEmpty]="hideWhenEmpty()"
16483
- [name]="filter.name"
16484
- [column]="filter.column" />
16525
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.29", 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 }, hideWhenEmpty: { classPropertyName: "hideWhenEmpty", publicName: "hideWhenEmpty", isSignal: true, isRequired: false, transformFunction: null }, searchText: { classPropertyName: "searchText", publicName: "searchText", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { searchText: "searchTextChange" }, host: { classAttribute: "scrollbar-thin" }, ngImport: i0, template: `
16526
+ @if (filters().length > SEARCH_THRESHOLD) {
16527
+ <div class="sticky top-0 z-10 bg-menu-bg pb-3 shadow-[0_-1px_0_0_var(--color-menu-bg)]">
16528
+ <InputGroup>
16529
+ <input
16530
+ input-group
16531
+ type="text"
16532
+ [attr.aria-label]="'filters.searchFilters' | transloco"
16533
+ [attr.placeholder]="'filters.searchFilters' | transloco"
16534
+ [(ngModel)]="searchText" />
16535
+ <InputGroupAddon>
16536
+ <SearchIcon class="text-foreground size-4" />
16537
+ </InputGroupAddon>
16538
+ <InputGroupAddon align="inline-end" class="gap-0.5!">
16539
+ <icon-button
16540
+ size="sm"
16541
+ [class]="
16542
+ searchText().length > 0
16543
+ ? 'opacity-100 transition-opacity duration-500'
16544
+ : 'pointer-events-none opacity-0 transition-opacity duration-500'
16545
+ "
16546
+ aria-label="Clear search"
16547
+ [tabindex]="searchText().length > 0 ? 0 : -1"
16548
+ (keydown.enter)="clearSearch($event)"
16549
+ (click)="clearSearch($event)">
16550
+ <XMarkIcon />
16551
+ </icon-button>
16552
+ </InputGroupAddon>
16553
+ </InputGroup>
16554
+ </div>
16485
16555
  }
16486
- `, isInline: true, styles: [":host{scrollbar-width:none}\n"], dependencies: [{ kind: "component", type: AggregationComponent, selector: "Aggregation, aggregation", inputs: ["class", "id", "name", "column", "showFiltersCount", "collapsible", "collapsed", "searchable", "showCheckbox", "applyOnSelect", "expandedLevel", "syncUrl", "query", "aggregation", "selection", "emitOn", "type", "hideWhenEmpty"], outputs: ["onSelect", "onApply", "onClear", "selectionChange", "filtersChange"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
16556
+
16557
+ <div class="divide-y divide-muted-foreground/18">
16558
+ @for (filter of filteredFilters(); track filter.name) {
16559
+ <Aggregation
16560
+ class="w-full min-w-60 px-1 [--scroll-height:15lh]"
16561
+ id="more-filters"
16562
+ [attr.title]="'filters.openFilter' | transloco: { filter: filter.name }"
16563
+ showFiltersCount
16564
+ [collapsible]="true"
16565
+ [collapsed]="true"
16566
+ [hideWhenEmpty]="hideWhenEmpty()"
16567
+ [name]="filter.name"
16568
+ [column]="filter.column" />
16569
+ } @empty {
16570
+ @if (searchText().trim().length > 0) {
16571
+ <p class="text-muted-foreground px-2 py-4 text-center text-sm">{{ "filters.noFilterMatch" | transloco }}</p>
16572
+ }
16573
+ }
16574
+ </div>
16575
+ `, isInline: true, dependencies: [{ kind: "component", type: AggregationComponent, selector: "Aggregation, aggregation", inputs: ["class", "id", "name", "column", "showFiltersCount", "collapsible", "collapsed", "searchable", "showCheckbox", "applyOnSelect", "expandedLevel", "syncUrl", "query", "aggregation", "selection", "emitOn", "type", "hideWhenEmpty"], outputs: ["onSelect", "onApply", "onClear", "selectionChange", "filtersChange"] }, { 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: InputGroupComponent, selector: "input-group, inputgroup, InputGroup", inputs: ["class"] }, { kind: "directive", type: InputGroupInput, selector: "input[input-group]", inputs: ["class", "type", "placeholder", "disabled"] }, { kind: "directive", type: InputGroupAddonComponent, selector: "input-group-addon, inputgroupaddon, InputGroupAddon", inputs: ["class", "align"] }, { kind: "component", type: SearchIcon, selector: "SearchIcon", 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: TranslocoPipe, name: "transloco" }] });
16487
16576
  }
16488
16577
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: MoreComponent, decorators: [{
16489
16578
  type: Component,
16490
- args: [{ selector: "more, More", standalone: true, imports: [AggregationComponent, TranslocoPipe], template: `
16491
- @for (filter of filters(); track $index) {
16492
- <Aggregation
16493
- class="w-60 max-w-80 px-1 [--height:15lh]"
16494
- id="more-filters"
16495
- [attr.title]="'filters.openFilter' | transloco: { filter: filter.name }"
16496
- showFiltersCount
16497
- [collapsible]="true"
16498
- [collapsed]="true"
16499
- [hideWhenEmpty]="hideWhenEmpty()"
16500
- [name]="filter.name"
16501
- [column]="filter.column" />
16579
+ args: [{
16580
+ selector: "more, More",
16581
+ standalone: true,
16582
+ imports: [
16583
+ AggregationComponent,
16584
+ TranslocoPipe,
16585
+ FormsModule,
16586
+ InputGroupComponent,
16587
+ InputGroupInput,
16588
+ InputGroupAddonComponent,
16589
+ SearchIcon,
16590
+ XMarkIcon,
16591
+ IconButtonComponent
16592
+ ],
16593
+ template: `
16594
+ @if (filters().length > SEARCH_THRESHOLD) {
16595
+ <div class="sticky top-0 z-10 bg-menu-bg pb-3 shadow-[0_-1px_0_0_var(--color-menu-bg)]">
16596
+ <InputGroup>
16597
+ <input
16598
+ input-group
16599
+ type="text"
16600
+ [attr.aria-label]="'filters.searchFilters' | transloco"
16601
+ [attr.placeholder]="'filters.searchFilters' | transloco"
16602
+ [(ngModel)]="searchText" />
16603
+ <InputGroupAddon>
16604
+ <SearchIcon class="text-foreground size-4" />
16605
+ </InputGroupAddon>
16606
+ <InputGroupAddon align="inline-end" class="gap-0.5!">
16607
+ <icon-button
16608
+ size="sm"
16609
+ [class]="
16610
+ searchText().length > 0
16611
+ ? 'opacity-100 transition-opacity duration-500'
16612
+ : 'pointer-events-none opacity-0 transition-opacity duration-500'
16613
+ "
16614
+ aria-label="Clear search"
16615
+ [tabindex]="searchText().length > 0 ? 0 : -1"
16616
+ (keydown.enter)="clearSearch($event)"
16617
+ (click)="clearSearch($event)">
16618
+ <XMarkIcon />
16619
+ </icon-button>
16620
+ </InputGroupAddon>
16621
+ </InputGroup>
16622
+ </div>
16502
16623
  }
16503
- `, host: {
16504
- class: "divide-y divide-muted-foreground/18"
16505
- }, styles: [":host{scrollbar-width:none}\n"] }]
16506
- }], 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 }] }], hideWhenEmpty: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideWhenEmpty", required: false }] }] } });
16624
+
16625
+ <div class="divide-y divide-muted-foreground/18">
16626
+ @for (filter of filteredFilters(); track filter.name) {
16627
+ <Aggregation
16628
+ class="w-full min-w-60 px-1 [--scroll-height:15lh]"
16629
+ id="more-filters"
16630
+ [attr.title]="'filters.openFilter' | transloco: { filter: filter.name }"
16631
+ showFiltersCount
16632
+ [collapsible]="true"
16633
+ [collapsed]="true"
16634
+ [hideWhenEmpty]="hideWhenEmpty()"
16635
+ [name]="filter.name"
16636
+ [column]="filter.column" />
16637
+ } @empty {
16638
+ @if (searchText().trim().length > 0) {
16639
+ <p class="text-muted-foreground px-2 py-4 text-center text-sm">{{ "filters.noFilterMatch" | transloco }}</p>
16640
+ }
16641
+ }
16642
+ </div>
16643
+ `,
16644
+ host: {
16645
+ class: "scrollbar-thin"
16646
+ }
16647
+ }]
16648
+ }], 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 }] }], hideWhenEmpty: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideWhenEmpty", required: false }] }], searchText: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchText", required: false }] }, { type: i0.Output, args: ["searchTextChange"] }] } });
16507
16649
 
16508
16650
  class MoreButtonComponent {
16509
16651
  appStore = inject(AppStore);
16510
16652
  queryParamsStore = inject(QueryParamsStore);
16511
16653
  nativeElement = inject(ElementRef).nativeElement;
16654
+ breakpointObserver = inject(BreakpointObserverService);
16512
16655
  aggregationsService = inject(AggregationsService);
16513
16656
  count = input(2, ...(ngDevMode ? [{ debugName: "count", transform: numberAttribute }] : [{ transform: numberAttribute }]));
16514
16657
  position = input("bottom-end", ...(ngDevMode ? [{ debugName: "position" }] : []));
16658
+ /**
16659
+ * When true, forces the popover to stay at the exact `position()` placement,
16660
+ * disabling floating-ui's automatic repositioning based on available viewport space.
16661
+ *
16662
+ * @default false
16663
+ */
16664
+ lockPosition = input(false, ...(ngDevMode ? [{ debugName: "lockPosition", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
16515
16665
  includedFilters = input([], ...(ngDevMode ? [{ debugName: "includedFilters" }] : []));
16516
16666
  excludedFilters = input([], ...(ngDevMode ? [{ debugName: "excludedFilters" }] : []));
16517
16667
  aggregations = input(...(ngDevMode ? [undefined, { debugName: "aggregations" }] : []));
@@ -16524,79 +16674,201 @@ class MoreButtonComponent {
16524
16674
  * @default false
16525
16675
  */
16526
16676
  hideWhenEmpty = input(false, ...(ngDevMode ? [{ debugName: "hideWhenEmpty", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
16527
- totalFiltersCount = computed(() => {
16528
- const count = this.count();
16529
- const authorizedFilters = this.aggregationsService
16530
- .getAuthorizedFilters(this.aggregations(), this.includedFilters(), this.excludedFilters(), this.homepage())
16531
- .toSpliced(0, count);
16532
- const total = authorizedFilters.reduce((acc, filter) => {
16533
- const f = this.queryParamsStore.getFilter(filter);
16534
- acc += f?.count || 0;
16535
- return acc;
16536
- }, 0);
16537
- return total;
16538
- }, ...(ngDevMode ? [{ debugName: "totalFiltersCount" }] : []));
16677
+ /**
16678
+ * Above this many overflow filters, the desktop Popover escalates to a modal
16679
+ * Dialog instead of an anchored popover — useful once there are too many facets
16680
+ * to browse comfortably in an anchored popover. Left unset (the default), the
16681
+ * Popover always shows every overflow facet, however many there are, adapting
16682
+ * its own height to the available space (see `PopoverContentComponent`'s
16683
+ * `size()` middleware) rather than escalating. A commonly-used value is `6`.
16684
+ *
16685
+ * @default undefined — never escalate to Dialog
16686
+ */
16687
+ enrichedThreshold = input(...(ngDevMode ? [undefined, { debugName: "enrichedThreshold" }] : []));
16688
+ /** Mobile drawer open state — the desktop Popover/Dialog manage their own. */
16689
+ sheetOpen = signal(false, ...(ngDevMode ? [{ debugName: "sheetOpen" }] : []));
16690
+ /** The inline enriched Dialog — `<dialog #dialogRefEl>` binds a `DialogComponent` instance to the ref by default. */
16691
+ dialogRef = viewChild("dialogRefEl", ...(ngDevMode ? [{ debugName: "dialogRef" }] : []));
16692
+ /** The authorized filters beyond the first `count` already shown in the bar. */
16693
+ overflowFilters = computed(() => this.aggregationsService
16694
+ .getAuthorizedFilters(this.aggregations(), this.includedFilters(), this.excludedFilters(), this.homepage())
16695
+ .toSpliced(0, this.count()), ...(ngDevMode ? [{ debugName: "overflowFilters" }] : []));
16696
+ totalFiltersCount = computed(() => this.overflowFilters().reduce((acc, filter) => {
16697
+ const f = this.queryParamsStore.getFilter(filter);
16698
+ acc += f?.count || 0;
16699
+ return acc;
16700
+ }, 0), ...(ngDevMode ? [{ debugName: "totalFiltersCount" }] : []));
16701
+ /**
16702
+ * Above {@link enrichedThreshold} overflow filters, the popover escalates to a modal
16703
+ * Dialog. Always `false` while `enrichedThreshold` is unset — the Popover then shows
16704
+ * every overflow facet regardless of count.
16705
+ */
16706
+ isEnriched = computed(() => {
16707
+ const threshold = this.enrichedThreshold();
16708
+ return threshold !== undefined && this.overflowFilters().length > threshold;
16709
+ }, ...(ngDevMode ? [{ debugName: "isEnriched" }] : []));
16539
16710
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: MoreButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
16540
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.29", 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 }, hideWhenEmpty: { classPropertyName: "hideWhenEmpty", publicName: "hideWhenEmpty", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
16541
- <Popover class="group/more">
16542
- <button
16543
- variant="ghost"
16544
- class="group-open/more:border-foreground/18 gap-1 truncate font-semibold group-open/more:border"
16545
- aria-label="more filters">
16546
- <list-filter-icon aria-hidden="true" />
16547
- <span sr-only class="hidden sm:inline">{{ 'filters.moreFilters' | transloco }}</span>
16548
-
16549
- @if (totalFiltersCount() > 0) {
16550
- <Badge size="xs">
16551
- {{ totalFiltersCount() }}
16552
- </Badge>
16553
- }
16711
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.29", 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 }, lockPosition: { classPropertyName: "lockPosition", publicName: "lockPosition", 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 }, hideWhenEmpty: { classPropertyName: "hideWhenEmpty", publicName: "hideWhenEmpty", isSignal: true, isRequired: false, transformFunction: null }, enrichedThreshold: { classPropertyName: "enrichedThreshold", publicName: "enrichedThreshold", isSignal: true, isRequired: false, transformFunction: null } }, providers: [provideTranslocoScope("filters")], viewQueries: [{ propertyName: "dialogRef", first: true, predicate: ["dialogRefEl"], descendants: true, isSignal: true }], ngImport: i0, template: `
16712
+ <ng-template #trigger>
16713
+ <list-filter-icon aria-hidden="true" />
16714
+ <span sr-only class="hidden sm:inline">{{ 'filters.moreFilters' | transloco }}</span>
16715
+
16716
+ @if (totalFiltersCount() > 0) {
16717
+ <Badge size="xs">
16718
+ {{ totalFiltersCount() }}
16719
+ </Badge>
16720
+ }
16721
+ </ng-template>
16722
+
16723
+ @if (breakpointObserver.isMobile()) {
16724
+ <button variant="ghost" class="gap-1 truncate font-semibold" aria-label="more filters" (click)="sheetOpen.set(true)">
16725
+ <ng-container *ngTemplateOutlet="trigger" />
16554
16726
  </button>
16555
16727
 
16556
- <PopoverContent #contentRef="popoverContent" [position]="position()" class="min-w-max">
16557
- @if(contentRef.isVisible) {
16558
- <More [count]="count()" [includedFilters]="includedFilters()" [excludedFilters]="excludedFilters()" [aggregations]="aggregations()" [homepage]="homepage()" [hideWhenEmpty]="hideWhenEmpty()" class="block h-full w-full max-w-80 [--height:55vh] min-w-40 overflow-hidden" />
16559
- }
16560
- </PopoverContent>
16561
- </Popover>
16562
- `, 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", "hideWhenEmpty"] }, { kind: "directive", type: BadgeComponent, selector: "badge, Badge", inputs: ["class", "variant", "scheme", "size", "aria-label"] }, { kind: "component", type: ListFilterIcon, selector: "list-filter-icon, ListFilterIcon", inputs: ["class"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
16728
+ <sheet [open]="sheetOpen()" (openChange)="sheetOpen.set($event)" side="bottom" class="max-w-none">
16729
+ <sheet-header>
16730
+ <sheet-title>{{ 'filters.moreFilters' | transloco }}</sheet-title>
16731
+ </sheet-header>
16732
+ <div class="max-h-[75vh] overflow-y-auto overscroll-contain">
16733
+ <More [count]="count()" [includedFilters]="includedFilters()" [excludedFilters]="excludedFilters()" [aggregations]="aggregations()" [homepage]="homepage()" [hideWhenEmpty]="hideWhenEmpty()" class="block w-full" />
16734
+ </div>
16735
+ </sheet>
16736
+ } @else if (isEnriched()) {
16737
+ <button variant="ghost" class="gap-1 truncate font-semibold" aria-label="more filters" (click)="dialogRef()?.showModal()">
16738
+ <ng-container *ngTemplateOutlet="trigger" />
16739
+ </button>
16740
+
16741
+ <dialog #dialogRefEl>
16742
+ <DialogContent class="sm:max-w-lg rounded-(--radius-xl,1rem)">
16743
+ <DialogHeader>
16744
+ <DialogTitle>{{ 'filters.moreFilters' | transloco }}</DialogTitle>
16745
+ </DialogHeader>
16746
+ <div class="max-h-[60vh] overflow-y-auto overscroll-contain">
16747
+ <More [count]="count()" [includedFilters]="includedFilters()" [excludedFilters]="excludedFilters()" [aggregations]="aggregations()" [homepage]="homepage()" [hideWhenEmpty]="hideWhenEmpty()" class="block w-full" />
16748
+ </div>
16749
+ </DialogContent>
16750
+ </dialog>
16751
+ } @else {
16752
+ <Popover class="group/more">
16753
+ <button
16754
+ variant="ghost"
16755
+ class="group-open/more:border-foreground/18 gap-1 truncate font-semibold group-open/more:border"
16756
+ aria-label="more filters">
16757
+ <ng-container *ngTemplateOutlet="trigger" />
16758
+ </button>
16759
+
16760
+ <PopoverContent #contentRef="popoverContent" [position]="position()" [lockPosition]="lockPosition()" class="min-w-max max-w-80 min-w-40 rounded-(--radius-xl,1rem) p-5">
16761
+ @if(contentRef.isVisible) {
16762
+ <div class="max-h-[calc(var(--popover-available-height,55vh)_-_2.5rem)] overflow-y-auto overscroll-contain">
16763
+ <More [count]="count()" [includedFilters]="includedFilters()" [excludedFilters]="excludedFilters()" [aggregations]="aggregations()" [homepage]="homepage()" [hideWhenEmpty]="hideWhenEmpty()" class="block w-full" />
16764
+ </div>
16765
+ }
16766
+ </PopoverContent>
16767
+ </Popover>
16768
+ }
16769
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { 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", "lockPosition"], exportAs: ["popoverContent"] }, { kind: "component", type: MoreComponent, selector: "more, More", inputs: ["count", "includedFilters", "excludedFilters", "aggregations", "homepage", "hideWhenEmpty", "searchText"], outputs: ["searchTextChange"] }, { kind: "directive", type: BadgeComponent, selector: "badge, Badge", inputs: ["class", "variant", "scheme", "size", "aria-label"] }, { kind: "component", type: ListFilterIcon, selector: "list-filter-icon, ListFilterIcon", inputs: ["class"] }, { kind: "component", type: SheetComponent, selector: "sheet", inputs: ["open", "side", "class", "showCloseButton"], outputs: ["openChange"] }, { kind: "directive", type: SheetHeaderComponent, selector: "sheet-header", inputs: ["class"] }, { kind: "directive", type: SheetTitleComponent, selector: "sheet-title", inputs: ["class"] }, { kind: "component", type: DialogComponent, selector: "dialog, [dialog]", outputs: ["closed"], exportAs: ["dialog"] }, { kind: "directive", type: DialogContentComponent, selector: "DialogContent", inputs: ["class"] }, { kind: "component", type: DialogHeaderComponent, selector: "DialogHeader" }, { kind: "directive", type: DialogTitleComponent, selector: "DialogTitle", inputs: ["class"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
16563
16770
  }
16564
16771
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: MoreButtonComponent, decorators: [{
16565
16772
  type: Component,
16566
16773
  args: [{
16567
16774
  selector: "more-button, MoreButton",
16568
16775
  standalone: true,
16569
- imports: [ButtonComponent, PopoverComponent, PopoverContentComponent, TranslocoPipe, MoreComponent, BadgeComponent, ListFilterIcon],
16776
+ imports: [
16777
+ NgTemplateOutlet,
16778
+ ButtonComponent,
16779
+ PopoverComponent,
16780
+ PopoverContentComponent,
16781
+ TranslocoPipe,
16782
+ MoreComponent,
16783
+ BadgeComponent,
16784
+ ListFilterIcon,
16785
+ SheetComponent,
16786
+ SheetHeaderComponent,
16787
+ SheetTitleComponent,
16788
+ DialogComponent,
16789
+ DialogContentComponent,
16790
+ DialogHeaderComponent,
16791
+ DialogTitleComponent
16792
+ ],
16570
16793
  template: `
16571
- <Popover class="group/more">
16572
- <button
16573
- variant="ghost"
16574
- class="group-open/more:border-foreground/18 gap-1 truncate font-semibold group-open/more:border"
16575
- aria-label="more filters">
16576
- <list-filter-icon aria-hidden="true" />
16577
- <span sr-only class="hidden sm:inline">{{ 'filters.moreFilters' | transloco }}</span>
16578
-
16579
- @if (totalFiltersCount() > 0) {
16580
- <Badge size="xs">
16581
- {{ totalFiltersCount() }}
16582
- </Badge>
16583
- }
16794
+ <ng-template #trigger>
16795
+ <list-filter-icon aria-hidden="true" />
16796
+ <span sr-only class="hidden sm:inline">{{ 'filters.moreFilters' | transloco }}</span>
16797
+
16798
+ @if (totalFiltersCount() > 0) {
16799
+ <Badge size="xs">
16800
+ {{ totalFiltersCount() }}
16801
+ </Badge>
16802
+ }
16803
+ </ng-template>
16804
+
16805
+ @if (breakpointObserver.isMobile()) {
16806
+ <button variant="ghost" class="gap-1 truncate font-semibold" aria-label="more filters" (click)="sheetOpen.set(true)">
16807
+ <ng-container *ngTemplateOutlet="trigger" />
16584
16808
  </button>
16585
16809
 
16586
- <PopoverContent #contentRef="popoverContent" [position]="position()" class="min-w-max">
16587
- @if(contentRef.isVisible) {
16588
- <More [count]="count()" [includedFilters]="includedFilters()" [excludedFilters]="excludedFilters()" [aggregations]="aggregations()" [homepage]="homepage()" [hideWhenEmpty]="hideWhenEmpty()" class="block h-full w-full max-w-80 [--height:55vh] min-w-40 overflow-hidden" />
16589
- }
16590
- </PopoverContent>
16591
- </Popover>
16592
- `
16810
+ <sheet [open]="sheetOpen()" (openChange)="sheetOpen.set($event)" side="bottom" class="max-w-none">
16811
+ <sheet-header>
16812
+ <sheet-title>{{ 'filters.moreFilters' | transloco }}</sheet-title>
16813
+ </sheet-header>
16814
+ <div class="max-h-[75vh] overflow-y-auto overscroll-contain">
16815
+ <More [count]="count()" [includedFilters]="includedFilters()" [excludedFilters]="excludedFilters()" [aggregations]="aggregations()" [homepage]="homepage()" [hideWhenEmpty]="hideWhenEmpty()" class="block w-full" />
16816
+ </div>
16817
+ </sheet>
16818
+ } @else if (isEnriched()) {
16819
+ <button variant="ghost" class="gap-1 truncate font-semibold" aria-label="more filters" (click)="dialogRef()?.showModal()">
16820
+ <ng-container *ngTemplateOutlet="trigger" />
16821
+ </button>
16822
+
16823
+ <dialog #dialogRefEl>
16824
+ <DialogContent class="sm:max-w-lg rounded-(--radius-xl,1rem)">
16825
+ <DialogHeader>
16826
+ <DialogTitle>{{ 'filters.moreFilters' | transloco }}</DialogTitle>
16827
+ </DialogHeader>
16828
+ <div class="max-h-[60vh] overflow-y-auto overscroll-contain">
16829
+ <More [count]="count()" [includedFilters]="includedFilters()" [excludedFilters]="excludedFilters()" [aggregations]="aggregations()" [homepage]="homepage()" [hideWhenEmpty]="hideWhenEmpty()" class="block w-full" />
16830
+ </div>
16831
+ </DialogContent>
16832
+ </dialog>
16833
+ } @else {
16834
+ <Popover class="group/more">
16835
+ <button
16836
+ variant="ghost"
16837
+ class="group-open/more:border-foreground/18 gap-1 truncate font-semibold group-open/more:border"
16838
+ aria-label="more filters">
16839
+ <ng-container *ngTemplateOutlet="trigger" />
16840
+ </button>
16841
+
16842
+ <PopoverContent #contentRef="popoverContent" [position]="position()" [lockPosition]="lockPosition()" class="min-w-max max-w-80 min-w-40 rounded-(--radius-xl,1rem) p-5">
16843
+ @if(contentRef.isVisible) {
16844
+ <div class="max-h-[calc(var(--popover-available-height,55vh)_-_2.5rem)] overflow-y-auto overscroll-contain">
16845
+ <More [count]="count()" [includedFilters]="includedFilters()" [excludedFilters]="excludedFilters()" [aggregations]="aggregations()" [homepage]="homepage()" [hideWhenEmpty]="hideWhenEmpty()" class="block w-full" />
16846
+ </div>
16847
+ }
16848
+ </PopoverContent>
16849
+ </Popover>
16850
+ }
16851
+ `,
16852
+ // Self-registers the scope so <more-button> resolves its own translations when used
16853
+ // standalone, outside <filters-bar> (which already registers it too — harmless overlap).
16854
+ providers: [provideTranslocoScope("filters")]
16593
16855
  }]
16594
- }], 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 }] }], hideWhenEmpty: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideWhenEmpty", required: false }] }] } });
16856
+ }], propDecorators: { count: [{ type: i0.Input, args: [{ isSignal: true, alias: "count", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], lockPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "lockPosition", 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 }] }], hideWhenEmpty: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideWhenEmpty", required: false }] }], enrichedThreshold: [{ type: i0.Input, args: [{ isSignal: true, alias: "enrichedThreshold", required: false }] }], dialogRef: [{ type: i0.ViewChild, args: ["dialogRefEl", { isSignal: true }] }] } });
16595
16857
 
16596
16858
  class FiltersBarComponent {
16597
16859
  class = input(...(ngDevMode ? [undefined, { debugName: "class" }] : []));
16598
16860
  position = input("bottom-start", ...(ngDevMode ? [{ debugName: "position" }] : []));
16599
16861
  morePosition = input("bottom-end", ...(ngDevMode ? [{ debugName: "morePosition" }] : []));
16862
+ /**
16863
+ * When true, forces every popover (filter buttons and the "more filters"
16864
+ * button) to stay at its exact `position()`/`morePosition()` placement,
16865
+ * disabling floating-ui's automatic repositioning based on available
16866
+ * viewport space. Relayed as-is to each `<FilterButton [lockPosition]>`
16867
+ * and to `<MoreButton [lockPosition]>`.
16868
+ *
16869
+ * @default false
16870
+ */
16871
+ lockPosition = input(false, ...(ngDevMode ? [{ debugName: "lockPosition", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
16600
16872
  aggregations = input(...(ngDevMode ? [undefined, { debugName: "aggregations" }] : []));
16601
16873
  /**
16602
16874
  * Filters that should be included from the filters bar.
@@ -16700,7 +16972,7 @@ class FiltersBarComponent {
16700
16972
  allAuthorizedFilters = computed(() => {
16701
16973
  return this.aggregationsService
16702
16974
  .getAuthorizedFilters(this.aggregations(), this.includeFilters(), this.excludeFilters(), this.homepage())
16703
- .map((f) => ({ name: f.name, column: f.column }));
16975
+ .map(f => ({ name: f.name, column: f.column }));
16704
16976
  }, ...(ngDevMode ? [{ debugName: "allAuthorizedFilters" }] : []));
16705
16977
  /**
16706
16978
  * Computes the list of additional filters that can be displayed in the "more filters" popover.
@@ -16717,7 +16989,7 @@ class FiltersBarComponent {
16717
16989
  hasMoreFilters = computed(() => {
16718
16990
  const moreFiltersAggregations = this.allAuthorizedFilters()
16719
16991
  .toSpliced(0, this.visibleFiltersCount())
16720
- .map((f) => this.aggregationsStore.getAggregation(f.column, "column"));
16992
+ .map(f => this.aggregationsStore.getAggregation(f.column, "column"));
16721
16993
  return moreFiltersAggregations;
16722
16994
  }, ...(ngDevMode ? [{ debugName: "hasMoreFilters" }] : []));
16723
16995
  /**
@@ -16739,9 +17011,7 @@ class FiltersBarComponent {
16739
17011
  */
16740
17012
  hasCappedFilters = computed(() => this.allAuthorizedFilters().length > this.filtersCount(), ...(ngDevMode ? [{ debugName: "hasCappedFilters" }] : []));
16741
17013
  constructor() {
16742
- this.transloco.events$
16743
- .pipe(takeUntilDestroyed(this.destroyRef), debounceTime(100))
16744
- .subscribe(() => this.overflowManagerRef()?.countItems());
17014
+ this.transloco.events$.pipe(takeUntilDestroyed(this.destroyRef), debounceTime(100)).subscribe(() => this.overflowManagerRef()?.countItems());
16745
17015
  // Recount the overflow whenever the applied filters or basket change (e.g.
16746
17016
  // a filter modified or removed from the "more filters" popover). A
16747
17017
  // FilterButton hidden by the overflow manager (display: none) emits no
@@ -16800,12 +17070,12 @@ class FiltersBarComponent {
16800
17070
  * @param event - The mouse event triggered by the user's click.
16801
17071
  */
16802
17072
  handleClick(event) {
16803
- this.filterButtonRefs().forEach((ref) => {
17073
+ this.filterButtonRefs().forEach(ref => {
16804
17074
  ref.popoverRef()?.onClick(event);
16805
17075
  });
16806
17076
  }
16807
17077
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: FiltersBarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
16808
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.29", 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: `
17078
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.29", 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 }, lockPosition: { classPropertyName: "lockPosition", publicName: "lockPosition", 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: `
16809
17079
  <div overflowManager [direction]="direction()" [reserveStop]="hasCappedFilters()" (count)="adjustFiltersCount($event)" class="flex items-end gap-2 rounded-[inherit] bg-inherit">
16810
17080
  @if (hasFilters()) {
16811
17081
  <button
@@ -16839,7 +17109,7 @@ class FiltersBarComponent {
16839
17109
  role="list"
16840
17110
  aria-label="Filters list">
16841
17111
  @for (filter of authorizedFilters(); track filter.name) {
16842
- <FilterButton class="max-w-60" overflowItem [name]="filter.name" [column]="filter.column" [position]="position()" [offset]="offset()" [expandedLevel]="expandedLevel()" />
17112
+ <FilterButton class="max-w-60" overflowItem [name]="filter.name" [column]="filter.column" [position]="position()" [lockPosition]="lockPosition()" [offset]="offset()" [expandedLevel]="expandedLevel()" />
16843
17113
  }
16844
17114
  </div>
16845
17115
 
@@ -16851,6 +17121,7 @@ class FiltersBarComponent {
16851
17121
  role="listitem"
16852
17122
  [count]="visibleFiltersCount()"
16853
17123
  [position]="morePosition()"
17124
+ [lockPosition]="lockPosition()"
16854
17125
  [includedFilters]="includeFilters()"
16855
17126
  [excludedFilters]="excludeFilters()"
16856
17127
  [aggregations]="aggregations()"
@@ -16858,7 +17129,7 @@ class FiltersBarComponent {
16858
17129
  }
16859
17130
  }
16860
17131
  </div>
16861
- `, 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", "hideWhenEmpty"] }, { 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" }] });
17132
+ `, 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", "lockPosition", "includedFilters", "excludedFilters", "aggregations", "homepage", "hideWhenEmpty", "enrichedThreshold"] }, { kind: "component", type: FilterButtonComponent, selector: "filter-button, FilterButton", inputs: ["name", "column", "position", "offset", "lockPosition", "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" }] });
16862
17133
  }
16863
17134
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: FiltersBarComponent, decorators: [{
16864
17135
  type: Component,
@@ -16911,7 +17182,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
16911
17182
  role="list"
16912
17183
  aria-label="Filters list">
16913
17184
  @for (filter of authorizedFilters(); track filter.name) {
16914
- <FilterButton class="max-w-60" overflowItem [name]="filter.name" [column]="filter.column" [position]="position()" [offset]="offset()" [expandedLevel]="expandedLevel()" />
17185
+ <FilterButton class="max-w-60" overflowItem [name]="filter.name" [column]="filter.column" [position]="position()" [lockPosition]="lockPosition()" [offset]="offset()" [expandedLevel]="expandedLevel()" />
16915
17186
  }
16916
17187
  </div>
16917
17188
 
@@ -16923,6 +17194,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
16923
17194
  role="listitem"
16924
17195
  [count]="visibleFiltersCount()"
16925
17196
  [position]="morePosition()"
17197
+ [lockPosition]="lockPosition()"
16926
17198
  [includedFilters]="includeFilters()"
16927
17199
  [excludedFilters]="excludeFilters()"
16928
17200
  [aggregations]="aggregations()"
@@ -16936,7 +17208,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
16936
17208
  "(click)": "handleClick($event)"
16937
17209
  }
16938
17210
  }]
16939
- }], 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 }] }] } });
17211
+ }], 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 }] }], lockPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "lockPosition", 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 }] }] } });
16940
17212
 
16941
17213
  class LabelService {
16942
17214
  appStore = inject(AppStore);