@sinequa/atomic-angular 1.7.2 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,13 @@
1
1
  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
- import { BehaviorSubject, Subscription, Subject, of, catchError, map, tap, EMPTY, throwError, filter, shareReplay, from, switchMap, firstValueFrom, fromEvent, debounceTime } from 'rxjs';
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
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';
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, getQueryParamsFromUrl, clearSessionTokens, login, info, error, setGlobalConfig, initializeAppConfig, notify, addConcepts, queryParamsFromUrl, patchUserSettings, deleteUserSettings, fetchUserSettings, warn, buildPathsAndLevels, escapeExpr, isAuthenticated, isExpired, debug, AuditEventType, fetchSuggest, isObject, Audit, getMetadata, bisect, isNotInputEvent, fetchSponsoredLinks, fetchQuery, translateAggregationToDateOptions, aggItemRegex, parseValueAndOperatorFromItem, fetchSuggestField, fetchSimilarDocuments, logout, fetchChangePassword, fetchSendPasswordResetEmail, expiresSoon, suggestionsToTreeAggregationNodes, labels, fetchLabels, guid, getRelativeDate, createUserProfile, deleteUserProfileProperty, patchUserProfile, isJsonable, addAuditAdditionalInfo, getToken, setToken, createHeaders } from '@sinequa/atomic';
10
+ import { EngineType, fetchApp, extraColumns, sysLang, fetchPrincipal, globalConfig, filter, getQueryParamsFromUrl, clearSessionTokens, login, info, 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';
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';
@@ -1365,6 +1365,64 @@ class InlineWorker {
1365
1365
  }
1366
1366
  }
1367
1367
 
1368
+ /**
1369
+ * Converts a `LegacyFilter` to the structured `Filter` model, built with the
1370
+ * `filter.*` helpers of `@sinequa/atomic`. The result is assignable as-is to
1371
+ * `query.filters` / `fetchQuery({ filters })`.
1372
+ *
1373
+ * Covers the shapes produced by the aggregation components when applying
1374
+ * filters (`toFilter`, `getFiltersForTree`, `getFormValueFilter`,
1375
+ * `buildAppliedFilter`): `in` values, `and`/`or` expression trees, `between`
1376
+ * ranges, scalar leaves (`eq`/`contains`/`gte`/`lte`/…) and the operator-less
1377
+ * `{ field, value }` form (treated as `eq`).
1378
+ *
1379
+ * Returns `undefined` for an absent filter or a degenerate shape (leaf without
1380
+ * `field`/`value`, empty combinator) — consistent with the `filter.and`/`or`
1381
+ * collapsing rules and with an absent `query.filters`.
1382
+ */
1383
+ function legacyFilterToFilter(legacy) {
1384
+ if (!legacy)
1385
+ return undefined;
1386
+ const { operator, field } = legacy;
1387
+ switch (operator) {
1388
+ case "and":
1389
+ case "or": {
1390
+ const children = (legacy.filters ?? []).map((f) => legacyFilterToFilter(f));
1391
+ return operator === "and" ? filter.and(...children) : filter.or(...children);
1392
+ }
1393
+ case "not": {
1394
+ const children = (legacy.filters ?? []).map((f) => legacyFilterToFilter(f));
1395
+ return filter.not(...children);
1396
+ }
1397
+ case "in":
1398
+ return field && legacy.values?.length ? filter.in(field, legacy.values) : undefined;
1399
+ case "between":
1400
+ return field && legacy.start !== undefined && legacy.end !== undefined
1401
+ ? filter.between(field, legacy.start, legacy.end)
1402
+ : undefined;
1403
+ case "null":
1404
+ return field ? filter.isNull(field) : undefined;
1405
+ case "notnull":
1406
+ return field ? filter.isNotNull(field) : undefined;
1407
+ case "eq":
1408
+ case "neq":
1409
+ case "gt":
1410
+ case "gte":
1411
+ case "lt":
1412
+ case "lte":
1413
+ case "like":
1414
+ case "contains":
1415
+ case "regex":
1416
+ return field && legacy.value !== undefined ? filter[operator](field, legacy.value) : undefined;
1417
+ case undefined:
1418
+ // operator-less leaf ({ field, value }) — the `toFilter` form for
1419
+ // non-string values; semantically an equality
1420
+ return field && legacy.value !== undefined ? filter.eq(field, legacy.value) : undefined;
1421
+ default:
1422
+ return undefined;
1423
+ }
1424
+ }
1425
+
1368
1426
  /**
1369
1427
  * Retrieves the query name from the current route.
1370
1428
  *
@@ -3209,11 +3267,13 @@ class AggregationsService {
3209
3267
  queryParamsStore = inject(QueryParamsStore);
3210
3268
  nonSearchableColumns = inject(NON_SEARCHABLE_COLUMNS);
3211
3269
  API_URL = `${globalConfig.backendUrl}/api/v1`;
3212
- loadMore(query, aggregation, audit) {
3270
+ loadMore(query, aggregation, audit, options) {
3213
3271
  const { app } = globalConfig;
3214
3272
  // skip cached items for computation
3215
3273
  const skip = aggregation.items?.filter((i) => !i.$cached)?.length || 0;
3216
- const count = this.appStore.getAggregationCount(query.name, aggregation.name) || 10;
3274
+ // options.count carries the page size of a controlled (non-attached) query —
3275
+ // getAggregationCount only knows queries attached to the current app
3276
+ const count = options?.count ?? (this.appStore.getAggregationCount(query.name, aggregation.name) || 10);
3217
3277
  const aggregations = { [aggregation.name]: { skip, count } };
3218
3278
  const q = { ...query, action: "aggregate", aggregations };
3219
3279
  // add audit record if available
@@ -3242,7 +3302,7 @@ class AggregationsService {
3242
3302
  info("aggregation.loadMore success - data: ", response);
3243
3303
  }));
3244
3304
  }
3245
- open(query, aggregation, item) {
3305
+ open(query, aggregation, item, options) {
3246
3306
  const value = `/${item.$path}/*`;
3247
3307
  const expression = `${aggregation.column}:${escapeExpr(value)}`;
3248
3308
  const q = {
@@ -3252,7 +3312,18 @@ class AggregationsService {
3252
3312
  open: [{ expression, aggregation: aggregation.name }]
3253
3313
  };
3254
3314
  // we dont' want to use the query params (filters) when opening nodes
3255
- return this.queryService.search(q, false).pipe(map((results) => {
3315
+ // Guard-free mode calls fetchQuery directly: QueryService.search reads the shared
3316
+ // QueryParamsStore and publishes into its app-wide `result` signal, which a
3317
+ // controlled (per-instance) aggregation must not touch.
3318
+ const results$ = options?.guardFree
3319
+ ? from(fetchQuery(q)).pipe(tap((results) => {
3320
+ results.aggregations?.forEach((agg) => {
3321
+ if (agg.isTree)
3322
+ buildPathsAndLevels(agg);
3323
+ });
3324
+ }))
3325
+ : this.queryService.search(q, false);
3326
+ return results$.pipe(map((results) => {
3256
3327
  // find the node in the tree and replace it with the new one
3257
3328
  const items = results.aggregations[0].items;
3258
3329
  // if the open call returns no items, we need to update the node to indicate it has no children
@@ -3285,6 +3356,19 @@ class AggregationsService {
3285
3356
  const agg = this.getAggregation(name, column);
3286
3357
  if (!agg)
3287
3358
  return null;
3359
+ return this.enrichAggregation(agg);
3360
+ }
3361
+ /**
3362
+ * Applies the app-level customization (custom JSON display/icon/hidden/searchable,
3363
+ * non-searchable columns) and marks the `$selected` items on the given aggregation.
3364
+ *
3365
+ * @param agg - The aggregation to enrich (mutated in place and returned).
3366
+ * @param selectionValues - Controlled selection: the selected values (list) or
3367
+ * `"/A/B/*"` paths (tree). When `undefined`, the selection is derived from the
3368
+ * shared `QueryParamsStore` filters (default behavior). An empty array means
3369
+ * "nothing selected" and skips the store read entirely.
3370
+ */
3371
+ enrichAggregation(agg, selectionValues) {
3288
3372
  const { items = [], display = agg.name, icon, hidden, expandedLevel, searchable = true } = this.appStore.getAggregationCustomization(agg.column, agg.name) || {};
3289
3373
  agg.display = display;
3290
3374
  agg.icon = icon;
@@ -3298,9 +3382,15 @@ class AggregationsService {
3298
3382
  agg.searchable = false;
3299
3383
  }
3300
3384
  if (agg.items) {
3301
- // get current filters for the aggregation
3302
- const filters = this.queryParamsStore.getFilter({ field: agg.column, name: agg.name });
3303
- const flattenedValues = filters ? this.flattenFilters([filters]) : [];
3385
+ let flattenedValues;
3386
+ if (selectionValues !== undefined) {
3387
+ flattenedValues = selectionValues;
3388
+ }
3389
+ else {
3390
+ // get current filters for the aggregation
3391
+ const filters = this.queryParamsStore.getFilter({ field: agg.column, name: agg.name });
3392
+ flattenedValues = filters ? this.flattenFilters([filters]) : [];
3393
+ }
3304
3394
  if (agg.isTree) {
3305
3395
  const selectedAgg = agg.items.map((item) => this.selectNode(item, flattenedValues));
3306
3396
  const openedAgg = selectedAgg.map((item) => this.openParentNodes(item, flattenedValues));
@@ -3450,16 +3540,33 @@ class AggregationsService {
3450
3540
  }
3451
3541
  /* aggregations helpers fot the filters components */
3452
3542
  getAuthorizedFilters(aggregations, includedFilters = [], excludedFilters = [], homepageOnly = false) {
3543
+ // An explicit list is a deliberate developer choice: it bypasses the `hidden`
3544
+ // admin rule. The rule only applies to the default, config-driven path.
3545
+ const explicit = aggregations !== undefined;
3453
3546
  const agg = aggregations || this.appStore.getAuthorizedFilters();
3454
3547
  const authorizedFilters = agg
3455
3548
  .filter((f) => this.getFilterCriteria()(f)) // filter only the filters present
3456
3549
  .filter((f) => !homepageOnly || this.getHomepageFilterCriteria()(f)) // when requested, keep only filters flagged `homepage: true`
3457
3550
  .filter((f) => !this.getLeftOnlyFilterCriteria()(f)) // exclude left-only filters (rendered in the left drawer, not the bar)
3551
+ .filter((f) => explicit || !this.getHiddenFilterCriteria()(f)) // exclude admin-hidden filters (default path only)
3458
3552
  .filter((f) => !excludedFilters.includes(f.name))
3459
3553
  .filter((f) => !includedFilters.length || includedFilters.includes(f.name))
3460
3554
  .map((f) => ({ field: f.column, column: f.column, name: f.name })); // field is needed for filters constructions
3461
3555
  return authorizedFilters;
3462
3556
  }
3557
+ /**
3558
+ * Removes the aggregations flagged `hidden: true` in the `filters` custom JSON
3559
+ * configuration (the administrator does not want them displayed).
3560
+ *
3561
+ * Exposed for consumers that build their own aggregation list without going through
3562
+ * {@link getAuthorizedFilters} (e.g. the aside filters drawer).
3563
+ *
3564
+ * @param aggregations - The aggregations to filter.
3565
+ * @returns The aggregations that are not flagged hidden.
3566
+ */
3567
+ rejectHidden(aggregations) {
3568
+ return aggregations.filter((agg) => !this.getHiddenFilterCriteria()(agg));
3569
+ }
3463
3570
  /**
3464
3571
  * Determines whether a custom JSON filter refers to the given aggregation.
3465
3572
  *
@@ -3503,6 +3610,10 @@ class AggregationsService {
3503
3610
  // `position: 'both'` (and an undefined position) stay in the bar.
3504
3611
  return (agg) => this.appStore.filters().some((filter) => filter.position === "left" && this.matchesAggregation(filter, agg));
3505
3612
  };
3613
+ getHiddenFilterCriteria = () => {
3614
+ // an aggregation is hidden when a matching filter in the custom JSON has `hidden: true`
3615
+ return (agg) => this.appStore.filters().some((filter) => filter.hidden && this.matchesAggregation(filter, agg));
3616
+ };
3506
3617
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: AggregationsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
3507
3618
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: AggregationsService, providedIn: "root" });
3508
3619
  }
@@ -4201,7 +4312,7 @@ class NavigationService {
4201
4312
  *
4202
4313
  * @type Observable<RouterEvent>
4203
4314
  */
4204
- navigationEnd$ = this.router.events.pipe(map((event) => event), filter((event) => event instanceof NavigationEnd), tap((event) => {
4315
+ navigationEnd$ = this.router.events.pipe(map((event) => event), filter$1((event) => event instanceof NavigationEnd), tap((event) => {
4205
4316
  const url = event.url.slice(1).split("?")[0]; // Extract route name
4206
4317
  if (url && url !== this.urlAfterNavigation) {
4207
4318
  this.auditService.notifyRouteChange(url);
@@ -6488,6 +6599,43 @@ function injectRouteNavigation(path, showCount) {
6488
6599
  return { tabs, currentPath, searchText };
6489
6600
  }
6490
6601
 
6602
+ /**
6603
+ * Marker directive: project a custom template for the content/label region of
6604
+ * each aggregation item. Put it on an `<ng-template>` inside `<aggregation-list>`
6605
+ * or `<aggregation-tree>`:
6606
+ *
6607
+ * ```html
6608
+ * <aggregation-list name="Languages" column="language" [aggregation]="agg">
6609
+ * <ng-template aggregationItem let-item let-count="count" let-selected="selected">
6610
+ * <span>{{ item.display }}</span>
6611
+ * <span class="badge">{{ count }}</span>
6612
+ * </ng-template>
6613
+ * </aggregation-list>
6614
+ * ```
6615
+ *
6616
+ * When no template is projected, the component renders its default row content.
6617
+ * The `let-*` variables are typed under `strictTemplates` via
6618
+ * {@link AggregationItemDirective.ngTemplateContextGuard}; the host must import
6619
+ * this directive in its component `imports`.
6620
+ */
6621
+ class AggregationItemDirective {
6622
+ template = inject(TemplateRef);
6623
+ /**
6624
+ * Narrows the `let-*` of `<ng-template aggregationItem>` under `strictTemplates`.
6625
+ * Tree-only fields (`level`/`expanded`/`hasChildren`) are typed as optional; a tree
6626
+ * template can re-annotate them via {@link AggregationTreeItemContext} if desired.
6627
+ */
6628
+ static ngTemplateContextGuard(_dir, ctx) {
6629
+ return true;
6630
+ }
6631
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: AggregationItemDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
6632
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.25", type: AggregationItemDirective, isStandalone: true, selector: "[aggregationItem]", ngImport: i0 });
6633
+ }
6634
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: AggregationItemDirective, decorators: [{
6635
+ type: Directive,
6636
+ args: [{ selector: "[aggregationItem]" }]
6637
+ }] });
6638
+
6491
6639
  /**
6492
6640
  * Directive to mark a child element for special processing.
6493
6641
  * This can be used to apply specific styles or behaviors to child elements.
@@ -8912,12 +9060,98 @@ function injectAggregationBase(refs) {
8912
9060
  const aggregationsService = inject(AggregationsService);
8913
9061
  const injector = inject(Injector);
8914
9062
  const destroyRef = inject(DestroyRef);
9063
+ /* controlled mode — each axis is controlled when its input is provided.
9064
+ Writes (store/URL/sessionStorage) are gated by the global `isControlled` flag. */
9065
+ const isQueryControlled = computed(() => refs.query?.() !== undefined, ...(ngDevMode ? [{ debugName: "isQueryControlled" }] : []));
9066
+ const isDataControlled = computed(() => refs.aggregation?.() !== undefined, ...(ngDevMode ? [{ debugName: "isDataControlled" }] : []));
9067
+ const isSelectionControlled = computed(() => refs.selection?.() !== undefined, ...(ngDevMode ? [{ debugName: "isSelectionControlled" }] : []));
9068
+ const isControlled = computed(() => isQueryControlled() || isDataControlled() || isSelectionControlled(), ...(ngDevMode ? [{ debugName: "isControlled" }] : []));
9069
+ /** The query the aggregation's own operations run against. */
9070
+ function resolvedQuery() {
9071
+ return refs.query?.() ?? queryParamsStore.getQuery();
9072
+ }
9073
+ /**
9074
+ * Load-more page size when the query axis is controlled: the host's query
9075
+ * carries its own aggregation counts (`query.aggregations[name].count`), since
9076
+ * `AppStore.getAggregationCount` only knows queries attached to the current app.
9077
+ * Returns `undefined` in default mode — the service then resolves the count
9078
+ * from the app configuration, as today.
9079
+ */
9080
+ function resolvedAggregationCount(agg) {
9081
+ const q = refs.query?.();
9082
+ if (!q)
9083
+ return undefined;
9084
+ const aggregations = q.aggregations;
9085
+ if (aggregations && !Array.isArray(aggregations)) {
9086
+ return aggregations[agg.name]?.count ?? 10;
9087
+ }
9088
+ return 10;
9089
+ }
9090
+ /**
9091
+ * Local aggregation state in controlled mode, seeded from the `aggregation`
9092
+ * input (or the shared store when only the query/selection axis is controlled).
9093
+ * `loadMore`/`open` write their results here instead of the shared store.
9094
+ *
9095
+ * The seed is deep-cloned: items are mutated in place (`$selected`, `$opened`),
9096
+ * so working on the host's (or the store's) objects would leak state across
9097
+ * instances and mutate the host's data. Any reference change of the input
9098
+ * reseeds the signal and discards local enrichments (loaded pages, opened nodes).
9099
+ */
9100
+ const controlledAggregation = linkedSignal(() => {
9101
+ if (!isControlled())
9102
+ return null;
9103
+ const source = refs.aggregation?.() ??
9104
+ (getState(aggregationsStore), aggregationsService.getAggregation(refs.name() ?? "", refs.column()));
9105
+ return source ? structuredClone(source) : null;
9106
+ }, ...(ngDevMode ? [{ debugName: "controlledAggregation" }] : []));
8915
9107
  const debouncedSearchText = debouncedSignal(refs.searchText, 300);
8916
9108
  const normalizedSearchText = computed(() => debouncedSearchText()
8917
9109
  .normalize("NFD")
8918
9110
  .replace(/[̀-ͯ]/g, ""), ...(ngDevMode ? [{ debugName: "normalizedSearchText" }] : []));
8919
9111
  const suggests = signal([], ...(ngDevMode ? [{ debugName: "suggests" }] : []));
9112
+ /**
9113
+ * The selected values (list) or `"/A/B/*"` paths (tree) in controlled mode.
9114
+ * `undefined` in default mode — the selection is then derived from the shared
9115
+ * `QueryParamsStore` filters. In controlled mode without a `selection` input,
9116
+ * this is an empty array: the store filters belong to the shared query, not to
9117
+ * the host's, and must never leak into a controlled instance.
9118
+ */
9119
+ const selectionValues = computed(() => {
9120
+ const sel = refs.selection?.();
9121
+ if (sel) {
9122
+ switch (sel.kind) {
9123
+ case "values":
9124
+ return sel.values;
9125
+ case "paths":
9126
+ return sel.paths;
9127
+ case "option":
9128
+ case "range":
9129
+ return [];
9130
+ }
9131
+ }
9132
+ return isControlled() ? [] : undefined;
9133
+ }, ...(ngDevMode ? [{ debugName: "selectionValues" }] : []));
9134
+ /** The controlled selection expressed in the shape `getFilter()` returns. */
9135
+ const selectionAsFilter = computed(() => {
9136
+ const sel = refs.selection?.();
9137
+ return sel?.kind === "values" && sel.values.length ? { values: sel.values } : null;
9138
+ }, ...(ngDevMode ? [{ debugName: "selectionAsFilter" }] : []));
8920
9139
  const _filterCount = computed(() => {
9140
+ if (isControlled()) {
9141
+ const sel = refs.selection?.();
9142
+ if (!sel)
9143
+ return 0;
9144
+ switch (sel.kind) {
9145
+ case "values":
9146
+ return sel.values.length;
9147
+ case "paths":
9148
+ return sel.paths.length;
9149
+ case "option":
9150
+ return sel.option ? 1 : 0;
9151
+ case "range":
9152
+ return sel.from || sel.to ? 1 : 0;
9153
+ }
9154
+ }
8921
9155
  getState(aggregationsStore);
8922
9156
  const { count = 0 } = queryParamsStore.getFilter({ field: refs.column() ?? undefined, name: refs.name() ?? undefined }) || {};
8923
9157
  return count;
@@ -8940,7 +9174,7 @@ function injectAggregationBase(refs) {
8940
9174
  .pipe(takeUntilDestroyed(destroyRef), switchMap(({ text, column }) => {
8941
9175
  if (text === "" || column === null)
8942
9176
  return of([]);
8943
- const q = queryParamsStore.getQuery();
9177
+ const q = resolvedQuery();
8944
9178
  return from(withFetch(() => fetchSuggestField(normalizedSearchText(), [column || ""], q), injector));
8945
9179
  }))
8946
9180
  .subscribe((result) => suggests.set(result || []));
@@ -8959,7 +9193,9 @@ function injectAggregationBase(refs) {
8959
9193
  function addCurrentFiltersToItems(aggregation) {
8960
9194
  const currentItems = aggregation?.items || [];
8961
9195
  if (!aggregation?.isTree && !aggregation?.isDistribution) {
8962
- const currentFilters = queryParamsStore.getFilter({ field: aggregation?.column, name: aggregation?.name });
9196
+ const currentFilters = isControlled()
9197
+ ? selectionAsFilter()
9198
+ : queryParamsStore.getFilter({ field: aggregation?.column, name: aggregation?.name });
8963
9199
  if (currentFilters) {
8964
9200
  if (Array.isArray(currentFilters.filters) && currentFilters.filters.length) {
8965
9201
  currentFilters.filters.forEach((filter) => {
@@ -8998,34 +9234,45 @@ function injectAggregationBase(refs) {
8998
9234
  }
8999
9235
  return currentItems;
9000
9236
  }
9001
- function applyFilters(appliedFilters, agg, clearFn) {
9237
+ /**
9238
+ * Aggregates the applied item filters into the single `LegacyFilter` that
9239
+ * `applyFilters` writes to the store. Also used by the controlled mode to
9240
+ * build the emitted structured filter, so both modes stay strictly
9241
+ * consistent. Returns `undefined` when nothing is applied (the clear case).
9242
+ */
9243
+ function buildAppliedFilter(appliedFilters, agg) {
9002
9244
  const { name: aggName, column: field } = agg;
9003
- const syncUrl = refs.syncUrl?.() ?? true;
9004
9245
  if (appliedFilters.length > 1) {
9005
9246
  const display = appliedFilters[0].display;
9006
9247
  if (agg.isDistribution) {
9007
- queryParamsStore.updateFilter({
9248
+ return {
9008
9249
  operator: "or",
9009
9250
  filters: appliedFilters,
9010
9251
  name: aggName,
9011
9252
  field,
9012
9253
  display,
9013
- }, undefined, { syncUrl });
9014
- }
9015
- else {
9016
- const values = appliedFilters.map((f) => f.value);
9017
- queryParamsStore.updateFilter({
9018
- operator: "in",
9019
- name: aggName,
9020
- field,
9021
- values,
9022
- display,
9023
- filters: appliedFilters,
9024
- }, undefined, { syncUrl });
9254
+ };
9025
9255
  }
9256
+ const values = appliedFilters.map((f) => f.value);
9257
+ return {
9258
+ operator: "in",
9259
+ name: aggName,
9260
+ field,
9261
+ values,
9262
+ display,
9263
+ filters: appliedFilters,
9264
+ };
9026
9265
  }
9027
- else if (appliedFilters.length === 1) {
9028
- queryParamsStore.updateFilter(appliedFilters[0], undefined, { syncUrl });
9266
+ if (appliedFilters.length === 1) {
9267
+ return appliedFilters[0];
9268
+ }
9269
+ return undefined;
9270
+ }
9271
+ function applyFilters(appliedFilters, agg, clearFn) {
9272
+ const syncUrl = refs.syncUrl?.() ?? true;
9273
+ const combined = buildAppliedFilter(appliedFilters, agg);
9274
+ if (combined) {
9275
+ queryParamsStore.updateFilter(combined, undefined, { syncUrl });
9029
9276
  }
9030
9277
  else {
9031
9278
  clearFn();
@@ -9038,6 +9285,14 @@ function injectAggregationBase(refs) {
9038
9285
  aggregationsService,
9039
9286
  injector,
9040
9287
  destroyRef,
9288
+ isControlled,
9289
+ isQueryControlled,
9290
+ isDataControlled,
9291
+ isSelectionControlled,
9292
+ resolvedQuery,
9293
+ resolvedAggregationCount,
9294
+ controlledAggregation,
9295
+ selectionValues,
9041
9296
  debouncedSearchText,
9042
9297
  normalizedSearchText,
9043
9298
  suggests,
@@ -9048,6 +9303,7 @@ function injectAggregationBase(refs) {
9048
9303
  clearSearch,
9049
9304
  selectItems,
9050
9305
  addCurrentFiltersToItems,
9306
+ buildAppliedFilter,
9051
9307
  applyFilters,
9052
9308
  };
9053
9309
  }
@@ -9095,12 +9351,56 @@ class AggregationDateComponent {
9095
9351
  * @default true
9096
9352
  */
9097
9353
  syncUrl = input(true, ...(ngDevMode ? [{ debugName: "syncUrl", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
9354
+ /**
9355
+ * Controlled query. When set, the component's own operations run against this
9356
+ * query instead of reading the shared `QueryParamsStore`. Use to target a query
9357
+ * not tied to the current app, without mutating the shared store or the URL.
9358
+ *
9359
+ * @default undefined — read `QueryParamsStore.getQuery()` (current behavior)
9360
+ */
9361
+ controlledQuery = input(undefined, ...(ngDevMode ? [{ debugName: "controlledQuery", alias: "query" }] : [{ alias: "query" }]));
9362
+ /**
9363
+ * Controlled data. When set, the component renders this aggregation object
9364
+ * instead of reading it from the shared `AggregationsStore`.
9365
+ *
9366
+ * @default undefined — read `AggregationsStore` (current behavior)
9367
+ */
9368
+ controlledAggregation = input(undefined, ...(ngDevMode ? [{ debugName: "controlledAggregation", alias: "aggregation" }] : [{ alias: "aggregation" }]));
9369
+ /**
9370
+ * Controlled selection. When set, the date form is derived from this value
9371
+ * instead of the shared `QueryParamsStore` filters:
9372
+ * `{ kind: "option", option }` re-selects the date option whose aggregation
9373
+ * item `value` matches (the real filter value, never the `display`);
9374
+ * `{ kind: "range", from, to }` fills the custom range from the bounds.
9375
+ * The two kinds are mutually exclusive — a date selection is either a preset
9376
+ * or a custom range.
9377
+ *
9378
+ * @default undefined — derive from `QueryParamsStore` filters (current behavior)
9379
+ */
9380
+ controlledSelection = input(undefined, ...(ngDevMode ? [{ debugName: "controlledSelection", alias: "selection" }] : [{ alias: "selection" }]));
9098
9381
  title = input({ label: "Date", icon: "far fa-calendar-day" }, ...(ngDevMode ? [{ debugName: "title" }] : []));
9099
9382
  displayEmptyDistributionIntervals = input(false, ...(ngDevMode ? [{ debugName: "displayEmptyDistributionIntervals" }] : []));
9100
9383
  /* outputs */
9101
9384
  onSelect = output();
9102
9385
  onApply = output();
9103
9386
  onClear = output();
9387
+ /**
9388
+ * Emits the new selection (`{ kind: "option" }` preset or `{ kind: "range" }`
9389
+ * custom range) in controlled mode. The counterpart of the `[selection]` input,
9390
+ * emitted before `filtersChange`. Date is a radio list, so this fires **live at
9391
+ * selection time** — picking a preset radio, or confirming a date in the custom
9392
+ * range picker — not on an apply click (the custom-range radio alone, with no
9393
+ * date picked, emits nothing). Also emitted on clear. In controlled mode this is
9394
+ * the only way changes surface: nothing is written to the shared stores or the URL.
9395
+ */
9396
+ selectionChange = output();
9397
+ /**
9398
+ * Emits the selection as a structured `Filter` (the exact filter the default
9399
+ * mode would write to the store), ready for `fetchQuery({ filters })`.
9400
+ * `undefined` on clear. Controlled mode only — emitted right after
9401
+ * `selectionChange` (see its live-emission timing).
9402
+ */
9403
+ filtersChange = output();
9104
9404
  /* collapse state */
9105
9405
  isCollapsed = linkedSignal(() => this.collapsed(), ...(ngDevMode ? [{ debugName: "isCollapsed" }] : []));
9106
9406
  expanded = computed(() => (this.isCollapsed() ? null : ""), ...(ngDevMode ? [{ debugName: "expanded" }] : []));
@@ -9115,8 +9415,12 @@ class AggregationDateComponent {
9115
9415
  searchInput: this.searchInput,
9116
9416
  expanded: this.expanded,
9117
9417
  syncUrl: this.syncUrl,
9418
+ query: this.controlledQuery,
9419
+ aggregation: this.controlledAggregation,
9420
+ selection: this.controlledSelection,
9118
9421
  });
9119
9422
  /* spread from base */
9423
+ isControlled = this.base.isControlled;
9120
9424
  aggregationsService = this.base.aggregationsService;
9121
9425
  queryParamsStore = this.base.queryParamsStore;
9122
9426
  hasFilters = this.base.hasFilters;
@@ -9127,7 +9431,24 @@ class AggregationDateComponent {
9127
9431
  /* state */
9128
9432
  selection = signal(false, ...(ngDevMode ? [{ debugName: "selection" }] : []));
9129
9433
  validSelection = signal(false, ...(ngDevMode ? [{ debugName: "validSelection" }] : []));
9434
+ /**
9435
+ * Guards live emission while the form is seeded programmatically from the
9436
+ * `[selection]` input (see the seeding effect below). `form.setValue`/`patchValue`
9437
+ * emit `valueChanges` synchronously; without this flag a controlled host that
9438
+ * re-feeds `[selection]` from `selectionChange` would loop indefinitely.
9439
+ */
9440
+ seedingForm = false;
9130
9441
  aggregation = computed(() => {
9442
+ if (this.isControlled()) {
9443
+ const controlled = this.base.controlledAggregation();
9444
+ if (!controlled)
9445
+ return null;
9446
+ const agg = this.aggregationsService.enrichAggregation(controlled, []);
9447
+ return {
9448
+ ...agg,
9449
+ items: agg.items?.filter((item) => item.display !== "custom-range") ?? []
9450
+ };
9451
+ }
9131
9452
  const name = this.name();
9132
9453
  if (name !== null) {
9133
9454
  const agg = this.aggregationsService.processAggregation(name, this.column());
@@ -9163,28 +9484,75 @@ class AggregationDateComponent {
9163
9484
  this.transloco.langChanges$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((lang) => {
9164
9485
  this.lang.set(lang);
9165
9486
  });
9166
- // apply current date filter from queryParamsStore
9487
+ // apply current date filter from the selection input (controlled) or the queryParamsStore
9167
9488
  effect(() => {
9168
9489
  const agg = this.aggregation();
9169
- if (agg) {
9490
+ if (!agg)
9491
+ return;
9492
+ // seeding the form from an input must not re-emit through valueChanges
9493
+ this.seedingForm = true;
9494
+ try {
9495
+ if (this.isControlled()) {
9496
+ this.updateFormFromSelection(this.controlledSelection());
9497
+ return;
9498
+ }
9170
9499
  this.updateForm(this.queryParamsStore.getFilter({
9171
9500
  field: agg.column,
9172
9501
  name: agg.name
9173
9502
  }));
9174
9503
  }
9504
+ finally {
9505
+ this.seedingForm = false;
9506
+ }
9175
9507
  });
9176
9508
  this.form.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((changes) => {
9177
9509
  this.validSelection.set(!!changes.option &&
9178
9510
  (changes.option !== "custom-range" || changes.customRange?.from !== null || changes.customRange?.to !== null));
9511
+ // controlled date is a radio list: emit the selection live, without waiting
9512
+ // for an apply click. Presets are complete as soon as the radio is picked;
9513
+ // the custom range is emitted only once a real date is chosen (see
9514
+ // `onRangeSelected`), never on toggling the "custom-range" radio, so a stale
9515
+ // range left in the form is not surfaced.
9516
+ if (!this.seedingForm &&
9517
+ this.isControlled() &&
9518
+ changes.option &&
9519
+ changes.option !== "custom-range") {
9520
+ try {
9521
+ this.emitCurrentSelection();
9522
+ }
9523
+ catch (err) {
9524
+ warn("Error emitting date selection:", err);
9525
+ }
9526
+ }
9179
9527
  });
9180
9528
  }
9529
+ /**
9530
+ * Emits the current form selection through `selectionChange` then `filtersChange`
9531
+ * (controlled mode). Shared by `apply()` and the live emission paths so both
9532
+ * produce identical payloads. Throws if the form is not a valid selection
9533
+ * (`getFormValueFilter`) — callers decide how to surface it.
9534
+ */
9535
+ emitCurrentSelection() {
9536
+ const filter = this.getFormValueFilter();
9537
+ const option = this.form.value.option;
9538
+ // a date selection is either a preset (the item's value — the real filter
9539
+ // value, robust across days) or a custom range (its bounds) — never both
9540
+ this.selectionChange.emit(option && option !== "custom-range"
9541
+ ? { kind: "option", option: this.optionToItemValue(option) ?? option }
9542
+ : this.toRangeSelection(filter));
9543
+ this.filtersChange.emit(legacyFilterToFilter(filter));
9544
+ }
9181
9545
  select() {
9182
9546
  this.selection.set(true);
9183
9547
  }
9184
9548
  apply() {
9185
9549
  try {
9186
- const filter = this.getFormValueFilter();
9187
- this.queryParamsStore.updateFilter(filter, undefined, { syncUrl: this.syncUrl() });
9550
+ if (this.isControlled()) {
9551
+ this.emitCurrentSelection();
9552
+ }
9553
+ else {
9554
+ this.queryParamsStore.updateFilter(this.getFormValueFilter(), undefined, { syncUrl: this.syncUrl() });
9555
+ }
9188
9556
  this.selection.set(false);
9189
9557
  this.onApply.emit();
9190
9558
  }
@@ -9205,7 +9573,13 @@ class AggregationDateComponent {
9205
9573
  });
9206
9574
  const agg = this.aggregation();
9207
9575
  if (agg && notify) {
9208
- this.queryParamsStore.removeFilterByName(agg.name, undefined, { syncUrl: this.syncUrl() });
9576
+ if (this.isControlled()) {
9577
+ this.selectionChange.emit({ kind: "range" });
9578
+ this.filtersChange.emit(undefined);
9579
+ }
9580
+ else {
9581
+ this.queryParamsStore.removeFilterByName(agg.name, undefined, { syncUrl: this.syncUrl() });
9582
+ }
9209
9583
  this.onClear.emit();
9210
9584
  }
9211
9585
  }
@@ -9223,12 +9597,22 @@ class AggregationDateComponent {
9223
9597
  this.dateRangeDialog()?.open();
9224
9598
  }
9225
9599
  onRangeSelected(range) {
9226
- console.log("Selected range:", range);
9227
9600
  this.form.patchValue({
9228
9601
  option: "custom-range",
9229
9602
  customRange: { from: range.from, to: range.to }
9230
9603
  });
9231
9604
  this.select();
9605
+ // a real date was picked in the picker: emit the custom range live in
9606
+ // controlled mode (the only place a custom range is emitted — never on
9607
+ // toggling the "custom-range" radio)
9608
+ if (this.isControlled()) {
9609
+ try {
9610
+ this.emitCurrentSelection();
9611
+ }
9612
+ catch (err) {
9613
+ warn("Error emitting date selection:", err);
9614
+ }
9615
+ }
9232
9616
  }
9233
9617
  updateForm(filter) {
9234
9618
  if (!filter) {
@@ -9268,6 +9652,90 @@ class AggregationDateComponent {
9268
9652
  };
9269
9653
  this.form.setValue(formValue);
9270
9654
  }
9655
+ /**
9656
+ * Derives the form from a controlled selection.
9657
+ * `{ kind: "option" }` re-selects the date option whose item `value` matches —
9658
+ * no operator/value matching, so presets whose bounds are computed relative to
9659
+ * today survive a round-trip across days; a value unknown to the aggregation
9660
+ * resets the form. `{ kind: "range" }` fills the custom range from the bounds.
9661
+ */
9662
+ updateFormFromSelection(sel) {
9663
+ if (sel?.kind === "option") {
9664
+ const code = this.itemValueToOption(sel.option);
9665
+ if (code && code !== "custom-range") {
9666
+ this.form.setValue({ option: code, customRange: { from: null, to: null } });
9667
+ }
9668
+ else {
9669
+ this.clear(false);
9670
+ }
9671
+ return;
9672
+ }
9673
+ this.updateForm(this.rangeToLegacyFilter(sel));
9674
+ }
9675
+ /**
9676
+ * The date option codes (what `form.option` holds) are the items' `display`
9677
+ * with a fallback on their `value` (see `translateAggregationToDateOptions`).
9678
+ * The controlled selection persists the item `value` — the real filter value —
9679
+ * so these two helpers convert between the option code and the item value.
9680
+ */
9681
+ optionToItemValue(option) {
9682
+ const item = this.items().find((it) => (it.display || String(it.value)) === option);
9683
+ return item ? String(item.value) : undefined;
9684
+ }
9685
+ itemValueToOption(value) {
9686
+ const item = this.items().find((it) => String(it.value) === value);
9687
+ return item ? item.display || String(item.value) : undefined;
9688
+ }
9689
+ /**
9690
+ * Converts a controlled `{ kind: "range" }` selection to the `LegacyFilter`
9691
+ * shape `updateForm` expects (which only reads `operator`/`value`/`filters`).
9692
+ * Returns `undefined` for an empty or non-range selection, which resets the form.
9693
+ */
9694
+ rangeToLegacyFilter(sel) {
9695
+ if (!sel || sel.kind !== "range")
9696
+ return undefined;
9697
+ const { from, to } = sel;
9698
+ if (from && to) {
9699
+ return {
9700
+ operator: "and",
9701
+ filters: [
9702
+ { operator: "gte", value: from },
9703
+ { operator: "lte", value: to }
9704
+ ]
9705
+ };
9706
+ }
9707
+ if (from)
9708
+ return { operator: "gte", value: from };
9709
+ if (to)
9710
+ return { operator: "lte", value: to };
9711
+ return undefined;
9712
+ }
9713
+ /**
9714
+ * Converts the applied `LegacyFilter` to a `{ kind: "range" }` selection
9715
+ * (the shape emitted through `selectionChange` in controlled mode).
9716
+ * Mirrors the operator handling of `updateForm`. Presets whose operator is not
9717
+ * a range operator (e.g. `eq`) collapse to `from = to = value`.
9718
+ */
9719
+ toRangeSelection(filter) {
9720
+ switch (filter.operator) {
9721
+ case "gte":
9722
+ return { kind: "range", from: filter.value };
9723
+ case "lte":
9724
+ return { kind: "range", to: filter.value };
9725
+ case "between":
9726
+ return { kind: "range", from: filter.start, to: filter.end };
9727
+ case "and": {
9728
+ const filters = filter.filters ?? [];
9729
+ return {
9730
+ kind: "range",
9731
+ from: filters.find((f) => f.operator === "gte")?.value,
9732
+ to: filters.find((f) => f.operator === "lte")?.value
9733
+ };
9734
+ }
9735
+ default:
9736
+ return { kind: "range", from: filter.value, to: filter.value };
9737
+ }
9738
+ }
9271
9739
  toEndOfDay(dateStr) {
9272
9740
  return `${new Date(dateStr).toLocaleDateString("en-CA", options)} 23:59:59`;
9273
9741
  }
@@ -9343,7 +9811,7 @@ class AggregationDateComponent {
9343
9811
  throw new Error("filters.filterInvalid");
9344
9812
  }
9345
9813
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: AggregationDateComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
9346
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: AggregationDateComponent, isStandalone: true, selector: "aggregation-date, AggregationDate, aggregationdate", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, column: { classPropertyName: "column", publicName: "column", isSignal: true, isRequired: true, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, collapsible: { classPropertyName: "collapsible", publicName: "collapsible", isSignal: true, isRequired: false, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, searchable: { classPropertyName: "searchable", publicName: "searchable", isSignal: true, isRequired: false, transformFunction: null }, showFiltersCount: { classPropertyName: "showFiltersCount", publicName: "showFiltersCount", isSignal: true, isRequired: false, transformFunction: null }, syncUrl: { classPropertyName: "syncUrl", publicName: "syncUrl", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, displayEmptyDistributionIntervals: { classPropertyName: "displayEmptyDistributionIntervals", publicName: "displayEmptyDistributionIntervals", isSignal: true, isRequired: false, transformFunction: null }, searchText: { classPropertyName: "searchText", publicName: "searchText", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSelect: "onSelect", onApply: "onApply", onClear: "onClear", searchText: "searchTextChange" }, host: { classAttribute: "@container" }, providers: [provideTranslocoScope("filters")], viewQueries: [{ propertyName: "dateRangeDialog", first: true, predicate: AggregationDateRangeDialogComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<details [attr.open]=\"expanded()\" [attr.name]=\"id()\" class=\"group space-y-2\">\r\n <summary\r\n [class.cursor-pointer]=\"collapsible()\"\r\n class=\"m-0 flex h-8 w-full items-center pl-1 font-semibold select-none\"\r\n (click)=\"onHeaderClick($event)\">\r\n <ng-content select=\"label\">\r\n @let icon = aggregation()?.icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\r\n }\r\n <span class=\"grow\">{{\r\n aggregation()?.display | syslang | transloco\r\n }}</span>\r\n </ng-content>\r\n\r\n @if (hasFilters()) {\r\n <icon-button\r\n [attr.title]=\"'filters.clearFilters' | transloco\"\r\n [attr.aria-label]=\"'filters.clearFilters' | transloco\"\r\n (click)=\"clear()\"\r\n (keydown.enter)=\"clear()\">\r\n <filter-x-icon />\r\n <span class=\"sr-only\">{{ \"filters.clearFilters\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n\r\n @if (selection() && validSelection()) {\r\n <icon-button\r\n [attr.title]=\"'filters.applyFilters' | transloco\"\r\n [attr.aria-label]=\"'filters.applyFilters' | transloco\"\r\n (click)=\"apply()\"\r\n (keydown.enter)=\"apply()\">\r\n <filter-icon />\r\n <span class=\"sr-only\">{{ \"filters.applyFilters\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n @if (collapsible()) {\r\n <icon-button\r\n title=\"Open/Close\"\r\n class=\"cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\r\n <chevronright />\r\n </icon-button>\r\n }\r\n </summary>\r\n\r\n <!-- content wrapper -->\r\n <form [formGroup]=\"form\">\r\n <ul\r\n class=\"scrollbar-thin flex max-h-[var(--scroll-height,20rem)] snap-y snap-start flex-col gap-1 overflow-auto pt-2\"\r\n role=\"list\">\r\n @for (option of dateOptions(); track $index) {\r\n <li\r\n role=\"listitem\"\r\n tabindex=\"0\"\r\n (click)=\"radio.click()\"\r\n [attr.aria-label]=\"option.display | syslang | transloco\"\r\n [class]=\"\r\n cn(\r\n 'flex p-0 px-2 leading-7',\r\n form.get('option')?.value === option.display && 'bg-accent',\r\n option.hidden && 'hidden',\r\n option.disabled && 'disabled pointer-events-none text-neutral-300'\r\n )\r\n \"\r\n [attr.aria-hidden]=\"option.disabled\">\r\n <input\r\n #radio\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-{{ option.display }}\"\r\n [attr.disabled]=\"option.disabled ? true : null\"\r\n [attr.aria-disabled]=\"option.disabled\"\r\n (click)=\"select()\"\r\n value=\"{{ option.display }}\" />\r\n\r\n <label\r\n for=\"date-filter-{{ option.display }}\"\r\n class=\"grow cursor-pointer p-1\">\r\n {{ option.display | syslang | transloco }}\r\n </label>\r\n </li>\r\n }\r\n\r\n @if (allowCustomRange) {\r\n <li\r\n role=\"listitem\"\r\n aria-label=\"open date range picker\"\r\n class=\"flex px-2 leading-7\"\r\n [class.select]=\"form.get('option')?.value === 'custom-range'\">\r\n <input\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-range-dialog\"\r\n value=\"custom-range\"\r\n (click)=\"select()\" />\r\n <div\r\n class=\"@container flex grow justify-end gap-1 p-1 @max-[340px]:flex-wrap\">\r\n <div class=\"flex gap-1\">\r\n <label for=\"datepicker-range-start\" class=\"min-w-10 truncate\">{{\r\n \"filters.from\" | transloco\r\n }}</label>\r\n <input\r\n id=\"datepicker-range-start\"\r\n name=\"start\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeFrom()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n <div class=\"flex gap-1\">\r\n <label\r\n for=\"datepicker-range-end\"\r\n class=\"min-w-10 truncate text-right\"\r\n >{{ \"filters.to\" | transloco }}</label\r\n >\r\n <input\r\n id=\"datepicker-range-end\"\r\n name=\"end\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeTo()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n </div>\r\n </li>\r\n }\r\n </ul>\r\n </form>\r\n</details>\r\n\r\n<aggregation-date-range-dialog\r\n [lang]=\"lang()\"\r\n [useDateRange]=\"false\"\r\n [min]=\"form.get('customRange.from')?.value || undefined\"\r\n [max]=\"form.get('customRange.to')?.value || undefined\"\r\n (rangeSelected)=\"onRangeSelected($event)\" />\r\n", styles: [":host{display:block;min-width:200px}ul[role=list]{scrollbar-width:thin}\n"], dependencies: [{ kind: "directive", type: InputComponent, selector: "input[type=\"text\"], input[type=\"email\"], input[type=\"number\"], input[type=\"password\"], input[type=\"tel\"], input[type=\"url\"], input[type=\"time\"], input[type=\"file\"]", inputs: ["class", "variant", "decoration"] }, { kind: "directive", type: ListItemComponent, selector: "[role=\"listitem\"], [role=\"option\"]", inputs: ["class", "variant", "decoration"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.RadioControlValueAccessor, selector: "input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: ChevronRightIcon, selector: "chevron-right, ChevronRight, chevronright, ChevronRightIcon, chevron-right-icon, chevronrighticon", inputs: ["class"] }, { kind: "component", type: AggregationDateRangeDialogComponent, selector: "aggregation-date-range-dialog", inputs: ["min", "max", "lang", "useDateRange"], outputs: ["rangeSelected"] }, { kind: "component", type: FaIconComponent, selector: "fa-icon, FaIcon", inputs: ["faClass", "class"] }, { kind: "component", type: FilterIcon, selector: "filter-icon, FilterIcon", inputs: ["class"] }, { kind: "component", type: FilterXIcon, selector: "filter-x-icon, FilterXIcon", inputs: ["class"] }, { kind: "directive", type: IconButtonComponent, selector: "button[icon-button], icon-button, IconButton", inputs: ["class", "size"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }, { kind: "pipe", type: SyslangPipe, name: "syslang" }] });
9814
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: AggregationDateComponent, isStandalone: true, selector: "aggregation-date, AggregationDate, aggregationdate", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, column: { classPropertyName: "column", publicName: "column", isSignal: true, isRequired: true, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, collapsible: { classPropertyName: "collapsible", publicName: "collapsible", isSignal: true, isRequired: false, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, searchable: { classPropertyName: "searchable", publicName: "searchable", isSignal: true, isRequired: false, transformFunction: null }, showFiltersCount: { classPropertyName: "showFiltersCount", publicName: "showFiltersCount", isSignal: true, isRequired: false, transformFunction: null }, syncUrl: { classPropertyName: "syncUrl", publicName: "syncUrl", isSignal: true, isRequired: false, transformFunction: null }, controlledQuery: { classPropertyName: "controlledQuery", publicName: "query", isSignal: true, isRequired: false, transformFunction: null }, controlledAggregation: { classPropertyName: "controlledAggregation", publicName: "aggregation", isSignal: true, isRequired: false, transformFunction: null }, controlledSelection: { classPropertyName: "controlledSelection", publicName: "selection", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, displayEmptyDistributionIntervals: { classPropertyName: "displayEmptyDistributionIntervals", publicName: "displayEmptyDistributionIntervals", isSignal: true, isRequired: false, transformFunction: null }, searchText: { classPropertyName: "searchText", publicName: "searchText", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSelect: "onSelect", onApply: "onApply", onClear: "onClear", selectionChange: "selectionChange", filtersChange: "filtersChange", searchText: "searchTextChange" }, host: { classAttribute: "@container" }, providers: [provideTranslocoScope("filters")], viewQueries: [{ propertyName: "dateRangeDialog", first: true, predicate: AggregationDateRangeDialogComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<details [attr.open]=\"expanded()\" [attr.name]=\"id()\" class=\"group space-y-2\">\r\n <summary\r\n [class.cursor-pointer]=\"collapsible()\"\r\n class=\"m-0 flex h-8 w-full items-center pl-1 font-semibold select-none\"\r\n (click)=\"onHeaderClick($event)\">\r\n <ng-content select=\"label\">\r\n @let icon = aggregation()?.icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\r\n }\r\n <span class=\"grow\">{{\r\n aggregation()?.display | syslang | transloco\r\n }}</span>\r\n </ng-content>\r\n\r\n @if (hasFilters()) {\r\n <icon-button\r\n [attr.title]=\"'filters.clearFilters' | transloco\"\r\n [attr.aria-label]=\"'filters.clearFilters' | transloco\"\r\n (click)=\"clear()\"\r\n (keydown.enter)=\"clear()\">\r\n <filter-x-icon />\r\n <span class=\"sr-only\">{{ \"filters.clearFilters\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n\r\n @if (!isControlled() && selection() && validSelection()) {\r\n <icon-button\r\n [attr.title]=\"'filters.applyFilters' | transloco\"\r\n [attr.aria-label]=\"'filters.applyFilters' | transloco\"\r\n (click)=\"apply()\"\r\n (keydown.enter)=\"apply()\">\r\n <filter-icon />\r\n <span class=\"sr-only\">{{ \"filters.applyFilters\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n @if (collapsible()) {\r\n <icon-button\r\n title=\"Open/Close\"\r\n class=\"cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\r\n <chevronright />\r\n </icon-button>\r\n }\r\n </summary>\r\n\r\n <!-- content wrapper -->\r\n <form [formGroup]=\"form\">\r\n <ul\r\n class=\"flex max-h-[var(--scroll-height,20rem)] snap-y snap-start scrollbar-thin flex-col gap-1 overflow-auto pt-2\"\r\n role=\"list\">\r\n @for (option of dateOptions(); track $index) {\r\n <li\r\n role=\"listitem\"\r\n tabindex=\"0\"\r\n (click)=\"radio.click()\"\r\n [attr.aria-label]=\"option.display | syslang | transloco\"\r\n [class]=\"\r\n cn(\r\n 'flex p-0 px-2 leading-7',\r\n form.get('option')?.value === option.display && 'bg-accent',\r\n option.hidden && 'hidden',\r\n option.disabled && 'disabled pointer-events-none text-neutral-300'\r\n )\r\n \"\r\n [attr.aria-hidden]=\"option.disabled\">\r\n <input\r\n #radio\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-{{ option.display }}\"\r\n [attr.disabled]=\"option.disabled ? true : null\"\r\n [attr.aria-disabled]=\"option.disabled\"\r\n (click)=\"select()\"\r\n value=\"{{ option.display }}\" />\r\n\r\n <label\r\n for=\"date-filter-{{ option.display }}\"\r\n class=\"grow cursor-pointer p-1\">\r\n {{ option.display | syslang | transloco }}\r\n </label>\r\n </li>\r\n }\r\n\r\n @if (allowCustomRange) {\r\n <li\r\n role=\"listitem\"\r\n aria-label=\"open date range picker\"\r\n class=\"flex px-2 leading-7\"\r\n [class.select]=\"form.get('option')?.value === 'custom-range'\">\r\n <input\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-range-dialog\"\r\n value=\"custom-range\"\r\n (click)=\"select()\" />\r\n <div\r\n class=\"@container flex grow justify-end gap-1 p-1 @max-[340px]:flex-wrap\">\r\n <div class=\"flex gap-1\">\r\n <label for=\"datepicker-range-start\" class=\"min-w-10 truncate\">{{\r\n \"filters.from\" | transloco\r\n }}</label>\r\n <input\r\n id=\"datepicker-range-start\"\r\n name=\"start\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeFrom()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n <div class=\"flex gap-1\">\r\n <label\r\n for=\"datepicker-range-end\"\r\n class=\"min-w-10 truncate text-right\"\r\n >{{ \"filters.to\" | transloco }}</label\r\n >\r\n <input\r\n id=\"datepicker-range-end\"\r\n name=\"end\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeTo()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n </div>\r\n </li>\r\n }\r\n </ul>\r\n </form>\r\n</details>\r\n\r\n<aggregation-date-range-dialog\r\n [lang]=\"lang()\"\r\n [useDateRange]=\"false\"\r\n [min]=\"form.get('customRange.from')?.value || undefined\"\r\n [max]=\"form.get('customRange.to')?.value || undefined\"\r\n (rangeSelected)=\"onRangeSelected($event)\" />\r\n", styles: [":host{display:block;min-width:200px}ul[role=list]{scrollbar-width:thin}\n"], dependencies: [{ kind: "directive", type: InputComponent, selector: "input[type=\"text\"], input[type=\"email\"], input[type=\"number\"], input[type=\"password\"], input[type=\"tel\"], input[type=\"url\"], input[type=\"time\"], input[type=\"file\"]", inputs: ["class", "variant", "decoration"] }, { kind: "directive", type: ListItemComponent, selector: "[role=\"listitem\"], [role=\"option\"]", inputs: ["class", "variant", "decoration"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.RadioControlValueAccessor, selector: "input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: ChevronRightIcon, selector: "chevron-right, ChevronRight, chevronright, ChevronRightIcon, chevron-right-icon, chevronrighticon", inputs: ["class"] }, { kind: "component", type: AggregationDateRangeDialogComponent, selector: "aggregation-date-range-dialog", inputs: ["min", "max", "lang", "useDateRange"], outputs: ["rangeSelected"] }, { kind: "component", type: FaIconComponent, selector: "fa-icon, FaIcon", inputs: ["faClass", "class"] }, { kind: "component", type: FilterIcon, selector: "filter-icon, FilterIcon", inputs: ["class"] }, { kind: "component", type: FilterXIcon, selector: "filter-x-icon, FilterXIcon", inputs: ["class"] }, { kind: "directive", type: IconButtonComponent, selector: "button[icon-button], icon-button, IconButton", inputs: ["class", "size"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }, { kind: "pipe", type: SyslangPipe, name: "syslang" }] });
9347
9815
  }
9348
9816
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: AggregationDateComponent, decorators: [{
9349
9817
  type: Component,
@@ -9361,8 +9829,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
9361
9829
  IconButtonComponent
9362
9830
  ], host: {
9363
9831
  class: "@container"
9364
- }, template: "<details [attr.open]=\"expanded()\" [attr.name]=\"id()\" class=\"group space-y-2\">\r\n <summary\r\n [class.cursor-pointer]=\"collapsible()\"\r\n class=\"m-0 flex h-8 w-full items-center pl-1 font-semibold select-none\"\r\n (click)=\"onHeaderClick($event)\">\r\n <ng-content select=\"label\">\r\n @let icon = aggregation()?.icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\r\n }\r\n <span class=\"grow\">{{\r\n aggregation()?.display | syslang | transloco\r\n }}</span>\r\n </ng-content>\r\n\r\n @if (hasFilters()) {\r\n <icon-button\r\n [attr.title]=\"'filters.clearFilters' | transloco\"\r\n [attr.aria-label]=\"'filters.clearFilters' | transloco\"\r\n (click)=\"clear()\"\r\n (keydown.enter)=\"clear()\">\r\n <filter-x-icon />\r\n <span class=\"sr-only\">{{ \"filters.clearFilters\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n\r\n @if (selection() && validSelection()) {\r\n <icon-button\r\n [attr.title]=\"'filters.applyFilters' | transloco\"\r\n [attr.aria-label]=\"'filters.applyFilters' | transloco\"\r\n (click)=\"apply()\"\r\n (keydown.enter)=\"apply()\">\r\n <filter-icon />\r\n <span class=\"sr-only\">{{ \"filters.applyFilters\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n @if (collapsible()) {\r\n <icon-button\r\n title=\"Open/Close\"\r\n class=\"cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\r\n <chevronright />\r\n </icon-button>\r\n }\r\n </summary>\r\n\r\n <!-- content wrapper -->\r\n <form [formGroup]=\"form\">\r\n <ul\r\n class=\"scrollbar-thin flex max-h-[var(--scroll-height,20rem)] snap-y snap-start flex-col gap-1 overflow-auto pt-2\"\r\n role=\"list\">\r\n @for (option of dateOptions(); track $index) {\r\n <li\r\n role=\"listitem\"\r\n tabindex=\"0\"\r\n (click)=\"radio.click()\"\r\n [attr.aria-label]=\"option.display | syslang | transloco\"\r\n [class]=\"\r\n cn(\r\n 'flex p-0 px-2 leading-7',\r\n form.get('option')?.value === option.display && 'bg-accent',\r\n option.hidden && 'hidden',\r\n option.disabled && 'disabled pointer-events-none text-neutral-300'\r\n )\r\n \"\r\n [attr.aria-hidden]=\"option.disabled\">\r\n <input\r\n #radio\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-{{ option.display }}\"\r\n [attr.disabled]=\"option.disabled ? true : null\"\r\n [attr.aria-disabled]=\"option.disabled\"\r\n (click)=\"select()\"\r\n value=\"{{ option.display }}\" />\r\n\r\n <label\r\n for=\"date-filter-{{ option.display }}\"\r\n class=\"grow cursor-pointer p-1\">\r\n {{ option.display | syslang | transloco }}\r\n </label>\r\n </li>\r\n }\r\n\r\n @if (allowCustomRange) {\r\n <li\r\n role=\"listitem\"\r\n aria-label=\"open date range picker\"\r\n class=\"flex px-2 leading-7\"\r\n [class.select]=\"form.get('option')?.value === 'custom-range'\">\r\n <input\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-range-dialog\"\r\n value=\"custom-range\"\r\n (click)=\"select()\" />\r\n <div\r\n class=\"@container flex grow justify-end gap-1 p-1 @max-[340px]:flex-wrap\">\r\n <div class=\"flex gap-1\">\r\n <label for=\"datepicker-range-start\" class=\"min-w-10 truncate\">{{\r\n \"filters.from\" | transloco\r\n }}</label>\r\n <input\r\n id=\"datepicker-range-start\"\r\n name=\"start\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeFrom()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n <div class=\"flex gap-1\">\r\n <label\r\n for=\"datepicker-range-end\"\r\n class=\"min-w-10 truncate text-right\"\r\n >{{ \"filters.to\" | transloco }}</label\r\n >\r\n <input\r\n id=\"datepicker-range-end\"\r\n name=\"end\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeTo()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n </div>\r\n </li>\r\n }\r\n </ul>\r\n </form>\r\n</details>\r\n\r\n<aggregation-date-range-dialog\r\n [lang]=\"lang()\"\r\n [useDateRange]=\"false\"\r\n [min]=\"form.get('customRange.from')?.value || undefined\"\r\n [max]=\"form.get('customRange.to')?.value || undefined\"\r\n (rangeSelected)=\"onRangeSelected($event)\" />\r\n", styles: [":host{display:block;min-width:200px}ul[role=list]{scrollbar-width:thin}\n"] }]
9365
- }], ctorParameters: () => [], propDecorators: { dateRangeDialog: [{ type: i0.ViewChild, args: [i0.forwardRef(() => AggregationDateRangeDialogComponent), { isSignal: true }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], column: [{ type: i0.Input, args: [{ isSignal: true, alias: "column", required: true }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], collapsible: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsible", required: false }] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], showFiltersCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFiltersCount", required: false }] }], syncUrl: [{ type: i0.Input, args: [{ isSignal: true, alias: "syncUrl", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], displayEmptyDistributionIntervals: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayEmptyDistributionIntervals", required: false }] }], onSelect: [{ type: i0.Output, args: ["onSelect"] }], onApply: [{ type: i0.Output, args: ["onApply"] }], onClear: [{ type: i0.Output, args: ["onClear"] }], searchText: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchText", required: false }] }, { type: i0.Output, args: ["searchTextChange"] }] } });
9832
+ }, template: "<details [attr.open]=\"expanded()\" [attr.name]=\"id()\" class=\"group space-y-2\">\r\n <summary\r\n [class.cursor-pointer]=\"collapsible()\"\r\n class=\"m-0 flex h-8 w-full items-center pl-1 font-semibold select-none\"\r\n (click)=\"onHeaderClick($event)\">\r\n <ng-content select=\"label\">\r\n @let icon = aggregation()?.icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\r\n }\r\n <span class=\"grow\">{{\r\n aggregation()?.display | syslang | transloco\r\n }}</span>\r\n </ng-content>\r\n\r\n @if (hasFilters()) {\r\n <icon-button\r\n [attr.title]=\"'filters.clearFilters' | transloco\"\r\n [attr.aria-label]=\"'filters.clearFilters' | transloco\"\r\n (click)=\"clear()\"\r\n (keydown.enter)=\"clear()\">\r\n <filter-x-icon />\r\n <span class=\"sr-only\">{{ \"filters.clearFilters\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n\r\n @if (!isControlled() && selection() && validSelection()) {\r\n <icon-button\r\n [attr.title]=\"'filters.applyFilters' | transloco\"\r\n [attr.aria-label]=\"'filters.applyFilters' | transloco\"\r\n (click)=\"apply()\"\r\n (keydown.enter)=\"apply()\">\r\n <filter-icon />\r\n <span class=\"sr-only\">{{ \"filters.applyFilters\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n @if (collapsible()) {\r\n <icon-button\r\n title=\"Open/Close\"\r\n class=\"cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\r\n <chevronright />\r\n </icon-button>\r\n }\r\n </summary>\r\n\r\n <!-- content wrapper -->\r\n <form [formGroup]=\"form\">\r\n <ul\r\n class=\"flex max-h-[var(--scroll-height,20rem)] snap-y snap-start scrollbar-thin flex-col gap-1 overflow-auto pt-2\"\r\n role=\"list\">\r\n @for (option of dateOptions(); track $index) {\r\n <li\r\n role=\"listitem\"\r\n tabindex=\"0\"\r\n (click)=\"radio.click()\"\r\n [attr.aria-label]=\"option.display | syslang | transloco\"\r\n [class]=\"\r\n cn(\r\n 'flex p-0 px-2 leading-7',\r\n form.get('option')?.value === option.display && 'bg-accent',\r\n option.hidden && 'hidden',\r\n option.disabled && 'disabled pointer-events-none text-neutral-300'\r\n )\r\n \"\r\n [attr.aria-hidden]=\"option.disabled\">\r\n <input\r\n #radio\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-{{ option.display }}\"\r\n [attr.disabled]=\"option.disabled ? true : null\"\r\n [attr.aria-disabled]=\"option.disabled\"\r\n (click)=\"select()\"\r\n value=\"{{ option.display }}\" />\r\n\r\n <label\r\n for=\"date-filter-{{ option.display }}\"\r\n class=\"grow cursor-pointer p-1\">\r\n {{ option.display | syslang | transloco }}\r\n </label>\r\n </li>\r\n }\r\n\r\n @if (allowCustomRange) {\r\n <li\r\n role=\"listitem\"\r\n aria-label=\"open date range picker\"\r\n class=\"flex px-2 leading-7\"\r\n [class.select]=\"form.get('option')?.value === 'custom-range'\">\r\n <input\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-range-dialog\"\r\n value=\"custom-range\"\r\n (click)=\"select()\" />\r\n <div\r\n class=\"@container flex grow justify-end gap-1 p-1 @max-[340px]:flex-wrap\">\r\n <div class=\"flex gap-1\">\r\n <label for=\"datepicker-range-start\" class=\"min-w-10 truncate\">{{\r\n \"filters.from\" | transloco\r\n }}</label>\r\n <input\r\n id=\"datepicker-range-start\"\r\n name=\"start\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeFrom()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n <div class=\"flex gap-1\">\r\n <label\r\n for=\"datepicker-range-end\"\r\n class=\"min-w-10 truncate text-right\"\r\n >{{ \"filters.to\" | transloco }}</label\r\n >\r\n <input\r\n id=\"datepicker-range-end\"\r\n name=\"end\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeTo()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n </div>\r\n </li>\r\n }\r\n </ul>\r\n </form>\r\n</details>\r\n\r\n<aggregation-date-range-dialog\r\n [lang]=\"lang()\"\r\n [useDateRange]=\"false\"\r\n [min]=\"form.get('customRange.from')?.value || undefined\"\r\n [max]=\"form.get('customRange.to')?.value || undefined\"\r\n (rangeSelected)=\"onRangeSelected($event)\" />\r\n", styles: [":host{display:block;min-width:200px}ul[role=list]{scrollbar-width:thin}\n"] }]
9833
+ }], ctorParameters: () => [], propDecorators: { dateRangeDialog: [{ type: i0.ViewChild, args: [i0.forwardRef(() => AggregationDateRangeDialogComponent), { isSignal: true }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], column: [{ type: i0.Input, args: [{ isSignal: true, alias: "column", required: true }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], collapsible: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsible", required: false }] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], showFiltersCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFiltersCount", required: false }] }], syncUrl: [{ type: i0.Input, args: [{ isSignal: true, alias: "syncUrl", required: false }] }], controlledQuery: [{ type: i0.Input, args: [{ isSignal: true, alias: "query", required: false }] }], controlledAggregation: [{ type: i0.Input, args: [{ isSignal: true, alias: "aggregation", required: false }] }], controlledSelection: [{ type: i0.Input, args: [{ isSignal: true, alias: "selection", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], displayEmptyDistributionIntervals: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayEmptyDistributionIntervals", required: false }] }], onSelect: [{ type: i0.Output, args: ["onSelect"] }], onApply: [{ type: i0.Output, args: ["onApply"] }], onClear: [{ type: i0.Output, args: ["onClear"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], filtersChange: [{ type: i0.Output, args: ["filtersChange"] }], searchText: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchText", required: false }] }, { type: i0.Output, args: ["searchTextChange"] }] } });
9366
9834
 
9367
9835
  /**
9368
9836
  * Component that allows users to select a date or a date range for filtering search results.
@@ -9371,7 +9839,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
9371
9839
  */
9372
9840
  class DateComponent extends AggregationDateComponent {
9373
9841
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: DateComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
9374
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: DateComponent, isStandalone: true, selector: "date-filter,DateFilter", host: { classAttribute: "@container" }, providers: [provideTranslocoScope("filters")], usesInheritance: true, ngImport: i0, template: "<details [attr.open]=\"expanded()\" [attr.name]=\"id()\" class=\"group space-y-2\">\r\n <summary\r\n [class.cursor-pointer]=\"collapsible()\"\r\n class=\"m-0 flex h-8 w-full items-center pl-1 font-semibold select-none\"\r\n (click)=\"onHeaderClick($event)\">\r\n <ng-content select=\"label\">\r\n @let icon = aggregation()?.icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\r\n }\r\n <span class=\"grow\">{{\r\n aggregation()?.display | syslang | transloco\r\n }}</span>\r\n </ng-content>\r\n\r\n @if (hasFilters()) {\r\n <icon-button\r\n [attr.title]=\"'filters.clearFilters' | transloco\"\r\n [attr.aria-label]=\"'filters.clearFilters' | transloco\"\r\n (click)=\"clear()\"\r\n (keydown.enter)=\"clear()\">\r\n <filter-x-icon />\r\n <span class=\"sr-only\">{{ \"filters.clearFilters\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n\r\n @if (selection() && validSelection()) {\r\n <icon-button\r\n [attr.title]=\"'filters.applyFilters' | transloco\"\r\n [attr.aria-label]=\"'filters.applyFilters' | transloco\"\r\n (click)=\"apply()\"\r\n (keydown.enter)=\"apply()\">\r\n <filter-icon />\r\n <span class=\"sr-only\">{{ \"filters.applyFilters\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n @if (collapsible()) {\r\n <icon-button\r\n title=\"Open/Close\"\r\n class=\"cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\r\n <chevronright />\r\n </icon-button>\r\n }\r\n </summary>\r\n\r\n <!-- content wrapper -->\r\n <form [formGroup]=\"form\">\r\n <ul\r\n class=\"scrollbar-thin flex max-h-[var(--scroll-height,20rem)] snap-y snap-start flex-col gap-1 overflow-auto pt-2\"\r\n role=\"list\">\r\n @for (option of dateOptions(); track $index) {\r\n <li\r\n role=\"listitem\"\r\n tabindex=\"0\"\r\n (click)=\"radio.click()\"\r\n [attr.aria-label]=\"option.display | syslang | transloco\"\r\n [class]=\"\r\n cn(\r\n 'flex p-0 px-2 leading-7',\r\n form.get('option')?.value === option.display && 'bg-accent',\r\n option.hidden && 'hidden',\r\n option.disabled && 'disabled pointer-events-none text-neutral-300'\r\n )\r\n \"\r\n [attr.aria-hidden]=\"option.disabled\">\r\n <input\r\n #radio\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-{{ option.display }}\"\r\n [attr.disabled]=\"option.disabled ? true : null\"\r\n [attr.aria-disabled]=\"option.disabled\"\r\n (click)=\"select()\"\r\n value=\"{{ option.display }}\" />\r\n\r\n <label\r\n for=\"date-filter-{{ option.display }}\"\r\n class=\"grow cursor-pointer p-1\">\r\n {{ option.display | syslang | transloco }}\r\n </label>\r\n </li>\r\n }\r\n\r\n @if (allowCustomRange) {\r\n <li\r\n role=\"listitem\"\r\n aria-label=\"open date range picker\"\r\n class=\"flex px-2 leading-7\"\r\n [class.select]=\"form.get('option')?.value === 'custom-range'\">\r\n <input\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-range-dialog\"\r\n value=\"custom-range\"\r\n (click)=\"select()\" />\r\n <div\r\n class=\"@container flex grow justify-end gap-1 p-1 @max-[340px]:flex-wrap\">\r\n <div class=\"flex gap-1\">\r\n <label for=\"datepicker-range-start\" class=\"min-w-10 truncate\">{{\r\n \"filters.from\" | transloco\r\n }}</label>\r\n <input\r\n id=\"datepicker-range-start\"\r\n name=\"start\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeFrom()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n <div class=\"flex gap-1\">\r\n <label\r\n for=\"datepicker-range-end\"\r\n class=\"min-w-10 truncate text-right\"\r\n >{{ \"filters.to\" | transloco }}</label\r\n >\r\n <input\r\n id=\"datepicker-range-end\"\r\n name=\"end\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeTo()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n </div>\r\n </li>\r\n }\r\n </ul>\r\n </form>\r\n</details>\r\n\r\n<aggregation-date-range-dialog\r\n [lang]=\"lang()\"\r\n [useDateRange]=\"false\"\r\n [min]=\"form.get('customRange.from')?.value || undefined\"\r\n [max]=\"form.get('customRange.to')?.value || undefined\"\r\n (rangeSelected)=\"onRangeSelected($event)\" />\r\n", styles: [":host{display:block;min-width:200px}ul[role=list]{scrollbar-width:thin}\n"], dependencies: [{ kind: "directive", type: IconButtonComponent, selector: "button[icon-button], icon-button, IconButton", inputs: ["class", "size"] }, { kind: "directive", type: ListItemComponent, selector: "[role=\"listitem\"], [role=\"option\"]", inputs: ["class", "variant", "decoration"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.RadioControlValueAccessor, selector: "input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: ChevronRightIcon, selector: "chevron-right, ChevronRight, chevronright, ChevronRightIcon, chevron-right-icon, chevronrighticon", inputs: ["class"] }, { kind: "component", type: AggregationDateRangeDialogComponent, selector: "aggregation-date-range-dialog", inputs: ["min", "max", "lang", "useDateRange"], outputs: ["rangeSelected"] }, { kind: "component", type: FaIconComponent, selector: "fa-icon, FaIcon", inputs: ["faClass", "class"] }, { kind: "component", type: FilterIcon, selector: "filter-icon, FilterIcon", inputs: ["class"] }, { kind: "component", type: FilterXIcon, selector: "filter-x-icon, FilterXIcon", inputs: ["class"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }, { kind: "pipe", type: SyslangPipe, name: "syslang" }] });
9842
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: DateComponent, isStandalone: true, selector: "date-filter,DateFilter", host: { classAttribute: "@container" }, providers: [provideTranslocoScope("filters")], usesInheritance: true, ngImport: i0, template: "<details [attr.open]=\"expanded()\" [attr.name]=\"id()\" class=\"group space-y-2\">\r\n <summary\r\n [class.cursor-pointer]=\"collapsible()\"\r\n class=\"m-0 flex h-8 w-full items-center pl-1 font-semibold select-none\"\r\n (click)=\"onHeaderClick($event)\">\r\n <ng-content select=\"label\">\r\n @let icon = aggregation()?.icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\r\n }\r\n <span class=\"grow\">{{\r\n aggregation()?.display | syslang | transloco\r\n }}</span>\r\n </ng-content>\r\n\r\n @if (hasFilters()) {\r\n <icon-button\r\n [attr.title]=\"'filters.clearFilters' | transloco\"\r\n [attr.aria-label]=\"'filters.clearFilters' | transloco\"\r\n (click)=\"clear()\"\r\n (keydown.enter)=\"clear()\">\r\n <filter-x-icon />\r\n <span class=\"sr-only\">{{ \"filters.clearFilters\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n\r\n @if (!isControlled() && selection() && validSelection()) {\r\n <icon-button\r\n [attr.title]=\"'filters.applyFilters' | transloco\"\r\n [attr.aria-label]=\"'filters.applyFilters' | transloco\"\r\n (click)=\"apply()\"\r\n (keydown.enter)=\"apply()\">\r\n <filter-icon />\r\n <span class=\"sr-only\">{{ \"filters.applyFilters\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n @if (collapsible()) {\r\n <icon-button\r\n title=\"Open/Close\"\r\n class=\"cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\r\n <chevronright />\r\n </icon-button>\r\n }\r\n </summary>\r\n\r\n <!-- content wrapper -->\r\n <form [formGroup]=\"form\">\r\n <ul\r\n class=\"flex max-h-[var(--scroll-height,20rem)] snap-y snap-start scrollbar-thin flex-col gap-1 overflow-auto pt-2\"\r\n role=\"list\">\r\n @for (option of dateOptions(); track $index) {\r\n <li\r\n role=\"listitem\"\r\n tabindex=\"0\"\r\n (click)=\"radio.click()\"\r\n [attr.aria-label]=\"option.display | syslang | transloco\"\r\n [class]=\"\r\n cn(\r\n 'flex p-0 px-2 leading-7',\r\n form.get('option')?.value === option.display && 'bg-accent',\r\n option.hidden && 'hidden',\r\n option.disabled && 'disabled pointer-events-none text-neutral-300'\r\n )\r\n \"\r\n [attr.aria-hidden]=\"option.disabled\">\r\n <input\r\n #radio\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-{{ option.display }}\"\r\n [attr.disabled]=\"option.disabled ? true : null\"\r\n [attr.aria-disabled]=\"option.disabled\"\r\n (click)=\"select()\"\r\n value=\"{{ option.display }}\" />\r\n\r\n <label\r\n for=\"date-filter-{{ option.display }}\"\r\n class=\"grow cursor-pointer p-1\">\r\n {{ option.display | syslang | transloco }}\r\n </label>\r\n </li>\r\n }\r\n\r\n @if (allowCustomRange) {\r\n <li\r\n role=\"listitem\"\r\n aria-label=\"open date range picker\"\r\n class=\"flex px-2 leading-7\"\r\n [class.select]=\"form.get('option')?.value === 'custom-range'\">\r\n <input\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-range-dialog\"\r\n value=\"custom-range\"\r\n (click)=\"select()\" />\r\n <div\r\n class=\"@container flex grow justify-end gap-1 p-1 @max-[340px]:flex-wrap\">\r\n <div class=\"flex gap-1\">\r\n <label for=\"datepicker-range-start\" class=\"min-w-10 truncate\">{{\r\n \"filters.from\" | transloco\r\n }}</label>\r\n <input\r\n id=\"datepicker-range-start\"\r\n name=\"start\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeFrom()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n <div class=\"flex gap-1\">\r\n <label\r\n for=\"datepicker-range-end\"\r\n class=\"min-w-10 truncate text-right\"\r\n >{{ \"filters.to\" | transloco }}</label\r\n >\r\n <input\r\n id=\"datepicker-range-end\"\r\n name=\"end\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeTo()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n </div>\r\n </li>\r\n }\r\n </ul>\r\n </form>\r\n</details>\r\n\r\n<aggregation-date-range-dialog\r\n [lang]=\"lang()\"\r\n [useDateRange]=\"false\"\r\n [min]=\"form.get('customRange.from')?.value || undefined\"\r\n [max]=\"form.get('customRange.to')?.value || undefined\"\r\n (rangeSelected)=\"onRangeSelected($event)\" />\r\n", styles: [":host{display:block;min-width:200px}ul[role=list]{scrollbar-width:thin}\n"], dependencies: [{ kind: "directive", type: IconButtonComponent, selector: "button[icon-button], icon-button, IconButton", inputs: ["class", "size"] }, { kind: "directive", type: ListItemComponent, selector: "[role=\"listitem\"], [role=\"option\"]", inputs: ["class", "variant", "decoration"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.RadioControlValueAccessor, selector: "input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: ChevronRightIcon, selector: "chevron-right, ChevronRight, chevronright, ChevronRightIcon, chevron-right-icon, chevronrighticon", inputs: ["class"] }, { kind: "component", type: AggregationDateRangeDialogComponent, selector: "aggregation-date-range-dialog", inputs: ["min", "max", "lang", "useDateRange"], outputs: ["rangeSelected"] }, { kind: "component", type: FaIconComponent, selector: "fa-icon, FaIcon", inputs: ["faClass", "class"] }, { kind: "component", type: FilterIcon, selector: "filter-icon, FilterIcon", inputs: ["class"] }, { kind: "component", type: FilterXIcon, selector: "filter-x-icon, FilterXIcon", inputs: ["class"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }, { kind: "pipe", type: SyslangPipe, name: "syslang" }] });
9375
9843
  }
9376
9844
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: DateComponent, decorators: [{
9377
9845
  type: Component,
@@ -9388,7 +9856,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
9388
9856
  FilterXIcon
9389
9857
  ], host: {
9390
9858
  class: "@container"
9391
- }, template: "<details [attr.open]=\"expanded()\" [attr.name]=\"id()\" class=\"group space-y-2\">\r\n <summary\r\n [class.cursor-pointer]=\"collapsible()\"\r\n class=\"m-0 flex h-8 w-full items-center pl-1 font-semibold select-none\"\r\n (click)=\"onHeaderClick($event)\">\r\n <ng-content select=\"label\">\r\n @let icon = aggregation()?.icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\r\n }\r\n <span class=\"grow\">{{\r\n aggregation()?.display | syslang | transloco\r\n }}</span>\r\n </ng-content>\r\n\r\n @if (hasFilters()) {\r\n <icon-button\r\n [attr.title]=\"'filters.clearFilters' | transloco\"\r\n [attr.aria-label]=\"'filters.clearFilters' | transloco\"\r\n (click)=\"clear()\"\r\n (keydown.enter)=\"clear()\">\r\n <filter-x-icon />\r\n <span class=\"sr-only\">{{ \"filters.clearFilters\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n\r\n @if (selection() && validSelection()) {\r\n <icon-button\r\n [attr.title]=\"'filters.applyFilters' | transloco\"\r\n [attr.aria-label]=\"'filters.applyFilters' | transloco\"\r\n (click)=\"apply()\"\r\n (keydown.enter)=\"apply()\">\r\n <filter-icon />\r\n <span class=\"sr-only\">{{ \"filters.applyFilters\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n @if (collapsible()) {\r\n <icon-button\r\n title=\"Open/Close\"\r\n class=\"cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\r\n <chevronright />\r\n </icon-button>\r\n }\r\n </summary>\r\n\r\n <!-- content wrapper -->\r\n <form [formGroup]=\"form\">\r\n <ul\r\n class=\"scrollbar-thin flex max-h-[var(--scroll-height,20rem)] snap-y snap-start flex-col gap-1 overflow-auto pt-2\"\r\n role=\"list\">\r\n @for (option of dateOptions(); track $index) {\r\n <li\r\n role=\"listitem\"\r\n tabindex=\"0\"\r\n (click)=\"radio.click()\"\r\n [attr.aria-label]=\"option.display | syslang | transloco\"\r\n [class]=\"\r\n cn(\r\n 'flex p-0 px-2 leading-7',\r\n form.get('option')?.value === option.display && 'bg-accent',\r\n option.hidden && 'hidden',\r\n option.disabled && 'disabled pointer-events-none text-neutral-300'\r\n )\r\n \"\r\n [attr.aria-hidden]=\"option.disabled\">\r\n <input\r\n #radio\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-{{ option.display }}\"\r\n [attr.disabled]=\"option.disabled ? true : null\"\r\n [attr.aria-disabled]=\"option.disabled\"\r\n (click)=\"select()\"\r\n value=\"{{ option.display }}\" />\r\n\r\n <label\r\n for=\"date-filter-{{ option.display }}\"\r\n class=\"grow cursor-pointer p-1\">\r\n {{ option.display | syslang | transloco }}\r\n </label>\r\n </li>\r\n }\r\n\r\n @if (allowCustomRange) {\r\n <li\r\n role=\"listitem\"\r\n aria-label=\"open date range picker\"\r\n class=\"flex px-2 leading-7\"\r\n [class.select]=\"form.get('option')?.value === 'custom-range'\">\r\n <input\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-range-dialog\"\r\n value=\"custom-range\"\r\n (click)=\"select()\" />\r\n <div\r\n class=\"@container flex grow justify-end gap-1 p-1 @max-[340px]:flex-wrap\">\r\n <div class=\"flex gap-1\">\r\n <label for=\"datepicker-range-start\" class=\"min-w-10 truncate\">{{\r\n \"filters.from\" | transloco\r\n }}</label>\r\n <input\r\n id=\"datepicker-range-start\"\r\n name=\"start\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeFrom()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n <div class=\"flex gap-1\">\r\n <label\r\n for=\"datepicker-range-end\"\r\n class=\"min-w-10 truncate text-right\"\r\n >{{ \"filters.to\" | transloco }}</label\r\n >\r\n <input\r\n id=\"datepicker-range-end\"\r\n name=\"end\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeTo()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n </div>\r\n </li>\r\n }\r\n </ul>\r\n </form>\r\n</details>\r\n\r\n<aggregation-date-range-dialog\r\n [lang]=\"lang()\"\r\n [useDateRange]=\"false\"\r\n [min]=\"form.get('customRange.from')?.value || undefined\"\r\n [max]=\"form.get('customRange.to')?.value || undefined\"\r\n (rangeSelected)=\"onRangeSelected($event)\" />\r\n", styles: [":host{display:block;min-width:200px}ul[role=list]{scrollbar-width:thin}\n"] }]
9859
+ }, template: "<details [attr.open]=\"expanded()\" [attr.name]=\"id()\" class=\"group space-y-2\">\r\n <summary\r\n [class.cursor-pointer]=\"collapsible()\"\r\n class=\"m-0 flex h-8 w-full items-center pl-1 font-semibold select-none\"\r\n (click)=\"onHeaderClick($event)\">\r\n <ng-content select=\"label\">\r\n @let icon = aggregation()?.icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\r\n }\r\n <span class=\"grow\">{{\r\n aggregation()?.display | syslang | transloco\r\n }}</span>\r\n </ng-content>\r\n\r\n @if (hasFilters()) {\r\n <icon-button\r\n [attr.title]=\"'filters.clearFilters' | transloco\"\r\n [attr.aria-label]=\"'filters.clearFilters' | transloco\"\r\n (click)=\"clear()\"\r\n (keydown.enter)=\"clear()\">\r\n <filter-x-icon />\r\n <span class=\"sr-only\">{{ \"filters.clearFilters\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n\r\n @if (!isControlled() && selection() && validSelection()) {\r\n <icon-button\r\n [attr.title]=\"'filters.applyFilters' | transloco\"\r\n [attr.aria-label]=\"'filters.applyFilters' | transloco\"\r\n (click)=\"apply()\"\r\n (keydown.enter)=\"apply()\">\r\n <filter-icon />\r\n <span class=\"sr-only\">{{ \"filters.applyFilters\" | transloco }}</span>\r\n </icon-button>\r\n }\r\n @if (collapsible()) {\r\n <icon-button\r\n title=\"Open/Close\"\r\n class=\"cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\r\n <chevronright />\r\n </icon-button>\r\n }\r\n </summary>\r\n\r\n <!-- content wrapper -->\r\n <form [formGroup]=\"form\">\r\n <ul\r\n class=\"flex max-h-[var(--scroll-height,20rem)] snap-y snap-start scrollbar-thin flex-col gap-1 overflow-auto pt-2\"\r\n role=\"list\">\r\n @for (option of dateOptions(); track $index) {\r\n <li\r\n role=\"listitem\"\r\n tabindex=\"0\"\r\n (click)=\"radio.click()\"\r\n [attr.aria-label]=\"option.display | syslang | transloco\"\r\n [class]=\"\r\n cn(\r\n 'flex p-0 px-2 leading-7',\r\n form.get('option')?.value === option.display && 'bg-accent',\r\n option.hidden && 'hidden',\r\n option.disabled && 'disabled pointer-events-none text-neutral-300'\r\n )\r\n \"\r\n [attr.aria-hidden]=\"option.disabled\">\r\n <input\r\n #radio\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-{{ option.display }}\"\r\n [attr.disabled]=\"option.disabled ? true : null\"\r\n [attr.aria-disabled]=\"option.disabled\"\r\n (click)=\"select()\"\r\n value=\"{{ option.display }}\" />\r\n\r\n <label\r\n for=\"date-filter-{{ option.display }}\"\r\n class=\"grow cursor-pointer p-1\">\r\n {{ option.display | syslang | transloco }}\r\n </label>\r\n </li>\r\n }\r\n\r\n @if (allowCustomRange) {\r\n <li\r\n role=\"listitem\"\r\n aria-label=\"open date range picker\"\r\n class=\"flex px-2 leading-7\"\r\n [class.select]=\"form.get('option')?.value === 'custom-range'\">\r\n <input\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-range-dialog\"\r\n value=\"custom-range\"\r\n (click)=\"select()\" />\r\n <div\r\n class=\"@container flex grow justify-end gap-1 p-1 @max-[340px]:flex-wrap\">\r\n <div class=\"flex gap-1\">\r\n <label for=\"datepicker-range-start\" class=\"min-w-10 truncate\">{{\r\n \"filters.from\" | transloco\r\n }}</label>\r\n <input\r\n id=\"datepicker-range-start\"\r\n name=\"start\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeFrom()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n <div class=\"flex gap-1\">\r\n <label\r\n for=\"datepicker-range-end\"\r\n class=\"min-w-10 truncate text-right\"\r\n >{{ \"filters.to\" | transloco }}</label\r\n >\r\n <input\r\n id=\"datepicker-range-end\"\r\n name=\"end\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeTo()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n </div>\r\n </li>\r\n }\r\n </ul>\r\n </form>\r\n</details>\r\n\r\n<aggregation-date-range-dialog\r\n [lang]=\"lang()\"\r\n [useDateRange]=\"false\"\r\n [min]=\"form.get('customRange.from')?.value || undefined\"\r\n [max]=\"form.get('customRange.to')?.value || undefined\"\r\n (rangeSelected)=\"onRangeSelected($event)\" />\r\n", styles: [":host{display:block;min-width:200px}ul[role=list]{scrollbar-width:thin}\n"] }]
9392
9860
  }] });
9393
9861
 
9394
9862
  class ArticleEntities {
@@ -13721,6 +14189,14 @@ class AggregationTreeComponent {
13721
14189
  collapsed = input(false, ...(ngDevMode ? [{ debugName: "collapsed" }] : []));
13722
14190
  searchable = input(undefined, ...(ngDevMode ? [{ debugName: "searchable" }] : []));
13723
14191
  showFiltersCount = input(false, ...(ngDevMode ? [{ debugName: "showFiltersCount", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
14192
+ /**
14193
+ * Whether to render the per-node checkbox. When `false`, the checkbox is hidden
14194
+ * but selection still toggles on row click — pair it with a custom `aggregationItem`
14195
+ * template that reflects the `selected` context to convey selection.
14196
+ *
14197
+ * @default true
14198
+ */
14199
+ showCheckbox = input(true, ...(ngDevMode ? [{ debugName: "showCheckbox", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
13724
14200
  expandedLevel = input(undefined, ...(ngDevMode ? [{ debugName: "expandedLevel", transform: (v) => {
13725
14201
  const n = numberAttribute(v);
13726
14202
  return Number.isNaN(n) ? undefined : n;
@@ -13737,13 +14213,63 @@ class AggregationTreeComponent {
13737
14213
  * @default true
13738
14214
  */
13739
14215
  syncUrl = input(true, ...(ngDevMode ? [{ debugName: "syncUrl", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
14216
+ /**
14217
+ * Controlled query. When set, the component's own operations (load-more,
14218
+ * search-within-facet, tree-expand) run against this query instead of reading
14219
+ * the shared `QueryParamsStore`. Use to target a query not tied to the current
14220
+ * app, without mutating the shared store or the URL.
14221
+ *
14222
+ * @default undefined — read `QueryParamsStore.getQuery()` (current behavior)
14223
+ */
14224
+ controlledQuery = input(undefined, ...(ngDevMode ? [{ debugName: "controlledQuery", alias: "query" }] : [{ alias: "query" }]));
14225
+ /**
14226
+ * Controlled data. When set, the component renders this aggregation object
14227
+ * instead of reading it from the shared `AggregationsStore`.
14228
+ *
14229
+ * @default undefined — read `AggregationsStore` (current behavior)
14230
+ */
14231
+ controlledAggregation = input(undefined, ...(ngDevMode ? [{ debugName: "controlledAggregation", alias: "aggregation" }] : [{ alias: "aggregation" }]));
14232
+ /**
14233
+ * Controlled selection. When set, item `$selected` state is derived from this
14234
+ * value (`paths` in the `"/A/B/*"` format) instead of the shared
14235
+ * `QueryParamsStore` filters.
14236
+ *
14237
+ * @default undefined — derive from `QueryParamsStore` filters (current behavior)
14238
+ */
14239
+ controlledSelection = input(undefined, ...(ngDevMode ? [{ debugName: "controlledSelection", alias: "selection" }] : [{ alias: "selection" }]));
13740
14240
  /* outputs */
13741
14241
  onSelect = output();
13742
14242
  onApply = output();
13743
14243
  onClear = output();
14244
+ /**
14245
+ * Emits the new selection (`"/A/B/*"` paths) when the user applies or clears
14246
+ * filters in controlled mode. The counterpart of the `[selection]` input —
14247
+ * emitted before `filtersChange` and `onApply`/`onClear`. In controlled mode
14248
+ * this is the only way changes surface: nothing is written to the shared
14249
+ * stores or the URL.
14250
+ */
14251
+ selectionChange = output();
14252
+ /**
14253
+ * Emits the applied selection as a structured `Filter` (the exact filter the
14254
+ * default mode would write to the store), ready for `fetchQuery({ filters })`.
14255
+ * `undefined` on clear. Controlled mode only — emitted after `selectionChange`,
14256
+ * before `onApply`/`onClear`.
14257
+ */
14258
+ filtersChange = output();
13744
14259
  /* view queries */
13745
14260
  virtualItems = viewChildren("virtualItem", ...(ngDevMode ? [{ debugName: "virtualItems" }] : []));
13746
14261
  scrollElement = viewChild("scrollElement", ...(ngDevMode ? [{ debugName: "scrollElement" }] : []));
14262
+ /* optional host-provided template for the node content/label region */
14263
+ itemDirective = contentChild(AggregationItemDirective, ...(ngDevMode ? [{ debugName: "itemDirective" }] : []));
14264
+ /**
14265
+ * Item content template relayed by the `<Aggregation>` wrapper (which cannot
14266
+ * project content into this component). Also usable directly as a `TemplateRef`
14267
+ * input. A directive projected straight onto this component takes precedence.
14268
+ */
14269
+ itemTemplateInput = input(undefined, ...(ngDevMode ? [{ debugName: "itemTemplateInput", alias: "itemTemplate" }] : [{
14270
+ alias: "itemTemplate",
14271
+ }]));
14272
+ resolvedItemTemplate = computed(() => this.itemDirective()?.template ?? this.itemTemplateInput(), ...(ngDevMode ? [{ debugName: "resolvedItemTemplate" }] : []));
13747
14273
  /* selection state */
13748
14274
  selection = signal(false, ...(ngDevMode ? [{ debugName: "selection" }] : []));
13749
14275
  isAllSelected = signal(false, ...(ngDevMode ? [{ debugName: "isAllSelected" }] : []));
@@ -13755,8 +14281,12 @@ class AggregationTreeComponent {
13755
14281
  column: this.column,
13756
14282
  searchText: this.searchText,
13757
14283
  syncUrl: this.syncUrl,
14284
+ query: this.controlledQuery,
14285
+ aggregation: this.controlledAggregation,
14286
+ selection: this.controlledSelection,
13758
14287
  });
13759
14288
  /* spread from base */
14289
+ isControlled = this.base.isControlled;
13760
14290
  aggregationsStore = this.base.aggregationsStore;
13761
14291
  queryParamsStore = this.base.queryParamsStore;
13762
14292
  appStore = this.base.appStore;
@@ -13783,6 +14313,21 @@ class AggregationTreeComponent {
13783
14313
  #measureItems = effect(() => this.virtualItems().forEach((el) => this.virtualizer.measureElement(el.nativeElement)), ...(ngDevMode ? [{ debugName: "#measureItems" }] : []));
13784
14314
  linkChildren = computed(() => this.appStore.general()?.features?.filterLinkChildren, ...(ngDevMode ? [{ debugName: "linkChildren" }] : []));
13785
14315
  aggregation = computed(() => {
14316
+ if (this.isControlled()) {
14317
+ const agg = this.base.controlledAggregation();
14318
+ if (!agg)
14319
+ return null;
14320
+ if (!agg.isTree) {
14321
+ error("The aggregation tree component does not support list aggregations. Please use the <Aggregation /> component instead.");
14322
+ }
14323
+ const enriched = this.aggregationsService.enrichAggregation(agg, this.base.selectionValues() ?? []);
14324
+ const expandedLevel = this.expandedLevel() ?? enriched.expandedLevel;
14325
+ if (expandedLevel) {
14326
+ this.expandItems(enriched.items, expandedLevel);
14327
+ }
14328
+ enriched.searchable = this.searchable() ?? enriched.searchable;
14329
+ return enriched;
14330
+ }
13786
14331
  getState(this.aggregationsStore);
13787
14332
  const name = this.name();
13788
14333
  const column = this.column();
@@ -13813,7 +14358,10 @@ class AggregationTreeComponent {
13813
14358
  else if (agg?.items) {
13814
14359
  res = this.addCurrentFiltersToItems();
13815
14360
  }
13816
- const sessionAggItem = sessionStorage.getItem(`agg-${agg?.column}`);
14361
+ // Controlled mode never touches sessionStorage: selection persistence is the
14362
+ // host's responsibility via the `selection` input, and the `agg-${column}` key
14363
+ // is shared with any default-mode instance on the same column.
14364
+ const sessionAggItem = this.isControlled() ? null : sessionStorage.getItem(`agg-${agg?.column}`);
13817
14365
  const sessionAgg = JSON.parse(sessionAggItem || "[]");
13818
14366
  return this.processAggregations(sessionAgg.length ? this.setSelected(res, sessionAgg) : res);
13819
14367
  }, ...(ngDevMode ? [{ debugName: "items" }] : []));
@@ -13837,13 +14385,16 @@ class AggregationTreeComponent {
13837
14385
  constructor() {
13838
14386
  effect(() => {
13839
14387
  getState(this.aggregationsStore);
14388
+ if (this.isControlled())
14389
+ return;
13840
14390
  sessionStorage.removeItem(`agg-${this.column()}`);
13841
14391
  });
13842
14392
  effect(() => {
13843
14393
  this.filters.set(this.getFilters());
13844
14394
  });
13845
14395
  this.destroyRef.onDestroy(() => {
13846
- sessionStorage.removeItem(`agg-${this.aggregation()?.column}`);
14396
+ if (!this.isControlled())
14397
+ sessionStorage.removeItem(`agg-${this.aggregation()?.column}`);
13847
14398
  if (this.selection()) {
13848
14399
  const unselect = (items) => {
13849
14400
  items.forEach((item) => {
@@ -13860,7 +14411,13 @@ class AggregationTreeComponent {
13860
14411
  clear() {
13861
14412
  const agg = this.aggregation();
13862
14413
  if (agg) {
13863
- this.queryParamsStore.removeFilterByName(agg.name, agg.column, { syncUrl: this.syncUrl() });
14414
+ if (this.isControlled()) {
14415
+ this.selectionChange.emit({ kind: "paths", paths: [] });
14416
+ this.filtersChange.emit(undefined);
14417
+ }
14418
+ else {
14419
+ this.queryParamsStore.removeFilterByName(agg.name, agg.column, { syncUrl: this.syncUrl() });
14420
+ }
13864
14421
  const unselect = (items) => {
13865
14422
  items.forEach((item) => {
13866
14423
  item.$selected = false;
@@ -13870,7 +14427,8 @@ class AggregationTreeComponent {
13870
14427
  });
13871
14428
  };
13872
14429
  unselect(this.items());
13873
- sessionStorage.removeItem(`agg-${this.aggregation()?.column}`);
14430
+ if (!this.isControlled())
14431
+ sessionStorage.removeItem(`agg-${this.aggregation()?.column}`);
13874
14432
  this.selection.set(false);
13875
14433
  this.isAllSelected.set(false);
13876
14434
  this.onClear.emit();
@@ -13891,21 +14449,57 @@ class AggregationTreeComponent {
13891
14449
  }
13892
14450
  }
13893
14451
  apply(overrideFilters) {
13894
- sessionStorage.setItem(`agg-${this.aggregation()?.column}`, JSON.stringify([...this.items()]));
13895
14452
  const filters = overrideFilters || this.getFilters();
13896
14453
  const agg = this.aggregation();
13897
- this.base.applyFilters(filters, agg, () => this.clear());
14454
+ if (this.isControlled()) {
14455
+ const paths = filters[0]?.values ?? [];
14456
+ this.selectionChange.emit({ kind: "paths", paths });
14457
+ this.filtersChange.emit(legacyFilterToFilter(this.base.buildAppliedFilter(filters, agg)));
14458
+ }
14459
+ else {
14460
+ sessionStorage.setItem(`agg-${agg?.column}`, JSON.stringify([...this.items()]));
14461
+ this.base.applyFilters(filters, agg, () => this.clear());
14462
+ }
13898
14463
  this.searchText.set("");
13899
14464
  this.selection.set(false);
13900
14465
  this.onApply.emit();
13901
14466
  }
13902
14467
  loadMore() {
14468
+ // Controlled mode loads into the local state — never the shared AggregationsStore.
14469
+ if (this.isControlled()) {
14470
+ const agg = this.base.controlledAggregation();
14471
+ if (!agg)
14472
+ return;
14473
+ this.aggregationsService
14474
+ .loadMore(this.base.resolvedQuery(), agg, undefined, { count: this.base.resolvedAggregationCount(agg) })
14475
+ .subscribe((aggregation) => {
14476
+ this.base.controlledAggregation.set(aggregation);
14477
+ });
14478
+ return;
14479
+ }
13903
14480
  const q = this.queryParamsStore.getQuery();
13904
14481
  this.aggregationsService.loadMore(q, this.aggregation()).subscribe((aggregation) => {
13905
14482
  this.aggregationsStore.updateAggregation(aggregation);
13906
14483
  });
13907
14484
  }
13908
14485
  async open(node) {
14486
+ // Controlled mode expands into the local state via the guard-free fetch —
14487
+ // never the shared AggregationsStore, never QueryService's global result signal.
14488
+ if (this.isControlled()) {
14489
+ const controlled = this.base.controlledAggregation();
14490
+ if (!controlled)
14491
+ return;
14492
+ // Spread before mutating: resolvedQuery() may return the host's query object.
14493
+ const q = { ...this.base.resolvedQuery() };
14494
+ delete q.filters;
14495
+ const agg = await firstValueFrom(this.aggregationsService.open(q, controlled, node, {
14496
+ guardFree: true,
14497
+ }));
14498
+ node.$opened = true;
14499
+ // replaceInTree mutates and returns the same reference — spread so the signal notifies.
14500
+ this.base.controlledAggregation.set({ ...agg });
14501
+ return;
14502
+ }
13909
14503
  const q = this.queryParamsStore.getQuery();
13910
14504
  delete q.filters;
13911
14505
  const agg = await firstValueFrom(this.aggregationsService.open(q, this.aggregation(), node));
@@ -13917,7 +14511,9 @@ class AggregationTreeComponent {
13917
14511
  this.onSelect.emit(selectedItems);
13918
14512
  this.selection.set(selectedItems.length > 0 || this.hasFilters());
13919
14513
  this.verifySelected();
13920
- sessionStorage.setItem(`agg-${this.aggregation()?.column}`, JSON.stringify([...this.items()]));
14514
+ if (!this.isControlled()) {
14515
+ sessionStorage.setItem(`agg-${this.aggregation()?.column}`, JSON.stringify([...this.items()]));
14516
+ }
13921
14517
  }
13922
14518
  /* item-level methods — called from the ng-template */
13923
14519
  treeItemName(item) {
@@ -13925,6 +14521,10 @@ class AggregationTreeComponent {
13925
14521
  return typeof value === "string" ? value : `${value}`;
13926
14522
  }
13927
14523
  isTreeItemFiltered(item, field) {
14524
+ if (this.isControlled()) {
14525
+ const values = [item.value, `/${item.$path}/*`];
14526
+ return (this.base.selectionValues() ?? []).some((value) => values.some((v) => v === value));
14527
+ }
13928
14528
  const filters = this.queryParamsStore.getFilter({ field: field ?? undefined, name: this.treeItemName(item) });
13929
14529
  if (!filters)
13930
14530
  return false;
@@ -13985,7 +14585,10 @@ class AggregationTreeComponent {
13985
14585
  const notSelectedItems = this.getFlattenTreeItems()
13986
14586
  .filter((item) => !item.$selected)
13987
14587
  .map((item) => (item.$path ? `/${item.$path}/*` : ""));
13988
- const currentFilters = this.queryParamsStore.getFilter({ field, name })?.values || [];
14588
+ const sel = this.controlledSelection();
14589
+ const currentFilters = this.isControlled()
14590
+ ? (sel?.kind === "paths" ? sel.paths : [])
14591
+ : this.queryParamsStore.getFilter({ field, name })?.values || [];
13989
14592
  if (currentFilters) {
13990
14593
  const filteredCurrentFilters = currentFilters.filter((filter) => !notSelectedItems.includes(filter));
13991
14594
  if (filteredCurrentFilters.length > 0) {
@@ -14111,7 +14714,7 @@ class AggregationTreeComponent {
14111
14714
  return false;
14112
14715
  }
14113
14716
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: AggregationTreeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
14114
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: AggregationTreeComponent, isStandalone: true, selector: "AggregationTree, aggregation-tree, aggregationtree", inputs: { class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null }, column: { classPropertyName: "column", publicName: "column", isSignal: true, isRequired: true, transformFunction: null }, collapsible: { classPropertyName: "collapsible", publicName: "collapsible", isSignal: true, isRequired: false, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, searchable: { classPropertyName: "searchable", publicName: "searchable", isSignal: true, isRequired: false, transformFunction: null }, showFiltersCount: { classPropertyName: "showFiltersCount", publicName: "showFiltersCount", isSignal: true, isRequired: false, transformFunction: null }, expandedLevel: { classPropertyName: "expandedLevel", publicName: "expandedLevel", isSignal: true, isRequired: false, transformFunction: null }, syncUrl: { classPropertyName: "syncUrl", publicName: "syncUrl", isSignal: true, isRequired: false, transformFunction: null }, searchText: { classPropertyName: "searchText", publicName: "searchText", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSelect: "onSelect", onApply: "onApply", onClear: "onClear", searchText: "searchTextChange" }, host: { properties: { "class": "cn(\"block h-[inherit] max-h-[inherit] w-[inherit]\", class())" } }, viewQueries: [{ propertyName: "virtualItems", predicate: ["virtualItem"], descendants: true, isSignal: true }, { propertyName: "scrollElement", first: true, predicate: ["scrollElement"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (!aggregation()?.isTree) {\r\n <div class=\"p-2 text-sm text-red-500\">\r\n <triangle-alert-icon class=\"mr-1\" />\r\n The aggregationTree component does not support list aggregations. Please use\r\n the &lt;Aggregation /&gt; component instead.\r\n </div>\r\n}\r\n<aggregation-panel\r\n [id]=\"id()\"\r\n [collapsible]=\"collapsible()\"\r\n [collapsed]=\"collapsed()\"\r\n [isEmpty]=\"isEmpty()\"\r\n [isDate]=\"isDate()\"\r\n [aggregation]=\"aggregation()\"\r\n [showFiltersCount]=\"showFiltersCount()\"\r\n [filtersCount]=\"filtersCount()\"\r\n [hasFilters]=\"hasFilters()\"\r\n [selection]=\"selection()\"\r\n [isAllSelected]=\"isAllSelected()\"\r\n [(searchText)]=\"searchText\"\r\n [itemsLength]=\"items().length\"\r\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\r\n [searchedItemsLength]=\"searchedItems().length\"\r\n (cleared)=\"clear()\"\r\n (applied)=\"apply()\"\r\n (allSelected)=\"selectAll()\"\r\n (allUnselected)=\"unselectAll()\"\r\n (loadedMore)=\"loadMore()\">\r\n <div\r\n #scrollElement\r\n class=\"scrollbar-thin max-h-(--scroll-height,20rem) w-full overflow-auto\">\r\n <div\r\n class=\"relative w-full\"\r\n [style.height]=\"virtualizer.getTotalSize() + 'px'\"\r\n role=\"tree\"\r\n [attr.aria-label]=\"aggregation()?.display | syslang | transloco\">\r\n <div\r\n class=\"absolute top-0 left-0 w-full\"\r\n [style.transform]=\"\r\n 'translateY(' +\r\n (virtualizer.getVirtualItems()[0]\r\n ? virtualizer.getVirtualItems()[0].start\r\n : 0) +\r\n 'px)'\r\n \">\r\n @for (vItem of virtualizer.getVirtualItems(); track vItem.index) {\r\n @let item = items()[vItem.index];\r\n <div #virtualItem [attr.data-index]=\"vItem.index\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"treeItemTpl\"\r\n [ngTemplateOutletContext]=\"{\r\n node: item,\r\n path: [],\r\n field: aggregation()?.column,\r\n tpl: treeItemTpl,\r\n parent: null\r\n }\">\r\n </ng-container>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n</aggregation-panel>\r\n\r\n<ng-template\r\n #treeItemTpl\r\n let-node=\"node\"\r\n let-path=\"path\"\r\n let-field=\"field\"\r\n let-tpl=\"tpl\"\r\n let-parent=\"parent\">\r\n <div\r\n role=\"treeitem\"\r\n [style.--level]=\"treeItemLevel(node)\"\r\n [attr.aria-selected]=\"node.$selected ?? false\"\r\n [attr.aria-expanded]=\"\r\n node.hasChildren !== false ? (node.$opened ?? false) : null\r\n \"\r\n [attr.aria-disabled]=\"node.count === 0 ? 'true' : null\"\r\n [attr.disabled]=\"node.count === 0 ? 'disabled' : null\">\r\n <a\r\n [attr.aria-label]=\"treeItemName(node) | syslang\"\r\n [class]=\"\r\n cn(\r\n 'flex grow items-center gap-2 p-1 leading-7',\r\n node.count === 0 && 'disabled pointer-events-none',\r\n (node.$selected || node.$selectedVisually) && ''\r\n )\r\n \"\r\n (click)=\"selectTreeItem(node, parent, $event, true)\">\r\n <button\r\n variant=\"none\"\r\n [iconOnly]=\"true\"\r\n (click)=\"toggleTreeNode($event, node)\"\r\n class=\"transition-transform ease-in hover:scale-125\"\r\n aria-label=\"Open\">\r\n <ChevronRight\r\n [class]=\"\r\n cn(\r\n 'size-4 translate-x-1',\r\n node.$opened && 'rotate-90',\r\n !node.hasChildren && 'hidden'\r\n )\r\n \"\r\n width=\"16\"\r\n height=\"16\" />\r\n </button>\r\n <input\r\n type=\"checkbox\"\r\n role=\"checkbox\"\r\n value=\"{{ node.value }}\"\r\n [attr.disabled]=\"node.count === 0 ? true : null\"\r\n [attr.aria-disabled]=\"node.count === 0\"\r\n (keydown.enter)=\"selectTreeItem(node, parent, $event)\"\r\n [checked]=\"node.$selected || node.$selectedVisually\" />\r\n @let icon = node.icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"self-center justify-self-center\" />\r\n }\r\n <span\r\n [class]=\"\r\n cn(\r\n 'line-clamp-1 break-all text-ellipsis',\r\n quickFilter() && 'hover:underline'\r\n )\r\n \"\r\n [title]=\"\r\n quickFilter()\r\n ? ((isTreeItemFiltered(node, field)\r\n ? 'filters.removeFilter'\r\n : 'filters.addFilter'\r\n ) | transloco) +\r\n ': ' +\r\n (treeItemName(node) | syslang)\r\n : (treeItemName(node) | syslang)\r\n \"\r\n (click)=\"onTreeItemTextClick(node, parent, $event)\">\r\n @for (\r\n chunk of (treeItemName(node) | syslang) ?? \"\"\r\n | highlightWord: searchText() : 10;\r\n track $index\r\n ) {\r\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\r\n chunk.text\r\n }}</span>\r\n }\r\n </span>\r\n @if (showCount() && node.count > 0) {\r\n <span class=\"ml-auto px-1 text-xs empty:hidden\" aria-hidden=\"true\">{{\r\n node.count\r\n }}</span>\r\n }\r\n </a>\r\n @if (node.hasChildren && node.$opened) {\r\n <div role=\"group\">\r\n @for (child of node.items; track $index) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"tpl\"\r\n [ngTemplateOutletContext]=\"{\r\n node: child,\r\n path: treeChildrenPath(node, path),\r\n field: field,\r\n tpl: tpl,\r\n parent: node\r\n }\">\r\n </ng-container>\r\n }\r\n </div>\r\n }\r\n </div>\r\n</ng-template>\r\n", styles: ["div[role=treeitem]:has(+div[role=treeitem]){margin-bottom:var(--agg-item-gap, 0)}div[role=treeitem]{display:block;-webkit-user-select:none;user-select:none}div[role=treeitem] a{padding-left:calc(var(--agg-tree-indent, .5rem) * var(--level, 0));line-height:var(--agg-item-height, inherit)}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: ButtonComponent, selector: "button", inputs: ["class", "variant", "decoration", "scheme", "iconOnly", "size", "solid"] }, { kind: "component", type: ChevronRightIcon, selector: "chevron-right, ChevronRight, chevronright, ChevronRightIcon, chevron-right-icon, chevronrighticon", inputs: ["class"] }, { kind: "component", type: FaIconComponent, selector: "fa-icon, FaIcon", inputs: ["faClass", "class"] }, { kind: "component", type: TriangleAlertIcon, selector: "triangle-alert-icon, TriangleAlertIcon", inputs: ["class"] }, { kind: "component", type: AggregationPanelComponent, selector: "AggregationPanel, aggregation-panel", inputs: ["id", "collapsible", "collapsed", "isDate", "isEmpty", "aggregation", "showFiltersCount", "filtersCount", "hasFilters", "selection", "isAllSelected", "searchText", "itemsLength", "hasMore", "searchedItemsLength"], outputs: ["searchTextChange", "cleared", "applied", "allSelected", "allUnselected", "loadedMore"] }, { kind: "pipe", type: SyslangPipe, name: "syslang" }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }, { kind: "pipe", type: HighlightWordPipe, name: "highlightWord" }] });
14717
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: AggregationTreeComponent, isStandalone: true, selector: "AggregationTree, aggregation-tree, aggregationtree", inputs: { class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null }, column: { classPropertyName: "column", publicName: "column", isSignal: true, isRequired: true, transformFunction: null }, collapsible: { classPropertyName: "collapsible", publicName: "collapsible", isSignal: true, isRequired: false, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, searchable: { classPropertyName: "searchable", publicName: "searchable", isSignal: true, isRequired: false, transformFunction: null }, showFiltersCount: { classPropertyName: "showFiltersCount", publicName: "showFiltersCount", isSignal: true, isRequired: false, transformFunction: null }, showCheckbox: { classPropertyName: "showCheckbox", publicName: "showCheckbox", isSignal: true, isRequired: false, transformFunction: null }, expandedLevel: { classPropertyName: "expandedLevel", publicName: "expandedLevel", isSignal: true, isRequired: false, transformFunction: null }, syncUrl: { classPropertyName: "syncUrl", publicName: "syncUrl", isSignal: true, isRequired: false, transformFunction: null }, controlledQuery: { classPropertyName: "controlledQuery", publicName: "query", isSignal: true, isRequired: false, transformFunction: null }, controlledAggregation: { classPropertyName: "controlledAggregation", publicName: "aggregation", isSignal: true, isRequired: false, transformFunction: null }, controlledSelection: { classPropertyName: "controlledSelection", publicName: "selection", isSignal: true, isRequired: false, transformFunction: null }, itemTemplateInput: { classPropertyName: "itemTemplateInput", publicName: "itemTemplate", isSignal: true, isRequired: false, transformFunction: null }, searchText: { classPropertyName: "searchText", publicName: "searchText", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSelect: "onSelect", onApply: "onApply", onClear: "onClear", selectionChange: "selectionChange", filtersChange: "filtersChange", searchText: "searchTextChange" }, host: { properties: { "class": "cn(\"block h-[inherit] max-h-[inherit] w-[inherit]\", class())" } }, queries: [{ propertyName: "itemDirective", first: true, predicate: AggregationItemDirective, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "virtualItems", predicate: ["virtualItem"], descendants: true, isSignal: true }, { propertyName: "scrollElement", first: true, predicate: ["scrollElement"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (!aggregation()?.isTree) {\r\n <div class=\"p-2 text-sm text-red-500\">\r\n <triangle-alert-icon class=\"mr-1\" />\r\n The aggregationTree component does not support list aggregations. Please use\r\n the &lt;Aggregation /&gt; component instead.\r\n </div>\r\n}\r\n<aggregation-panel\r\n [id]=\"id()\"\r\n [collapsible]=\"collapsible()\"\r\n [collapsed]=\"collapsed()\"\r\n [isEmpty]=\"isEmpty()\"\r\n [isDate]=\"isDate()\"\r\n [aggregation]=\"aggregation()\"\r\n [showFiltersCount]=\"showFiltersCount()\"\r\n [filtersCount]=\"filtersCount()\"\r\n [hasFilters]=\"hasFilters()\"\r\n [selection]=\"selection()\"\r\n [isAllSelected]=\"isAllSelected()\"\r\n [(searchText)]=\"searchText\"\r\n [itemsLength]=\"items().length\"\r\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\r\n [searchedItemsLength]=\"searchedItems().length\"\r\n (cleared)=\"clear()\"\r\n (applied)=\"apply()\"\r\n (allSelected)=\"selectAll()\"\r\n (allUnselected)=\"unselectAll()\"\r\n (loadedMore)=\"loadMore()\">\r\n <div\r\n #scrollElement\r\n class=\"max-h-(--scroll-height,20rem) w-full scrollbar-thin overflow-auto\">\r\n <div\r\n class=\"relative w-full\"\r\n [style.height]=\"virtualizer.getTotalSize() + 'px'\"\r\n role=\"tree\"\r\n [attr.aria-label]=\"aggregation()?.display | syslang | transloco\">\r\n <div\r\n class=\"absolute top-0 left-0 w-full\"\r\n [style.transform]=\"\r\n 'translateY(' +\r\n (virtualizer.getVirtualItems()[0]\r\n ? virtualizer.getVirtualItems()[0].start\r\n : 0) +\r\n 'px)'\r\n \">\r\n @for (vItem of virtualizer.getVirtualItems(); track vItem.index) {\r\n @let item = items()[vItem.index];\r\n <div #virtualItem [attr.data-index]=\"vItem.index\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"treeItemTpl\"\r\n [ngTemplateOutletContext]=\"{\r\n node: item,\r\n path: [],\r\n field: aggregation()?.column,\r\n tpl: treeItemTpl,\r\n parent: null\r\n }\">\r\n </ng-container>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n</aggregation-panel>\r\n\r\n<ng-template\r\n #treeItemTpl\r\n let-node=\"node\"\r\n let-path=\"path\"\r\n let-field=\"field\"\r\n let-tpl=\"tpl\"\r\n let-parent=\"parent\">\r\n <div\r\n role=\"treeitem\"\r\n [style.--level]=\"treeItemLevel(node)\"\r\n [attr.aria-selected]=\"node.$selected ?? false\"\r\n [attr.aria-expanded]=\"\r\n node.hasChildren !== false ? (node.$opened ?? false) : null\r\n \"\r\n [attr.aria-disabled]=\"node.count === 0 ? 'true' : null\"\r\n [attr.disabled]=\"node.count === 0 ? 'disabled' : null\">\r\n <a\r\n [attr.aria-label]=\"treeItemName(node) | syslang\"\r\n [class]=\"\r\n cn(\r\n 'flex grow items-center gap-2 p-1 leading-7',\r\n node.count === 0 && 'disabled pointer-events-none',\r\n (node.$selected || node.$selectedVisually) && ''\r\n )\r\n \"\r\n (click)=\"selectTreeItem(node, parent, $event, true)\">\r\n <button\r\n variant=\"none\"\r\n [iconOnly]=\"true\"\r\n (click)=\"toggleTreeNode($event, node)\"\r\n class=\"transition-transform ease-in hover:scale-125\"\r\n aria-label=\"Open\">\r\n <ChevronRight\r\n [class]=\"\r\n cn(\r\n 'size-4 translate-x-1',\r\n node.$opened && 'rotate-90',\r\n !node.hasChildren && 'hidden'\r\n )\r\n \"\r\n width=\"16\"\r\n height=\"16\" />\r\n </button>\r\n @if (showCheckbox()) {\r\n <input\r\n type=\"checkbox\"\r\n role=\"checkbox\"\r\n value=\"{{ node.value }}\"\r\n [attr.disabled]=\"node.count === 0 ? true : null\"\r\n [attr.aria-disabled]=\"node.count === 0\"\r\n (keydown.enter)=\"selectTreeItem(node, parent, $event)\"\r\n [checked]=\"node.$selected || node.$selectedVisually\" />\r\n }\r\n @if (resolvedItemTemplate(); as tpl) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"tpl\"\r\n [ngTemplateOutletContext]=\"{\r\n $implicit: node,\r\n field: field,\r\n selected: node.$selected || node.$selectedVisually || false,\r\n count: node.count,\r\n name: (treeItemName(node) | syslang) ?? '',\r\n searchText: searchText(),\r\n level: treeItemLevel(node),\r\n expanded: node.$opened ?? false,\r\n hasChildren: node.hasChildren ?? false\r\n }\" />\r\n } @else {\r\n @let icon = node.icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"self-center justify-self-center\" />\r\n }\r\n <span\r\n [class]=\"\r\n cn(\r\n 'line-clamp-1 break-all text-ellipsis',\r\n quickFilter() && 'hover:underline'\r\n )\r\n \"\r\n [title]=\"\r\n quickFilter()\r\n ? ((isTreeItemFiltered(node, field)\r\n ? 'filters.removeFilter'\r\n : 'filters.addFilter'\r\n ) | transloco) +\r\n ': ' +\r\n (treeItemName(node) | syslang)\r\n : (treeItemName(node) | syslang)\r\n \"\r\n (click)=\"onTreeItemTextClick(node, parent, $event)\">\r\n @for (\r\n chunk of (treeItemName(node) | syslang) ?? \"\"\r\n | highlightWord: searchText() : 10;\r\n track $index\r\n ) {\r\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\r\n chunk.text\r\n }}</span>\r\n }\r\n </span>\r\n @if (showCount() && node.count > 0) {\r\n <span class=\"ml-auto px-1 text-xs empty:hidden\" aria-hidden=\"true\">{{\r\n node.count\r\n }}</span>\r\n }\r\n }\r\n </a>\r\n @if (node.hasChildren && node.$opened) {\r\n <div role=\"group\">\r\n @for (child of node.items; track $index) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"tpl\"\r\n [ngTemplateOutletContext]=\"{\r\n node: child,\r\n path: treeChildrenPath(node, path),\r\n field: field,\r\n tpl: tpl,\r\n parent: node\r\n }\">\r\n </ng-container>\r\n }\r\n </div>\r\n }\r\n </div>\r\n</ng-template>\r\n", styles: ["div[role=treeitem]:has(+div[role=treeitem]){margin-bottom:var(--agg-item-gap, 0)}div[role=treeitem]{display:block;-webkit-user-select:none;user-select:none}div[role=treeitem] a{padding-left:calc(var(--agg-tree-indent, .5rem) * var(--level, 0));line-height:var(--agg-item-height, inherit)}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: ButtonComponent, selector: "button", inputs: ["class", "variant", "decoration", "scheme", "iconOnly", "size", "solid"] }, { kind: "component", type: ChevronRightIcon, selector: "chevron-right, ChevronRight, chevronright, ChevronRightIcon, chevron-right-icon, chevronrighticon", inputs: ["class"] }, { kind: "component", type: FaIconComponent, selector: "fa-icon, FaIcon", inputs: ["faClass", "class"] }, { kind: "component", type: TriangleAlertIcon, selector: "triangle-alert-icon, TriangleAlertIcon", inputs: ["class"] }, { kind: "component", type: AggregationPanelComponent, selector: "AggregationPanel, aggregation-panel", inputs: ["id", "collapsible", "collapsed", "isDate", "isEmpty", "aggregation", "showFiltersCount", "filtersCount", "hasFilters", "selection", "isAllSelected", "searchText", "itemsLength", "hasMore", "searchedItemsLength"], outputs: ["searchTextChange", "cleared", "applied", "allSelected", "allUnselected", "loadedMore"] }, { kind: "pipe", type: SyslangPipe, name: "syslang" }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }, { kind: "pipe", type: HighlightWordPipe, name: "highlightWord" }] });
14115
14718
  }
14116
14719
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: AggregationTreeComponent, decorators: [{
14117
14720
  type: Component,
@@ -14129,8 +14732,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
14129
14732
  AggregationPanelComponent,
14130
14733
  ], standalone: true, host: {
14131
14734
  "[class]": 'cn("block h-[inherit] max-h-[inherit] w-[inherit]", class())',
14132
- }, template: "@if (!aggregation()?.isTree) {\r\n <div class=\"p-2 text-sm text-red-500\">\r\n <triangle-alert-icon class=\"mr-1\" />\r\n The aggregationTree component does not support list aggregations. Please use\r\n the &lt;Aggregation /&gt; component instead.\r\n </div>\r\n}\r\n<aggregation-panel\r\n [id]=\"id()\"\r\n [collapsible]=\"collapsible()\"\r\n [collapsed]=\"collapsed()\"\r\n [isEmpty]=\"isEmpty()\"\r\n [isDate]=\"isDate()\"\r\n [aggregation]=\"aggregation()\"\r\n [showFiltersCount]=\"showFiltersCount()\"\r\n [filtersCount]=\"filtersCount()\"\r\n [hasFilters]=\"hasFilters()\"\r\n [selection]=\"selection()\"\r\n [isAllSelected]=\"isAllSelected()\"\r\n [(searchText)]=\"searchText\"\r\n [itemsLength]=\"items().length\"\r\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\r\n [searchedItemsLength]=\"searchedItems().length\"\r\n (cleared)=\"clear()\"\r\n (applied)=\"apply()\"\r\n (allSelected)=\"selectAll()\"\r\n (allUnselected)=\"unselectAll()\"\r\n (loadedMore)=\"loadMore()\">\r\n <div\r\n #scrollElement\r\n class=\"scrollbar-thin max-h-(--scroll-height,20rem) w-full overflow-auto\">\r\n <div\r\n class=\"relative w-full\"\r\n [style.height]=\"virtualizer.getTotalSize() + 'px'\"\r\n role=\"tree\"\r\n [attr.aria-label]=\"aggregation()?.display | syslang | transloco\">\r\n <div\r\n class=\"absolute top-0 left-0 w-full\"\r\n [style.transform]=\"\r\n 'translateY(' +\r\n (virtualizer.getVirtualItems()[0]\r\n ? virtualizer.getVirtualItems()[0].start\r\n : 0) +\r\n 'px)'\r\n \">\r\n @for (vItem of virtualizer.getVirtualItems(); track vItem.index) {\r\n @let item = items()[vItem.index];\r\n <div #virtualItem [attr.data-index]=\"vItem.index\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"treeItemTpl\"\r\n [ngTemplateOutletContext]=\"{\r\n node: item,\r\n path: [],\r\n field: aggregation()?.column,\r\n tpl: treeItemTpl,\r\n parent: null\r\n }\">\r\n </ng-container>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n</aggregation-panel>\r\n\r\n<ng-template\r\n #treeItemTpl\r\n let-node=\"node\"\r\n let-path=\"path\"\r\n let-field=\"field\"\r\n let-tpl=\"tpl\"\r\n let-parent=\"parent\">\r\n <div\r\n role=\"treeitem\"\r\n [style.--level]=\"treeItemLevel(node)\"\r\n [attr.aria-selected]=\"node.$selected ?? false\"\r\n [attr.aria-expanded]=\"\r\n node.hasChildren !== false ? (node.$opened ?? false) : null\r\n \"\r\n [attr.aria-disabled]=\"node.count === 0 ? 'true' : null\"\r\n [attr.disabled]=\"node.count === 0 ? 'disabled' : null\">\r\n <a\r\n [attr.aria-label]=\"treeItemName(node) | syslang\"\r\n [class]=\"\r\n cn(\r\n 'flex grow items-center gap-2 p-1 leading-7',\r\n node.count === 0 && 'disabled pointer-events-none',\r\n (node.$selected || node.$selectedVisually) && ''\r\n )\r\n \"\r\n (click)=\"selectTreeItem(node, parent, $event, true)\">\r\n <button\r\n variant=\"none\"\r\n [iconOnly]=\"true\"\r\n (click)=\"toggleTreeNode($event, node)\"\r\n class=\"transition-transform ease-in hover:scale-125\"\r\n aria-label=\"Open\">\r\n <ChevronRight\r\n [class]=\"\r\n cn(\r\n 'size-4 translate-x-1',\r\n node.$opened && 'rotate-90',\r\n !node.hasChildren && 'hidden'\r\n )\r\n \"\r\n width=\"16\"\r\n height=\"16\" />\r\n </button>\r\n <input\r\n type=\"checkbox\"\r\n role=\"checkbox\"\r\n value=\"{{ node.value }}\"\r\n [attr.disabled]=\"node.count === 0 ? true : null\"\r\n [attr.aria-disabled]=\"node.count === 0\"\r\n (keydown.enter)=\"selectTreeItem(node, parent, $event)\"\r\n [checked]=\"node.$selected || node.$selectedVisually\" />\r\n @let icon = node.icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"self-center justify-self-center\" />\r\n }\r\n <span\r\n [class]=\"\r\n cn(\r\n 'line-clamp-1 break-all text-ellipsis',\r\n quickFilter() && 'hover:underline'\r\n )\r\n \"\r\n [title]=\"\r\n quickFilter()\r\n ? ((isTreeItemFiltered(node, field)\r\n ? 'filters.removeFilter'\r\n : 'filters.addFilter'\r\n ) | transloco) +\r\n ': ' +\r\n (treeItemName(node) | syslang)\r\n : (treeItemName(node) | syslang)\r\n \"\r\n (click)=\"onTreeItemTextClick(node, parent, $event)\">\r\n @for (\r\n chunk of (treeItemName(node) | syslang) ?? \"\"\r\n | highlightWord: searchText() : 10;\r\n track $index\r\n ) {\r\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\r\n chunk.text\r\n }}</span>\r\n }\r\n </span>\r\n @if (showCount() && node.count > 0) {\r\n <span class=\"ml-auto px-1 text-xs empty:hidden\" aria-hidden=\"true\">{{\r\n node.count\r\n }}</span>\r\n }\r\n </a>\r\n @if (node.hasChildren && node.$opened) {\r\n <div role=\"group\">\r\n @for (child of node.items; track $index) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"tpl\"\r\n [ngTemplateOutletContext]=\"{\r\n node: child,\r\n path: treeChildrenPath(node, path),\r\n field: field,\r\n tpl: tpl,\r\n parent: node\r\n }\">\r\n </ng-container>\r\n }\r\n </div>\r\n }\r\n </div>\r\n</ng-template>\r\n", styles: ["div[role=treeitem]:has(+div[role=treeitem]){margin-bottom:var(--agg-item-gap, 0)}div[role=treeitem]{display:block;-webkit-user-select:none;user-select:none}div[role=treeitem] a{padding-left:calc(var(--agg-tree-indent, .5rem) * var(--level, 0));line-height:var(--agg-item-height, inherit)}\n"] }]
14133
- }], ctorParameters: () => [], propDecorators: { class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }], column: [{ type: i0.Input, args: [{ isSignal: true, alias: "column", required: true }] }], collapsible: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsible", required: false }] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], showFiltersCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFiltersCount", required: false }] }], expandedLevel: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandedLevel", required: false }] }], syncUrl: [{ type: i0.Input, args: [{ isSignal: true, alias: "syncUrl", required: false }] }], onSelect: [{ type: i0.Output, args: ["onSelect"] }], onApply: [{ type: i0.Output, args: ["onApply"] }], onClear: [{ type: i0.Output, args: ["onClear"] }], virtualItems: [{ type: i0.ViewChildren, args: ["virtualItem", { isSignal: true }] }], scrollElement: [{ type: i0.ViewChild, args: ["scrollElement", { isSignal: true }] }], searchText: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchText", required: false }] }, { type: i0.Output, args: ["searchTextChange"] }] } });
14735
+ }, template: "@if (!aggregation()?.isTree) {\r\n <div class=\"p-2 text-sm text-red-500\">\r\n <triangle-alert-icon class=\"mr-1\" />\r\n The aggregationTree component does not support list aggregations. Please use\r\n the &lt;Aggregation /&gt; component instead.\r\n </div>\r\n}\r\n<aggregation-panel\r\n [id]=\"id()\"\r\n [collapsible]=\"collapsible()\"\r\n [collapsed]=\"collapsed()\"\r\n [isEmpty]=\"isEmpty()\"\r\n [isDate]=\"isDate()\"\r\n [aggregation]=\"aggregation()\"\r\n [showFiltersCount]=\"showFiltersCount()\"\r\n [filtersCount]=\"filtersCount()\"\r\n [hasFilters]=\"hasFilters()\"\r\n [selection]=\"selection()\"\r\n [isAllSelected]=\"isAllSelected()\"\r\n [(searchText)]=\"searchText\"\r\n [itemsLength]=\"items().length\"\r\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\r\n [searchedItemsLength]=\"searchedItems().length\"\r\n (cleared)=\"clear()\"\r\n (applied)=\"apply()\"\r\n (allSelected)=\"selectAll()\"\r\n (allUnselected)=\"unselectAll()\"\r\n (loadedMore)=\"loadMore()\">\r\n <div\r\n #scrollElement\r\n class=\"max-h-(--scroll-height,20rem) w-full scrollbar-thin overflow-auto\">\r\n <div\r\n class=\"relative w-full\"\r\n [style.height]=\"virtualizer.getTotalSize() + 'px'\"\r\n role=\"tree\"\r\n [attr.aria-label]=\"aggregation()?.display | syslang | transloco\">\r\n <div\r\n class=\"absolute top-0 left-0 w-full\"\r\n [style.transform]=\"\r\n 'translateY(' +\r\n (virtualizer.getVirtualItems()[0]\r\n ? virtualizer.getVirtualItems()[0].start\r\n : 0) +\r\n 'px)'\r\n \">\r\n @for (vItem of virtualizer.getVirtualItems(); track vItem.index) {\r\n @let item = items()[vItem.index];\r\n <div #virtualItem [attr.data-index]=\"vItem.index\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"treeItemTpl\"\r\n [ngTemplateOutletContext]=\"{\r\n node: item,\r\n path: [],\r\n field: aggregation()?.column,\r\n tpl: treeItemTpl,\r\n parent: null\r\n }\">\r\n </ng-container>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n</aggregation-panel>\r\n\r\n<ng-template\r\n #treeItemTpl\r\n let-node=\"node\"\r\n let-path=\"path\"\r\n let-field=\"field\"\r\n let-tpl=\"tpl\"\r\n let-parent=\"parent\">\r\n <div\r\n role=\"treeitem\"\r\n [style.--level]=\"treeItemLevel(node)\"\r\n [attr.aria-selected]=\"node.$selected ?? false\"\r\n [attr.aria-expanded]=\"\r\n node.hasChildren !== false ? (node.$opened ?? false) : null\r\n \"\r\n [attr.aria-disabled]=\"node.count === 0 ? 'true' : null\"\r\n [attr.disabled]=\"node.count === 0 ? 'disabled' : null\">\r\n <a\r\n [attr.aria-label]=\"treeItemName(node) | syslang\"\r\n [class]=\"\r\n cn(\r\n 'flex grow items-center gap-2 p-1 leading-7',\r\n node.count === 0 && 'disabled pointer-events-none',\r\n (node.$selected || node.$selectedVisually) && ''\r\n )\r\n \"\r\n (click)=\"selectTreeItem(node, parent, $event, true)\">\r\n <button\r\n variant=\"none\"\r\n [iconOnly]=\"true\"\r\n (click)=\"toggleTreeNode($event, node)\"\r\n class=\"transition-transform ease-in hover:scale-125\"\r\n aria-label=\"Open\">\r\n <ChevronRight\r\n [class]=\"\r\n cn(\r\n 'size-4 translate-x-1',\r\n node.$opened && 'rotate-90',\r\n !node.hasChildren && 'hidden'\r\n )\r\n \"\r\n width=\"16\"\r\n height=\"16\" />\r\n </button>\r\n @if (showCheckbox()) {\r\n <input\r\n type=\"checkbox\"\r\n role=\"checkbox\"\r\n value=\"{{ node.value }}\"\r\n [attr.disabled]=\"node.count === 0 ? true : null\"\r\n [attr.aria-disabled]=\"node.count === 0\"\r\n (keydown.enter)=\"selectTreeItem(node, parent, $event)\"\r\n [checked]=\"node.$selected || node.$selectedVisually\" />\r\n }\r\n @if (resolvedItemTemplate(); as tpl) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"tpl\"\r\n [ngTemplateOutletContext]=\"{\r\n $implicit: node,\r\n field: field,\r\n selected: node.$selected || node.$selectedVisually || false,\r\n count: node.count,\r\n name: (treeItemName(node) | syslang) ?? '',\r\n searchText: searchText(),\r\n level: treeItemLevel(node),\r\n expanded: node.$opened ?? false,\r\n hasChildren: node.hasChildren ?? false\r\n }\" />\r\n } @else {\r\n @let icon = node.icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"self-center justify-self-center\" />\r\n }\r\n <span\r\n [class]=\"\r\n cn(\r\n 'line-clamp-1 break-all text-ellipsis',\r\n quickFilter() && 'hover:underline'\r\n )\r\n \"\r\n [title]=\"\r\n quickFilter()\r\n ? ((isTreeItemFiltered(node, field)\r\n ? 'filters.removeFilter'\r\n : 'filters.addFilter'\r\n ) | transloco) +\r\n ': ' +\r\n (treeItemName(node) | syslang)\r\n : (treeItemName(node) | syslang)\r\n \"\r\n (click)=\"onTreeItemTextClick(node, parent, $event)\">\r\n @for (\r\n chunk of (treeItemName(node) | syslang) ?? \"\"\r\n | highlightWord: searchText() : 10;\r\n track $index\r\n ) {\r\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\r\n chunk.text\r\n }}</span>\r\n }\r\n </span>\r\n @if (showCount() && node.count > 0) {\r\n <span class=\"ml-auto px-1 text-xs empty:hidden\" aria-hidden=\"true\">{{\r\n node.count\r\n }}</span>\r\n }\r\n }\r\n </a>\r\n @if (node.hasChildren && node.$opened) {\r\n <div role=\"group\">\r\n @for (child of node.items; track $index) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"tpl\"\r\n [ngTemplateOutletContext]=\"{\r\n node: child,\r\n path: treeChildrenPath(node, path),\r\n field: field,\r\n tpl: tpl,\r\n parent: node\r\n }\">\r\n </ng-container>\r\n }\r\n </div>\r\n }\r\n </div>\r\n</ng-template>\r\n", styles: ["div[role=treeitem]:has(+div[role=treeitem]){margin-bottom:var(--agg-item-gap, 0)}div[role=treeitem]{display:block;-webkit-user-select:none;user-select:none}div[role=treeitem] a{padding-left:calc(var(--agg-tree-indent, .5rem) * var(--level, 0));line-height:var(--agg-item-height, inherit)}\n"] }]
14736
+ }], ctorParameters: () => [], propDecorators: { class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }], column: [{ type: i0.Input, args: [{ isSignal: true, alias: "column", required: true }] }], collapsible: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsible", required: false }] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], showFiltersCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFiltersCount", required: false }] }], showCheckbox: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCheckbox", required: false }] }], expandedLevel: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandedLevel", required: false }] }], syncUrl: [{ type: i0.Input, args: [{ isSignal: true, alias: "syncUrl", required: false }] }], controlledQuery: [{ type: i0.Input, args: [{ isSignal: true, alias: "query", required: false }] }], controlledAggregation: [{ type: i0.Input, args: [{ isSignal: true, alias: "aggregation", required: false }] }], controlledSelection: [{ type: i0.Input, args: [{ isSignal: true, alias: "selection", required: false }] }], onSelect: [{ type: i0.Output, args: ["onSelect"] }], onApply: [{ type: i0.Output, args: ["onApply"] }], onClear: [{ type: i0.Output, args: ["onClear"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], filtersChange: [{ type: i0.Output, args: ["filtersChange"] }], virtualItems: [{ type: i0.ViewChildren, args: ["virtualItem", { isSignal: true }] }], scrollElement: [{ type: i0.ViewChild, args: ["scrollElement", { isSignal: true }] }], itemDirective: [{ type: i0.ContentChild, args: [i0.forwardRef(() => AggregationItemDirective), { isSignal: true }] }], itemTemplateInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemTemplate", required: false }] }], searchText: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchText", required: false }] }, { type: i0.Output, args: ["searchTextChange"] }] } });
14134
14737
 
14135
14738
  class AggregationListComponent {
14136
14739
  cn = cn;
@@ -14143,6 +14746,14 @@ class AggregationListComponent {
14143
14746
  collapsed = input(false, ...(ngDevMode ? [{ debugName: "collapsed" }] : []));
14144
14747
  searchable = input(undefined, ...(ngDevMode ? [{ debugName: "searchable" }] : []));
14145
14748
  showFiltersCount = input(null, ...(ngDevMode ? [{ debugName: "showFiltersCount", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
14749
+ /**
14750
+ * Whether to render the per-item checkbox. When `false`, the checkbox is hidden
14751
+ * but selection still toggles on row click — pair it with a custom `aggregationItem`
14752
+ * template that reflects the `selected` context to convey selection.
14753
+ *
14754
+ * @default true
14755
+ */
14756
+ showCheckbox = input(true, ...(ngDevMode ? [{ debugName: "showCheckbox", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
14146
14757
  /**
14147
14758
  * When `false`, applying or clearing filters updates the store without
14148
14759
  * synchronizing the URL (no browser history entry, not shareable/restorable).
@@ -14150,12 +14761,61 @@ class AggregationListComponent {
14150
14761
  * @default true
14151
14762
  */
14152
14763
  syncUrl = input(true, ...(ngDevMode ? [{ debugName: "syncUrl", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
14764
+ /**
14765
+ * Controlled query. When set, the component's own operations (load-more,
14766
+ * search-within-facet) run against this query instead of reading the shared
14767
+ * `QueryParamsStore`. Use to target a query not tied to the current app,
14768
+ * without mutating the shared store or the URL.
14769
+ *
14770
+ * @default undefined — read `QueryParamsStore.getQuery()` (current behavior)
14771
+ */
14772
+ controlledQuery = input(undefined, ...(ngDevMode ? [{ debugName: "controlledQuery", alias: "query" }] : [{ alias: "query" }]));
14773
+ /**
14774
+ * Controlled data. When set, the component renders this aggregation object
14775
+ * instead of reading it from the shared `AggregationsStore`.
14776
+ *
14777
+ * @default undefined — read `AggregationsStore` (current behavior)
14778
+ */
14779
+ controlledAggregation = input(undefined, ...(ngDevMode ? [{ debugName: "controlledAggregation", alias: "aggregation" }] : [{ alias: "aggregation" }]));
14780
+ /**
14781
+ * Controlled selection. When set, item `$selected` state is derived from this
14782
+ * value instead of the shared `QueryParamsStore` filters.
14783
+ *
14784
+ * @default undefined — derive from `QueryParamsStore` filters (current behavior)
14785
+ */
14786
+ controlledSelection = input(undefined, ...(ngDevMode ? [{ debugName: "controlledSelection", alias: "selection" }] : [{ alias: "selection" }]));
14153
14787
  /* outputs */
14154
14788
  onSelect = output();
14155
14789
  onApply = output();
14156
14790
  onClear = output();
14791
+ /**
14792
+ * Emits the new selection (item values) when the user applies or clears
14793
+ * filters in controlled mode. The counterpart of the `[selection]` input —
14794
+ * emitted before `filtersChange` and `onApply`/`onClear`. In controlled mode
14795
+ * this is the only way changes surface: nothing is written to the shared
14796
+ * stores or the URL.
14797
+ */
14798
+ selectionChange = output();
14799
+ /**
14800
+ * Emits the applied selection as a structured `Filter` (the exact filter the
14801
+ * default mode would write to the store), ready for `fetchQuery({ filters })`.
14802
+ * `undefined` on clear. Controlled mode only — emitted after `selectionChange`,
14803
+ * before `onApply`/`onClear`.
14804
+ */
14805
+ filtersChange = output();
14157
14806
  /* view queries */
14158
14807
  scrollElement = viewChild("scrollElement", ...(ngDevMode ? [{ debugName: "scrollElement" }] : []));
14808
+ /* optional host-provided template for the item content/label region */
14809
+ itemDirective = contentChild(AggregationItemDirective, ...(ngDevMode ? [{ debugName: "itemDirective" }] : []));
14810
+ /**
14811
+ * Item content template relayed by the `<Aggregation>` wrapper (which cannot
14812
+ * project content into this component). Also usable directly as a `TemplateRef`
14813
+ * input. A directive projected straight onto this component takes precedence.
14814
+ */
14815
+ itemTemplateInput = input(undefined, ...(ngDevMode ? [{ debugName: "itemTemplateInput", alias: "itemTemplate" }] : [{
14816
+ alias: "itemTemplate",
14817
+ }]));
14818
+ resolvedItemTemplate = computed(() => this.itemDirective()?.template ?? this.itemTemplateInput(), ...(ngDevMode ? [{ debugName: "resolvedItemTemplate" }] : []));
14159
14819
  /* selection state */
14160
14820
  selection = signal(false, ...(ngDevMode ? [{ debugName: "selection" }] : []));
14161
14821
  isAllSelected = signal(false, ...(ngDevMode ? [{ debugName: "isAllSelected" }] : []));
@@ -14167,8 +14827,12 @@ class AggregationListComponent {
14167
14827
  column: this.column,
14168
14828
  searchText: this.searchText,
14169
14829
  syncUrl: this.syncUrl,
14830
+ query: this.controlledQuery,
14831
+ aggregation: this.controlledAggregation,
14832
+ selection: this.controlledSelection,
14170
14833
  });
14171
14834
  /* spread from base */
14835
+ isControlled = this.base.isControlled;
14172
14836
  aggregationsStore = this.base.aggregationsStore;
14173
14837
  queryParamsStore = this.base.queryParamsStore;
14174
14838
  appStore = this.base.appStore;
@@ -14192,6 +14856,17 @@ class AggregationListComponent {
14192
14856
  scrollElement: this.scrollElement(),
14193
14857
  }));
14194
14858
  aggregation = computed(() => {
14859
+ if (this.isControlled()) {
14860
+ const agg = this.base.controlledAggregation();
14861
+ if (!agg)
14862
+ return null;
14863
+ if (agg.isTree) {
14864
+ error("The aggregation component does not support tree aggregations. Please use the <AggregationTree /> component instead.");
14865
+ }
14866
+ const enriched = this.aggregationsService.enrichAggregation(agg, this.base.selectionValues() ?? []);
14867
+ enriched.searchable = this.searchable() ?? enriched.searchable;
14868
+ return enriched;
14869
+ }
14195
14870
  getState(this.aggregationsStore);
14196
14871
  const name = this.name();
14197
14872
  const column = this.column();
@@ -14253,7 +14928,13 @@ class AggregationListComponent {
14253
14928
  clear() {
14254
14929
  const agg = this.aggregation();
14255
14930
  if (agg) {
14256
- this.queryParamsStore.removeFilterByName(agg.name, agg.column, { syncUrl: this.syncUrl() });
14931
+ if (this.isControlled()) {
14932
+ this.selectionChange.emit({ kind: "values", values: [] });
14933
+ this.filtersChange.emit(undefined);
14934
+ }
14935
+ else {
14936
+ this.queryParamsStore.removeFilterByName(agg.name, agg.column, { syncUrl: this.syncUrl() });
14937
+ }
14257
14938
  this.selection.set(false);
14258
14939
  this.isAllSelected.set(false);
14259
14940
  }
@@ -14278,12 +14959,34 @@ class AggregationListComponent {
14278
14959
  const agg = this.aggregation();
14279
14960
  if (!agg)
14280
14961
  return;
14281
- this.base.applyFilters(this.getFilters(), agg, () => this.clear());
14962
+ if (this.isControlled()) {
14963
+ const values = [...this.items(), ...this.searchedItems()]
14964
+ .filter((item) => item.$selected)
14965
+ .map((item) => String(item.value));
14966
+ this.selectionChange.emit({ kind: "values", values: Array.from(new Set(values)) });
14967
+ this.filtersChange.emit(legacyFilterToFilter(this.base.buildAppliedFilter(this.getFilters(), agg)));
14968
+ }
14969
+ else {
14970
+ this.base.applyFilters(this.getFilters(), agg, () => this.clear());
14971
+ }
14282
14972
  this.searchText.set("");
14283
14973
  this.selection.set(false);
14284
14974
  this.onApply.emit();
14285
14975
  }
14286
14976
  loadMore() {
14977
+ // Controlled mode loads into the local state — never the shared AggregationsStore.
14978
+ if (this.isControlled()) {
14979
+ const agg = this.base.controlledAggregation();
14980
+ if (!agg)
14981
+ return;
14982
+ this.aggregationsService
14983
+ .loadMore(this.base.resolvedQuery(), agg, undefined, { count: this.base.resolvedAggregationCount(agg) })
14984
+ .subscribe((aggregation) => {
14985
+ this.base.controlledAggregation.set(aggregation);
14986
+ this.cdr.detectChanges();
14987
+ });
14988
+ return;
14989
+ }
14287
14990
  const q = this.queryParamsStore.getQuery();
14288
14991
  this.aggregationsService.loadMore(q, this.aggregation()).subscribe((aggregation) => {
14289
14992
  this.aggregationsStore.updateAggregation(aggregation);
@@ -14301,6 +15004,9 @@ class AggregationListComponent {
14301
15004
  return typeof value === "string" ? value : `${value}`;
14302
15005
  }
14303
15006
  isListItemFiltered(item, field) {
15007
+ if (this.isControlled()) {
15008
+ return (this.base.selectionValues() ?? []).some((value) => value === item.value);
15009
+ }
14304
15010
  const filters = this.queryParamsStore.getFilter({ field: field ?? undefined, name: this.listItemName(item) });
14305
15011
  if (!filters)
14306
15012
  return false;
@@ -14330,7 +15036,7 @@ class AggregationListComponent {
14330
15036
  return this.base.addCurrentFiltersToItems(this.aggregation());
14331
15037
  }
14332
15038
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: AggregationListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
14333
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: AggregationListComponent, isStandalone: true, selector: "AggregationList, aggregation-list, aggregationlist", inputs: { class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null }, column: { classPropertyName: "column", publicName: "column", isSignal: true, isRequired: true, transformFunction: null }, collapsible: { classPropertyName: "collapsible", publicName: "collapsible", isSignal: true, isRequired: false, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, searchable: { classPropertyName: "searchable", publicName: "searchable", isSignal: true, isRequired: false, transformFunction: null }, showFiltersCount: { classPropertyName: "showFiltersCount", publicName: "showFiltersCount", isSignal: true, isRequired: false, transformFunction: null }, syncUrl: { classPropertyName: "syncUrl", publicName: "syncUrl", isSignal: true, isRequired: false, transformFunction: null }, searchText: { classPropertyName: "searchText", publicName: "searchText", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSelect: "onSelect", onApply: "onApply", onClear: "onClear", searchText: "searchTextChange" }, host: { properties: { "class": "cn(\"block h-[inherit] max-h-[inherit]\", class())" } }, viewQueries: [{ propertyName: "scrollElement", first: true, predicate: ["scrollElement"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (aggregation()?.isTree) {\r\n <div class=\"p-2 text-sm text-red-500\">\r\n <triangle-alert-icon class=\"mr-1\" />\r\n The aggregation component no longer supports tree aggregations. Please use\r\n the &lt;AggregationTree /&gt; component instead.\r\n </div>\r\n}\r\n<aggregation-panel\r\n [id]=\"id()\"\r\n [collapsible]=\"collapsible()\"\r\n [collapsed]=\"collapsed()\"\r\n [isEmpty]=\"isEmpty()\"\r\n [isDate]=\"isDate()\"\r\n [aggregation]=\"aggregation()\"\r\n [showFiltersCount]=\"showFiltersCount()\"\r\n [filtersCount]=\"filtersCount()\"\r\n [hasFilters]=\"hasFilters()\"\r\n [selection]=\"selection()\"\r\n [isAllSelected]=\"isAllSelected()\"\r\n [(searchText)]=\"searchText\"\r\n [itemsLength]=\"items().length\"\r\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\r\n [searchedItemsLength]=\"searchedItems().length\"\r\n (cleared)=\"clear()\"\r\n (applied)=\"apply()\"\r\n (allSelected)=\"selectAll()\"\r\n (allUnselected)=\"unselectAll()\"\r\n (loadedMore)=\"loadMore()\">\r\n <div\r\n #scrollElement\r\n class=\"scrollbar-thin max-h-(--scroll-height,20rem) w-full overflow-auto\">\r\n <div\r\n class=\"relative w-full\"\r\n [style.height]=\"virtualizer.getTotalSize() + 'px'\"\r\n role=\"listbox\"\r\n aria-multiselectable=\"true\"\r\n [attr.aria-label]=\"aggregation()?.display | syslang | transloco\">\r\n @for (vItem of virtualizer.getVirtualItems(); track vItem.index) {\r\n @let item = items()[vItem.index];\r\n <div\r\n class=\"absolute w-full\"\r\n [style.transform]=\"'translateY(' + vItem.start + 'px)'\"\r\n role=\"option\"\r\n [attr.aria-selected]=\"item.$selected ?? false\"\r\n [attr.aria-disabled]=\"item.count === 0 ? 'true' : null\"\r\n [attr.disabled]=\"item.count === 0 ? 'disabled' : null\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"listItemTpl\"\r\n [ngTemplateOutletContext]=\"{\r\n $implicit: item,\r\n field: aggregation()?.column\r\n }\">\r\n </ng-container>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n</aggregation-panel>\r\n\r\n<ng-template #listItemTpl let-item let-field=\"field\">\r\n <a\r\n [attr.aria-label]=\"listItemName(item) | syslang\"\r\n [class]=\"\r\n cn(\r\n 'flex grow items-center gap-2 p-1 leading-7',\r\n item.count === 0 && 'disabled pointer-events-none'\r\n )\r\n \"\r\n (click)=\"selectListItem(item, $event)\">\r\n <input\r\n type=\"checkbox\"\r\n role=\"checkbox\"\r\n value=\"{{ item.value }}\"\r\n [attr.disabled]=\"item.count === 0 ? true : null\"\r\n [attr.aria-disabled]=\"item.count === 0\"\r\n (keydown.enter)=\"selectListItem(item, $event)\"\r\n [checked]=\"item.$selected\" />\r\n @let icon = item.icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"self-center justify-self-center\" />\r\n }\r\n <span\r\n [class]=\"\r\n cn(\r\n 'line-clamp-1 break-all text-ellipsis',\r\n quickFilter() && 'hover:underline'\r\n )\r\n \"\r\n [title]=\"\r\n quickFilter()\r\n ? ((isListItemFiltered(item, field)\r\n ? 'filters.removeFilter'\r\n : 'filters.addFilter'\r\n ) | transloco) +\r\n ': ' +\r\n (listItemName(item) | syslang)\r\n : (listItemName(item) | syslang)\r\n \"\r\n (click)=\"onListItemTextClick(item, $event)\">\r\n @for (\r\n chunk of (listItemName(item) | syslang) ?? \"\"\r\n | highlightWord: searchText() : 10;\r\n track $index\r\n ) {\r\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\r\n chunk.text\r\n }}</span>\r\n }\r\n </span>\r\n @if (showCount() && item.count > 0) {\r\n <span class=\"ml-auto px-1 text-xs empty:hidden\" aria-hidden=\"true\">{{\r\n item.count\r\n }}</span>\r\n }\r\n </a>\r\n</ng-template>\r\n", styles: ["div[role=option]{display:block;-webkit-user-select:none;user-select:none}div[role=option] a{padding-left:var(--agg-tree-indent, .5rem);line-height:var(--agg-item-height, inherit)}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "component", type: FaIconComponent, selector: "fa-icon, FaIcon", inputs: ["faClass", "class"] }, { kind: "component", type: TriangleAlertIcon, selector: "triangle-alert-icon, TriangleAlertIcon", inputs: ["class"] }, { kind: "component", type: AggregationPanelComponent, selector: "AggregationPanel, aggregation-panel", inputs: ["id", "collapsible", "collapsed", "isDate", "isEmpty", "aggregation", "showFiltersCount", "filtersCount", "hasFilters", "selection", "isAllSelected", "searchText", "itemsLength", "hasMore", "searchedItemsLength"], outputs: ["searchTextChange", "cleared", "applied", "allSelected", "allUnselected", "loadedMore"] }, { kind: "pipe", type: SyslangPipe, name: "syslang" }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }, { kind: "pipe", type: HighlightWordPipe, name: "highlightWord" }] });
15039
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: AggregationListComponent, isStandalone: true, selector: "AggregationList, aggregation-list, aggregationlist", inputs: { class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null }, column: { classPropertyName: "column", publicName: "column", isSignal: true, isRequired: true, transformFunction: null }, collapsible: { classPropertyName: "collapsible", publicName: "collapsible", isSignal: true, isRequired: false, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, searchable: { classPropertyName: "searchable", publicName: "searchable", isSignal: true, isRequired: false, transformFunction: null }, showFiltersCount: { classPropertyName: "showFiltersCount", publicName: "showFiltersCount", isSignal: true, isRequired: false, transformFunction: null }, showCheckbox: { classPropertyName: "showCheckbox", publicName: "showCheckbox", isSignal: true, isRequired: false, transformFunction: null }, syncUrl: { classPropertyName: "syncUrl", publicName: "syncUrl", isSignal: true, isRequired: false, transformFunction: null }, controlledQuery: { classPropertyName: "controlledQuery", publicName: "query", isSignal: true, isRequired: false, transformFunction: null }, controlledAggregation: { classPropertyName: "controlledAggregation", publicName: "aggregation", isSignal: true, isRequired: false, transformFunction: null }, controlledSelection: { classPropertyName: "controlledSelection", publicName: "selection", isSignal: true, isRequired: false, transformFunction: null }, itemTemplateInput: { classPropertyName: "itemTemplateInput", publicName: "itemTemplate", isSignal: true, isRequired: false, transformFunction: null }, searchText: { classPropertyName: "searchText", publicName: "searchText", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSelect: "onSelect", onApply: "onApply", onClear: "onClear", selectionChange: "selectionChange", filtersChange: "filtersChange", searchText: "searchTextChange" }, host: { properties: { "class": "cn(\"block h-[inherit] max-h-[inherit]\", class())" } }, queries: [{ propertyName: "itemDirective", first: true, predicate: AggregationItemDirective, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "scrollElement", first: true, predicate: ["scrollElement"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (aggregation()?.isTree) {\r\n <div class=\"p-2 text-sm text-red-500\">\r\n <triangle-alert-icon class=\"mr-1\" />\r\n The aggregation component no longer supports tree aggregations. Please use\r\n the &lt;AggregationTree /&gt; component instead.\r\n </div>\r\n}\r\n<aggregation-panel\r\n [id]=\"id()\"\r\n [collapsible]=\"collapsible()\"\r\n [collapsed]=\"collapsed()\"\r\n [isEmpty]=\"isEmpty()\"\r\n [isDate]=\"isDate()\"\r\n [aggregation]=\"aggregation()\"\r\n [showFiltersCount]=\"showFiltersCount()\"\r\n [filtersCount]=\"filtersCount()\"\r\n [hasFilters]=\"hasFilters()\"\r\n [selection]=\"selection()\"\r\n [isAllSelected]=\"isAllSelected()\"\r\n [(searchText)]=\"searchText\"\r\n [itemsLength]=\"items().length\"\r\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\r\n [searchedItemsLength]=\"searchedItems().length\"\r\n (cleared)=\"clear()\"\r\n (applied)=\"apply()\"\r\n (allSelected)=\"selectAll()\"\r\n (allUnselected)=\"unselectAll()\"\r\n (loadedMore)=\"loadMore()\">\r\n <div\r\n #scrollElement\r\n class=\"max-h-(--scroll-height,20rem) w-full scrollbar-thin overflow-auto\">\r\n <div\r\n class=\"relative w-full\"\r\n [style.height]=\"virtualizer.getTotalSize() + 'px'\"\r\n role=\"listbox\"\r\n aria-multiselectable=\"true\"\r\n [attr.aria-label]=\"aggregation()?.display | syslang | transloco\">\r\n @for (vItem of virtualizer.getVirtualItems(); track vItem.index) {\r\n @let item = items()[vItem.index];\r\n <div\r\n class=\"absolute w-full\"\r\n [style.transform]=\"'translateY(' + vItem.start + 'px)'\"\r\n role=\"option\"\r\n [attr.aria-selected]=\"item.$selected ?? false\"\r\n [attr.aria-disabled]=\"item.count === 0 ? 'true' : null\"\r\n [attr.disabled]=\"item.count === 0 ? 'disabled' : null\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"listItemTpl\"\r\n [ngTemplateOutletContext]=\"{\r\n $implicit: item,\r\n field: aggregation()?.column\r\n }\">\r\n </ng-container>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n</aggregation-panel>\r\n\r\n<ng-template #listItemTpl let-item let-field=\"field\">\r\n <a\r\n [attr.aria-label]=\"listItemName(item) | syslang\"\r\n [class]=\"\r\n cn(\r\n 'flex grow items-center gap-2 p-1 leading-7',\r\n item.count === 0 && 'disabled pointer-events-none'\r\n )\r\n \"\r\n (click)=\"selectListItem(item, $event)\">\r\n @if (showCheckbox()) {\r\n <input\r\n type=\"checkbox\"\r\n role=\"checkbox\"\r\n value=\"{{ item.value }}\"\r\n [attr.disabled]=\"item.count === 0 ? true : null\"\r\n [attr.aria-disabled]=\"item.count === 0\"\r\n (keydown.enter)=\"selectListItem(item, $event)\"\r\n [checked]=\"item.$selected\" />\r\n }\r\n @if (resolvedItemTemplate(); as tpl) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"tpl\"\r\n [ngTemplateOutletContext]=\"{\r\n $implicit: item,\r\n field: field,\r\n selected: item.$selected ?? false,\r\n count: item.count,\r\n name: (listItemName(item) | syslang) ?? '',\r\n searchText: searchText()\r\n }\" />\r\n } @else {\r\n @let icon = item.icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"self-center justify-self-center\" />\r\n }\r\n <span\r\n [class]=\"\r\n cn(\r\n 'line-clamp-1 break-all text-ellipsis',\r\n quickFilter() && 'hover:underline'\r\n )\r\n \"\r\n [title]=\"\r\n quickFilter()\r\n ? ((isListItemFiltered(item, field)\r\n ? 'filters.removeFilter'\r\n : 'filters.addFilter'\r\n ) | transloco) +\r\n ': ' +\r\n (listItemName(item) | syslang)\r\n : (listItemName(item) | syslang)\r\n \"\r\n (click)=\"onListItemTextClick(item, $event)\">\r\n @for (\r\n chunk of (listItemName(item) | syslang) ?? \"\"\r\n | highlightWord: searchText() : 10;\r\n track $index\r\n ) {\r\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\r\n chunk.text\r\n }}</span>\r\n }\r\n </span>\r\n @if (showCount() && item.count > 0) {\r\n <span class=\"ml-auto px-1 text-xs empty:hidden\" aria-hidden=\"true\">{{\r\n item.count\r\n }}</span>\r\n }\r\n }\r\n </a>\r\n</ng-template>\r\n", styles: ["div[role=option]{display:block;-webkit-user-select:none;user-select:none}div[role=option] a{padding-left:var(--agg-tree-indent, .5rem);line-height:var(--agg-item-height, inherit)}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "component", type: FaIconComponent, selector: "fa-icon, FaIcon", inputs: ["faClass", "class"] }, { kind: "component", type: TriangleAlertIcon, selector: "triangle-alert-icon, TriangleAlertIcon", inputs: ["class"] }, { kind: "component", type: AggregationPanelComponent, selector: "AggregationPanel, aggregation-panel", inputs: ["id", "collapsible", "collapsed", "isDate", "isEmpty", "aggregation", "showFiltersCount", "filtersCount", "hasFilters", "selection", "isAllSelected", "searchText", "itemsLength", "hasMore", "searchedItemsLength"], outputs: ["searchTextChange", "cleared", "applied", "allSelected", "allUnselected", "loadedMore"] }, { kind: "pipe", type: SyslangPipe, name: "syslang" }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }, { kind: "pipe", type: HighlightWordPipe, name: "highlightWord" }] });
14334
15040
  }
14335
15041
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: AggregationListComponent, decorators: [{
14336
15042
  type: Component,
@@ -14346,8 +15052,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
14346
15052
  AggregationPanelComponent,
14347
15053
  ], standalone: true, host: {
14348
15054
  "[class]": 'cn("block h-[inherit] max-h-[inherit]", class())',
14349
- }, template: "@if (aggregation()?.isTree) {\r\n <div class=\"p-2 text-sm text-red-500\">\r\n <triangle-alert-icon class=\"mr-1\" />\r\n The aggregation component no longer supports tree aggregations. Please use\r\n the &lt;AggregationTree /&gt; component instead.\r\n </div>\r\n}\r\n<aggregation-panel\r\n [id]=\"id()\"\r\n [collapsible]=\"collapsible()\"\r\n [collapsed]=\"collapsed()\"\r\n [isEmpty]=\"isEmpty()\"\r\n [isDate]=\"isDate()\"\r\n [aggregation]=\"aggregation()\"\r\n [showFiltersCount]=\"showFiltersCount()\"\r\n [filtersCount]=\"filtersCount()\"\r\n [hasFilters]=\"hasFilters()\"\r\n [selection]=\"selection()\"\r\n [isAllSelected]=\"isAllSelected()\"\r\n [(searchText)]=\"searchText\"\r\n [itemsLength]=\"items().length\"\r\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\r\n [searchedItemsLength]=\"searchedItems().length\"\r\n (cleared)=\"clear()\"\r\n (applied)=\"apply()\"\r\n (allSelected)=\"selectAll()\"\r\n (allUnselected)=\"unselectAll()\"\r\n (loadedMore)=\"loadMore()\">\r\n <div\r\n #scrollElement\r\n class=\"scrollbar-thin max-h-(--scroll-height,20rem) w-full overflow-auto\">\r\n <div\r\n class=\"relative w-full\"\r\n [style.height]=\"virtualizer.getTotalSize() + 'px'\"\r\n role=\"listbox\"\r\n aria-multiselectable=\"true\"\r\n [attr.aria-label]=\"aggregation()?.display | syslang | transloco\">\r\n @for (vItem of virtualizer.getVirtualItems(); track vItem.index) {\r\n @let item = items()[vItem.index];\r\n <div\r\n class=\"absolute w-full\"\r\n [style.transform]=\"'translateY(' + vItem.start + 'px)'\"\r\n role=\"option\"\r\n [attr.aria-selected]=\"item.$selected ?? false\"\r\n [attr.aria-disabled]=\"item.count === 0 ? 'true' : null\"\r\n [attr.disabled]=\"item.count === 0 ? 'disabled' : null\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"listItemTpl\"\r\n [ngTemplateOutletContext]=\"{\r\n $implicit: item,\r\n field: aggregation()?.column\r\n }\">\r\n </ng-container>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n</aggregation-panel>\r\n\r\n<ng-template #listItemTpl let-item let-field=\"field\">\r\n <a\r\n [attr.aria-label]=\"listItemName(item) | syslang\"\r\n [class]=\"\r\n cn(\r\n 'flex grow items-center gap-2 p-1 leading-7',\r\n item.count === 0 && 'disabled pointer-events-none'\r\n )\r\n \"\r\n (click)=\"selectListItem(item, $event)\">\r\n <input\r\n type=\"checkbox\"\r\n role=\"checkbox\"\r\n value=\"{{ item.value }}\"\r\n [attr.disabled]=\"item.count === 0 ? true : null\"\r\n [attr.aria-disabled]=\"item.count === 0\"\r\n (keydown.enter)=\"selectListItem(item, $event)\"\r\n [checked]=\"item.$selected\" />\r\n @let icon = item.icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"self-center justify-self-center\" />\r\n }\r\n <span\r\n [class]=\"\r\n cn(\r\n 'line-clamp-1 break-all text-ellipsis',\r\n quickFilter() && 'hover:underline'\r\n )\r\n \"\r\n [title]=\"\r\n quickFilter()\r\n ? ((isListItemFiltered(item, field)\r\n ? 'filters.removeFilter'\r\n : 'filters.addFilter'\r\n ) | transloco) +\r\n ': ' +\r\n (listItemName(item) | syslang)\r\n : (listItemName(item) | syslang)\r\n \"\r\n (click)=\"onListItemTextClick(item, $event)\">\r\n @for (\r\n chunk of (listItemName(item) | syslang) ?? \"\"\r\n | highlightWord: searchText() : 10;\r\n track $index\r\n ) {\r\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\r\n chunk.text\r\n }}</span>\r\n }\r\n </span>\r\n @if (showCount() && item.count > 0) {\r\n <span class=\"ml-auto px-1 text-xs empty:hidden\" aria-hidden=\"true\">{{\r\n item.count\r\n }}</span>\r\n }\r\n </a>\r\n</ng-template>\r\n", styles: ["div[role=option]{display:block;-webkit-user-select:none;user-select:none}div[role=option] a{padding-left:var(--agg-tree-indent, .5rem);line-height:var(--agg-item-height, inherit)}\n"] }]
14350
- }], ctorParameters: () => [], propDecorators: { class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }], column: [{ type: i0.Input, args: [{ isSignal: true, alias: "column", required: true }] }], collapsible: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsible", required: false }] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], showFiltersCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFiltersCount", required: false }] }], syncUrl: [{ type: i0.Input, args: [{ isSignal: true, alias: "syncUrl", required: false }] }], onSelect: [{ type: i0.Output, args: ["onSelect"] }], onApply: [{ type: i0.Output, args: ["onApply"] }], onClear: [{ type: i0.Output, args: ["onClear"] }], scrollElement: [{ type: i0.ViewChild, args: ["scrollElement", { isSignal: true }] }], searchText: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchText", required: false }] }, { type: i0.Output, args: ["searchTextChange"] }] } });
15055
+ }, template: "@if (aggregation()?.isTree) {\r\n <div class=\"p-2 text-sm text-red-500\">\r\n <triangle-alert-icon class=\"mr-1\" />\r\n The aggregation component no longer supports tree aggregations. Please use\r\n the &lt;AggregationTree /&gt; component instead.\r\n </div>\r\n}\r\n<aggregation-panel\r\n [id]=\"id()\"\r\n [collapsible]=\"collapsible()\"\r\n [collapsed]=\"collapsed()\"\r\n [isEmpty]=\"isEmpty()\"\r\n [isDate]=\"isDate()\"\r\n [aggregation]=\"aggregation()\"\r\n [showFiltersCount]=\"showFiltersCount()\"\r\n [filtersCount]=\"filtersCount()\"\r\n [hasFilters]=\"hasFilters()\"\r\n [selection]=\"selection()\"\r\n [isAllSelected]=\"isAllSelected()\"\r\n [(searchText)]=\"searchText\"\r\n [itemsLength]=\"items().length\"\r\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\r\n [searchedItemsLength]=\"searchedItems().length\"\r\n (cleared)=\"clear()\"\r\n (applied)=\"apply()\"\r\n (allSelected)=\"selectAll()\"\r\n (allUnselected)=\"unselectAll()\"\r\n (loadedMore)=\"loadMore()\">\r\n <div\r\n #scrollElement\r\n class=\"max-h-(--scroll-height,20rem) w-full scrollbar-thin overflow-auto\">\r\n <div\r\n class=\"relative w-full\"\r\n [style.height]=\"virtualizer.getTotalSize() + 'px'\"\r\n role=\"listbox\"\r\n aria-multiselectable=\"true\"\r\n [attr.aria-label]=\"aggregation()?.display | syslang | transloco\">\r\n @for (vItem of virtualizer.getVirtualItems(); track vItem.index) {\r\n @let item = items()[vItem.index];\r\n <div\r\n class=\"absolute w-full\"\r\n [style.transform]=\"'translateY(' + vItem.start + 'px)'\"\r\n role=\"option\"\r\n [attr.aria-selected]=\"item.$selected ?? false\"\r\n [attr.aria-disabled]=\"item.count === 0 ? 'true' : null\"\r\n [attr.disabled]=\"item.count === 0 ? 'disabled' : null\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"listItemTpl\"\r\n [ngTemplateOutletContext]=\"{\r\n $implicit: item,\r\n field: aggregation()?.column\r\n }\">\r\n </ng-container>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n</aggregation-panel>\r\n\r\n<ng-template #listItemTpl let-item let-field=\"field\">\r\n <a\r\n [attr.aria-label]=\"listItemName(item) | syslang\"\r\n [class]=\"\r\n cn(\r\n 'flex grow items-center gap-2 p-1 leading-7',\r\n item.count === 0 && 'disabled pointer-events-none'\r\n )\r\n \"\r\n (click)=\"selectListItem(item, $event)\">\r\n @if (showCheckbox()) {\r\n <input\r\n type=\"checkbox\"\r\n role=\"checkbox\"\r\n value=\"{{ item.value }}\"\r\n [attr.disabled]=\"item.count === 0 ? true : null\"\r\n [attr.aria-disabled]=\"item.count === 0\"\r\n (keydown.enter)=\"selectListItem(item, $event)\"\r\n [checked]=\"item.$selected\" />\r\n }\r\n @if (resolvedItemTemplate(); as tpl) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"tpl\"\r\n [ngTemplateOutletContext]=\"{\r\n $implicit: item,\r\n field: field,\r\n selected: item.$selected ?? false,\r\n count: item.count,\r\n name: (listItemName(item) | syslang) ?? '',\r\n searchText: searchText()\r\n }\" />\r\n } @else {\r\n @let icon = item.icon;\r\n @if (icon) {\r\n <FaIcon [faClass]=\"icon\" class=\"self-center justify-self-center\" />\r\n }\r\n <span\r\n [class]=\"\r\n cn(\r\n 'line-clamp-1 break-all text-ellipsis',\r\n quickFilter() && 'hover:underline'\r\n )\r\n \"\r\n [title]=\"\r\n quickFilter()\r\n ? ((isListItemFiltered(item, field)\r\n ? 'filters.removeFilter'\r\n : 'filters.addFilter'\r\n ) | transloco) +\r\n ': ' +\r\n (listItemName(item) | syslang)\r\n : (listItemName(item) | syslang)\r\n \"\r\n (click)=\"onListItemTextClick(item, $event)\">\r\n @for (\r\n chunk of (listItemName(item) | syslang) ?? \"\"\r\n | highlightWord: searchText() : 10;\r\n track $index\r\n ) {\r\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\r\n chunk.text\r\n }}</span>\r\n }\r\n </span>\r\n @if (showCount() && item.count > 0) {\r\n <span class=\"ml-auto px-1 text-xs empty:hidden\" aria-hidden=\"true\">{{\r\n item.count\r\n }}</span>\r\n }\r\n }\r\n </a>\r\n</ng-template>\r\n", styles: ["div[role=option]{display:block;-webkit-user-select:none;user-select:none}div[role=option] a{padding-left:var(--agg-tree-indent, .5rem);line-height:var(--agg-item-height, inherit)}\n"] }]
15056
+ }], ctorParameters: () => [], propDecorators: { class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }], column: [{ type: i0.Input, args: [{ isSignal: true, alias: "column", required: true }] }], collapsible: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsible", required: false }] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], showFiltersCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFiltersCount", required: false }] }], showCheckbox: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCheckbox", required: false }] }], syncUrl: [{ type: i0.Input, args: [{ isSignal: true, alias: "syncUrl", required: false }] }], controlledQuery: [{ type: i0.Input, args: [{ isSignal: true, alias: "query", required: false }] }], controlledAggregation: [{ type: i0.Input, args: [{ isSignal: true, alias: "aggregation", required: false }] }], controlledSelection: [{ type: i0.Input, args: [{ isSignal: true, alias: "selection", required: false }] }], onSelect: [{ type: i0.Output, args: ["onSelect"] }], onApply: [{ type: i0.Output, args: ["onApply"] }], onClear: [{ type: i0.Output, args: ["onClear"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], filtersChange: [{ type: i0.Output, args: ["filtersChange"] }], scrollElement: [{ type: i0.ViewChild, args: ["scrollElement", { isSignal: true }] }], itemDirective: [{ type: i0.ContentChild, args: [i0.forwardRef(() => AggregationItemDirective), { isSignal: true }] }], itemTemplateInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemTemplate", required: false }] }], searchText: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchText", required: false }] }, { type: i0.Output, args: ["searchTextChange"] }] } });
14351
15057
 
14352
15058
  /**
14353
15059
  * The `AggregationComponent` is responsible for rendering the appropriate aggregation component based on the type of aggregation (date, tree, or list). It uses the `AggregationsService` to retrieve the aggregation data and determines which component to display based on the column type and whether it is a tree structure. The component also provides inputs for configuring its behavior, such as collapsibility, searchability, and showing filter counts.
@@ -14372,6 +15078,12 @@ class AggregationComponent {
14372
15078
  aggregationDate = viewChild(AggregationDateComponent, ...(ngDevMode ? [{ debugName: "aggregationDate" }] : []));
14373
15079
  aggregationTree = viewChild(AggregationTreeComponent, ...(ngDevMode ? [{ debugName: "aggregationTree" }] : []));
14374
15080
  aggregationList = viewChild(AggregationListComponent, ...(ngDevMode ? [{ debugName: "aggregationList" }] : []));
15081
+ /**
15082
+ * Host-projected `<ng-template aggregationItem>`, relayed to the active list/tree
15083
+ * sub-component (the wrapper has no `<ng-content>`, so the sub-component's own
15084
+ * `contentChild` cannot see it). See {@link AggregationItemDirective}.
15085
+ */
15086
+ itemDirective = contentChild(AggregationItemDirective, ...(ngDevMode ? [{ debugName: "itemDirective" }] : []));
14375
15087
  class = input("", ...(ngDevMode ? [{ debugName: "class" }] : []));
14376
15088
  /**
14377
15089
  * The name of the <details> element. When you provide the same id, the component work as an accordion
@@ -14415,6 +15127,13 @@ class AggregationComponent {
14415
15127
  * "Undefined" and not "false" because this input overrides the custom json settings
14416
15128
  */
14417
15129
  searchable = input(undefined, ...(ngDevMode ? [{ debugName: "searchable" }] : []));
15130
+ /**
15131
+ * Whether to render the per-item checkbox (list/tree). When `false`, the checkbox
15132
+ * is hidden but selection still toggles on row click. Ignored by the date variant.
15133
+ *
15134
+ * @default true
15135
+ */
15136
+ showCheckbox = input(true, ...(ngDevMode ? [{ debugName: "showCheckbox", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
14418
15137
  expandedLevel = input(undefined, ...(ngDevMode ? [{ debugName: "expandedLevel", transform: numberAttribute }] : [{ transform: numberAttribute }]));
14419
15138
  /**
14420
15139
  * Controls whether applying or clearing a filter synchronizes the URL.
@@ -14426,12 +15145,64 @@ class AggregationComponent {
14426
15145
  * @default true
14427
15146
  */
14428
15147
  syncUrl = input(true, ...(ngDevMode ? [{ debugName: "syncUrl", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
15148
+ /**
15149
+ * Controlled query. When set, the aggregation's own operations (load-more,
15150
+ * search-within-facet, tree-expand) run against this query instead of reading
15151
+ * the shared `QueryParamsStore`. Use to target a query not tied to the current
15152
+ * app, without mutating the shared store or the URL.
15153
+ *
15154
+ * @default undefined — read `QueryParamsStore.getQuery()` (current behavior)
15155
+ */
15156
+ controlledQuery = input(undefined, ...(ngDevMode ? [{ debugName: "controlledQuery", alias: "query" }] : [{ alias: "query" }]));
15157
+ /**
15158
+ * Controlled data. When set, the component renders this aggregation object
15159
+ * (name/column/items) instead of reading it from the shared `AggregationsStore`.
15160
+ *
15161
+ * @default undefined — read `AggregationsStore` (current behavior)
15162
+ */
15163
+ controlledAggregation = input(undefined, ...(ngDevMode ? [{ debugName: "controlledAggregation", alias: "aggregation" }] : [{ alias: "aggregation" }]));
15164
+ /**
15165
+ * Controlled selection. When set, item `$selected` state is derived from this
15166
+ * value (list: item values; tree: `"/A/B/*"` paths; date: the range) instead
15167
+ * of the shared `QueryParamsStore` filters.
15168
+ *
15169
+ * @default undefined — derive from `QueryParamsStore` filters (current behavior)
15170
+ */
15171
+ controlledSelection = input(undefined, ...(ngDevMode ? [{ debugName: "controlledSelection", alias: "selection" }] : [{ alias: "selection" }]));
15172
+ /**
15173
+ * Which sub-component renders the aggregation.
15174
+ *
15175
+ * `"auto"` (default) keeps the current heuristic: a date column of the app
15176
+ * (`AppStore.isDateColumn`) renders `<AggregationDate>`, a tree aggregation
15177
+ * renders `<AggregationTree>`, anything else `<AggregationList>`. Set an
15178
+ * explicit value in controlled mode when the heuristic cannot apply — e.g. a
15179
+ * date facet of a query not attached to the current app, whose column is
15180
+ * unknown to the app's column map.
15181
+ *
15182
+ * @default "auto"
15183
+ */
15184
+ type = input("auto", ...(ngDevMode ? [{ debugName: "type" }] : []));
14429
15185
  /**
14430
15186
  * An output event that emits an array of selected `AggregationItem` objects when the user selects items in the aggregation component. This allows parent components to listen for selection changes and react accordingly, such as applying filters or updating the UI based on the selected items.
14431
15187
  */
14432
15188
  onSelect = output();
14433
15189
  onApply = output();
14434
15190
  onClear = output();
15191
+ /**
15192
+ * Emits the new selection when the user applies or clears filters in
15193
+ * controlled mode (list: item values; tree: `"/A/B/*"` paths; date: option or
15194
+ * range). The counterpart of the `[selection]` input — emitted before
15195
+ * `filtersChange` and `onApply`/`onClear`. In controlled mode this is the only
15196
+ * way changes surface: nothing is written to the shared stores or the URL.
15197
+ */
15198
+ selectionChange = output();
15199
+ /**
15200
+ * Emits the applied selection as a structured `Filter` (the exact filter the
15201
+ * default mode would write to the store), ready for `fetchQuery({ filters })`.
15202
+ * `undefined` on clear. Controlled mode only — emitted after `selectionChange`,
15203
+ * before `onApply`/`onClear`.
15204
+ */
15205
+ filtersChange = output();
14435
15206
  /**
14436
15207
  * A computed property that retrieves the items to be displayed in the aggregation component. It checks for the presence of date, tree, or list aggregation components and returns their respective items. If none of these components are present, it returns an empty array. This allows the component to dynamically display the appropriate items based on the type of aggregation being rendered.
14437
15208
  *
@@ -14449,6 +15220,10 @@ class AggregationComponent {
14449
15220
  selectedItems = computed(() => this.items().filter(item => item.$selected), ...(ngDevMode ? [{ debugName: "selectedItems" }] : []));
14450
15221
  /* aggregation */
14451
15222
  aggregation = computed(() => {
15223
+ // controlled mode: the input is the data source — the store is not read
15224
+ const controlled = this.controlledAggregation();
15225
+ if (controlled)
15226
+ return controlled;
14452
15227
  // when the aggegation store updates, we need to check if the aggregation is still valid
14453
15228
  getState(this.aggregationsStore);
14454
15229
  const agg = this.aggregationsService.getAggregation(this.name(), this.column());
@@ -14458,6 +15233,20 @@ class AggregationComponent {
14458
15233
  }
14459
15234
  return agg;
14460
15235
  }, ...(ngDevMode ? [{ debugName: "aggregation" }] : []));
15236
+ /**
15237
+ * The sub-component to render: the explicit `type` input, or the current
15238
+ * date/tree/list heuristic when `type` is `"auto"`.
15239
+ */
15240
+ resolvedType = computed(() => {
15241
+ const type = this.type();
15242
+ if (type !== "auto")
15243
+ return type;
15244
+ if (this.isDate(this.aggregation()?.column))
15245
+ return "date";
15246
+ if (this.aggregation()?.isTree)
15247
+ return "tree";
15248
+ return "list";
15249
+ }, ...(ngDevMode ? [{ debugName: "resolvedType" }] : []));
14461
15250
  /**
14462
15251
  * Checks if the specified column is a date filter.
14463
15252
  *
@@ -14471,8 +15260,9 @@ class AggregationComponent {
14471
15260
  return this.appStore.isDateColumn(column);
14472
15261
  }
14473
15262
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: AggregationComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
14474
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: AggregationComponent, isStandalone: true, selector: "Aggregation, aggregation", inputs: { class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null }, column: { classPropertyName: "column", publicName: "column", isSignal: true, isRequired: true, transformFunction: null }, showFiltersCount: { classPropertyName: "showFiltersCount", publicName: "showFiltersCount", isSignal: true, isRequired: false, transformFunction: null }, collapsible: { classPropertyName: "collapsible", publicName: "collapsible", isSignal: true, isRequired: false, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, searchable: { classPropertyName: "searchable", publicName: "searchable", isSignal: true, isRequired: false, transformFunction: null }, expandedLevel: { classPropertyName: "expandedLevel", publicName: "expandedLevel", isSignal: true, isRequired: false, transformFunction: null }, syncUrl: { classPropertyName: "syncUrl", publicName: "syncUrl", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSelect: "onSelect", onApply: "onApply", onClear: "onClear" }, host: { properties: { "class": "cn(\"block h-[inherit] max-h-[inherit]\",class())" } }, viewQueries: [{ propertyName: "aggregationDate", first: true, predicate: AggregationDateComponent, descendants: true, isSignal: true }, { propertyName: "aggregationTree", first: true, predicate: AggregationTreeComponent, descendants: true, isSignal: true }, { propertyName: "aggregationList", first: true, predicate: AggregationListComponent, descendants: true, isSignal: true }], ngImport: i0, template: `
14475
- @if (isDate(aggregation()?.column)) {
15263
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: AggregationComponent, isStandalone: true, selector: "Aggregation, aggregation", inputs: { class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null }, column: { classPropertyName: "column", publicName: "column", isSignal: true, isRequired: true, transformFunction: null }, showFiltersCount: { classPropertyName: "showFiltersCount", publicName: "showFiltersCount", isSignal: true, isRequired: false, transformFunction: null }, collapsible: { classPropertyName: "collapsible", publicName: "collapsible", isSignal: true, isRequired: false, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, searchable: { classPropertyName: "searchable", publicName: "searchable", isSignal: true, isRequired: false, transformFunction: null }, showCheckbox: { classPropertyName: "showCheckbox", publicName: "showCheckbox", isSignal: true, isRequired: false, transformFunction: null }, expandedLevel: { classPropertyName: "expandedLevel", publicName: "expandedLevel", isSignal: true, isRequired: false, transformFunction: null }, syncUrl: { classPropertyName: "syncUrl", publicName: "syncUrl", isSignal: true, isRequired: false, transformFunction: null }, controlledQuery: { classPropertyName: "controlledQuery", publicName: "query", isSignal: true, isRequired: false, transformFunction: null }, controlledAggregation: { classPropertyName: "controlledAggregation", publicName: "aggregation", isSignal: true, isRequired: false, transformFunction: null }, controlledSelection: { classPropertyName: "controlledSelection", publicName: "selection", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSelect: "onSelect", onApply: "onApply", onClear: "onClear", selectionChange: "selectionChange", filtersChange: "filtersChange" }, host: { properties: { "class": "cn(\"block h-[inherit] max-h-[inherit]\",class())" } }, queries: [{ propertyName: "itemDirective", first: true, predicate: AggregationItemDirective, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "aggregationDate", first: true, predicate: AggregationDateComponent, descendants: true, isSignal: true }, { propertyName: "aggregationTree", first: true, predicate: AggregationTreeComponent, descendants: true, isSignal: true }, { propertyName: "aggregationList", first: true, predicate: AggregationListComponent, descendants: true, isSignal: true }], ngImport: i0, template: `
15264
+ @switch (resolvedType()) {
15265
+ @case ("date") {
14476
15266
  <AggregationDate
14477
15267
  [id]="id()"
14478
15268
  [name]="name()"
@@ -14482,11 +15272,17 @@ class AggregationComponent {
14482
15272
  [showFiltersCount]="showFiltersCount()"
14483
15273
  [searchable]="searchable()"
14484
15274
  [syncUrl]="syncUrl()"
15275
+ [query]="controlledQuery()"
15276
+ [aggregation]="controlledAggregation()"
15277
+ [selection]="controlledSelection()"
14485
15278
  [title]="{ label: 'Date', icon: 'far fa-calendar-day' }"
14486
15279
  (onApply)="onApply.emit($event)"
14487
15280
  (onClear)="onClear.emit($event)"
15281
+ (selectionChange)="selectionChange.emit($event)"
15282
+ (filtersChange)="filtersChange.emit($event)"
14488
15283
  />
14489
- } @else if (aggregation()?.isTree) {
15284
+ }
15285
+ @case ("tree") {
14490
15286
  <AggregationTree
14491
15287
  [id]="id()"
14492
15288
  [name]="name()"
@@ -14496,13 +15292,20 @@ class AggregationComponent {
14496
15292
  [showFiltersCount]="showFiltersCount()"
14497
15293
  [searchable]="searchable()"
14498
15294
  [syncUrl]="syncUrl()"
15295
+ [query]="controlledQuery()"
15296
+ [aggregation]="controlledAggregation()"
15297
+ [selection]="controlledSelection()"
15298
+ [itemTemplate]="itemDirective()?.template"
15299
+ [showCheckbox]="showCheckbox()"
14499
15300
  [expandedLevel]="expandedLevel()"
14500
15301
  (onSelect)="onSelect.emit($event)"
14501
15302
  (onApply)="onApply.emit($event)"
14502
15303
  (onClear)="onClear.emit($event)"
15304
+ (selectionChange)="selectionChange.emit($event)"
15305
+ (filtersChange)="filtersChange.emit($event)"
14503
15306
  />
14504
- }
14505
- @else {
15307
+ }
15308
+ @default {
14506
15309
  <AggregationList
14507
15310
  [id]="id()"
14508
15311
  [name]="name()"
@@ -14512,12 +15315,20 @@ class AggregationComponent {
14512
15315
  [showFiltersCount]="showFiltersCount()"
14513
15316
  [searchable]="searchable()"
14514
15317
  [syncUrl]="syncUrl()"
15318
+ [query]="controlledQuery()"
15319
+ [aggregation]="controlledAggregation()"
15320
+ [selection]="controlledSelection()"
15321
+ [itemTemplate]="itemDirective()?.template"
15322
+ [showCheckbox]="showCheckbox()"
14515
15323
  (onSelect)="onSelect.emit($event)"
14516
15324
  (onApply)="onApply.emit($event)"
14517
15325
  (onClear)="onClear.emit($event)"
15326
+ (selectionChange)="selectionChange.emit($event)"
15327
+ (filtersChange)="filtersChange.emit($event)"
14518
15328
  />
14519
- }
14520
- `, isInline: true, dependencies: [{ kind: "component", type: AggregationListComponent, selector: "AggregationList, aggregation-list, aggregationlist", inputs: ["class", "id", "name", "column", "collapsible", "collapsed", "searchable", "showFiltersCount", "syncUrl", "searchText"], outputs: ["onSelect", "onApply", "onClear", "searchTextChange"] }, { kind: "component", type: AggregationTreeComponent, selector: "AggregationTree, aggregation-tree, aggregationtree", inputs: ["class", "id", "name", "column", "collapsible", "collapsed", "searchable", "showFiltersCount", "expandedLevel", "syncUrl", "searchText"], outputs: ["onSelect", "onApply", "onClear", "searchTextChange"] }, { kind: "component", type: AggregationDateComponent, selector: "aggregation-date, AggregationDate, aggregationdate", inputs: ["name", "column", "id", "collapsible", "collapsed", "searchable", "showFiltersCount", "syncUrl", "title", "displayEmptyDistributionIntervals", "searchText"], outputs: ["onSelect", "onApply", "onClear", "searchTextChange"] }] });
15329
+ }
15330
+ }
15331
+ `, isInline: true, dependencies: [{ kind: "component", type: AggregationListComponent, selector: "AggregationList, aggregation-list, aggregationlist", inputs: ["class", "id", "name", "column", "collapsible", "collapsed", "searchable", "showFiltersCount", "showCheckbox", "syncUrl", "query", "aggregation", "selection", "itemTemplate", "searchText"], outputs: ["onSelect", "onApply", "onClear", "selectionChange", "filtersChange", "searchTextChange"] }, { kind: "component", type: AggregationTreeComponent, selector: "AggregationTree, aggregation-tree, aggregationtree", inputs: ["class", "id", "name", "column", "collapsible", "collapsed", "searchable", "showFiltersCount", "showCheckbox", "expandedLevel", "syncUrl", "query", "aggregation", "selection", "itemTemplate", "searchText"], outputs: ["onSelect", "onApply", "onClear", "selectionChange", "filtersChange", "searchTextChange"] }, { kind: "component", type: AggregationDateComponent, selector: "aggregation-date, AggregationDate, aggregationdate", inputs: ["name", "column", "id", "collapsible", "collapsed", "searchable", "showFiltersCount", "syncUrl", "query", "aggregation", "selection", "title", "displayEmptyDistributionIntervals", "searchText"], outputs: ["onSelect", "onApply", "onClear", "selectionChange", "filtersChange", "searchTextChange"] }] });
14521
15332
  }
14522
15333
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: AggregationComponent, decorators: [{
14523
15334
  type: Component,
@@ -14525,7 +15336,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
14525
15336
  selector: "Aggregation, aggregation",
14526
15337
  imports: [AggregationListComponent, AggregationTreeComponent, AggregationDateComponent],
14527
15338
  template: `
14528
- @if (isDate(aggregation()?.column)) {
15339
+ @switch (resolvedType()) {
15340
+ @case ("date") {
14529
15341
  <AggregationDate
14530
15342
  [id]="id()"
14531
15343
  [name]="name()"
@@ -14535,11 +15347,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
14535
15347
  [showFiltersCount]="showFiltersCount()"
14536
15348
  [searchable]="searchable()"
14537
15349
  [syncUrl]="syncUrl()"
15350
+ [query]="controlledQuery()"
15351
+ [aggregation]="controlledAggregation()"
15352
+ [selection]="controlledSelection()"
14538
15353
  [title]="{ label: 'Date', icon: 'far fa-calendar-day' }"
14539
15354
  (onApply)="onApply.emit($event)"
14540
15355
  (onClear)="onClear.emit($event)"
15356
+ (selectionChange)="selectionChange.emit($event)"
15357
+ (filtersChange)="filtersChange.emit($event)"
14541
15358
  />
14542
- } @else if (aggregation()?.isTree) {
15359
+ }
15360
+ @case ("tree") {
14543
15361
  <AggregationTree
14544
15362
  [id]="id()"
14545
15363
  [name]="name()"
@@ -14549,13 +15367,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
14549
15367
  [showFiltersCount]="showFiltersCount()"
14550
15368
  [searchable]="searchable()"
14551
15369
  [syncUrl]="syncUrl()"
15370
+ [query]="controlledQuery()"
15371
+ [aggregation]="controlledAggregation()"
15372
+ [selection]="controlledSelection()"
15373
+ [itemTemplate]="itemDirective()?.template"
15374
+ [showCheckbox]="showCheckbox()"
14552
15375
  [expandedLevel]="expandedLevel()"
14553
15376
  (onSelect)="onSelect.emit($event)"
14554
15377
  (onApply)="onApply.emit($event)"
14555
15378
  (onClear)="onClear.emit($event)"
15379
+ (selectionChange)="selectionChange.emit($event)"
15380
+ (filtersChange)="filtersChange.emit($event)"
14556
15381
  />
14557
- }
14558
- @else {
15382
+ }
15383
+ @default {
14559
15384
  <AggregationList
14560
15385
  [id]="id()"
14561
15386
  [name]="name()"
@@ -14565,43 +15390,78 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
14565
15390
  [showFiltersCount]="showFiltersCount()"
14566
15391
  [searchable]="searchable()"
14567
15392
  [syncUrl]="syncUrl()"
15393
+ [query]="controlledQuery()"
15394
+ [aggregation]="controlledAggregation()"
15395
+ [selection]="controlledSelection()"
15396
+ [itemTemplate]="itemDirective()?.template"
15397
+ [showCheckbox]="showCheckbox()"
14568
15398
  (onSelect)="onSelect.emit($event)"
14569
15399
  (onApply)="onApply.emit($event)"
14570
15400
  (onClear)="onClear.emit($event)"
15401
+ (selectionChange)="selectionChange.emit($event)"
15402
+ (filtersChange)="filtersChange.emit($event)"
14571
15403
  />
14572
- }
15404
+ }
15405
+ }
14573
15406
  `,
14574
15407
  host: {
14575
15408
  "[class]": 'cn("block h-[inherit] max-h-[inherit]",class())'
14576
15409
  }
14577
15410
  }]
14578
- }], propDecorators: { aggregationDate: [{ type: i0.ViewChild, args: [i0.forwardRef(() => AggregationDateComponent), { isSignal: true }] }], aggregationTree: [{ type: i0.ViewChild, args: [i0.forwardRef(() => AggregationTreeComponent), { isSignal: true }] }], aggregationList: [{ type: i0.ViewChild, args: [i0.forwardRef(() => AggregationListComponent), { isSignal: true }] }], class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }], column: [{ type: i0.Input, args: [{ isSignal: true, alias: "column", required: true }] }], showFiltersCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFiltersCount", required: false }] }], collapsible: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsible", required: false }] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], expandedLevel: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandedLevel", required: false }] }], syncUrl: [{ type: i0.Input, args: [{ isSignal: true, alias: "syncUrl", required: false }] }], onSelect: [{ type: i0.Output, args: ["onSelect"] }], onApply: [{ type: i0.Output, args: ["onApply"] }], onClear: [{ type: i0.Output, args: ["onClear"] }] } });
15411
+ }], propDecorators: { aggregationDate: [{ type: i0.ViewChild, args: [i0.forwardRef(() => AggregationDateComponent), { isSignal: true }] }], aggregationTree: [{ type: i0.ViewChild, args: [i0.forwardRef(() => AggregationTreeComponent), { isSignal: true }] }], aggregationList: [{ type: i0.ViewChild, args: [i0.forwardRef(() => AggregationListComponent), { isSignal: true }] }], itemDirective: [{ type: i0.ContentChild, args: [i0.forwardRef(() => AggregationItemDirective), { isSignal: true }] }], class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }], column: [{ type: i0.Input, args: [{ isSignal: true, alias: "column", required: true }] }], showFiltersCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFiltersCount", required: false }] }], collapsible: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsible", required: false }] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], showCheckbox: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCheckbox", required: false }] }], expandedLevel: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandedLevel", required: false }] }], syncUrl: [{ type: i0.Input, args: [{ isSignal: true, alias: "syncUrl", required: false }] }], controlledQuery: [{ type: i0.Input, args: [{ isSignal: true, alias: "query", required: false }] }], controlledAggregation: [{ type: i0.Input, args: [{ isSignal: true, alias: "aggregation", required: false }] }], controlledSelection: [{ type: i0.Input, args: [{ isSignal: true, alias: "selection", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], onSelect: [{ type: i0.Output, args: ["onSelect"] }], onApply: [{ type: i0.Output, args: ["onApply"] }], onClear: [{ type: i0.Output, args: ["onClear"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], filtersChange: [{ type: i0.Output, args: ["filtersChange"] }] } });
14579
15412
 
14580
15413
  class AsideFiltersComponent {
14581
15414
  cn = cn;
14582
15415
  class = input(...(ngDevMode ? [undefined, { debugName: "class" }] : []));
14583
- position = input(...(ngDevMode ? [undefined, { debugName: "position" }] : []));
15416
+ /**
15417
+ * Explicit, developer-defined list of aggregations to display.
15418
+ *
15419
+ * Each entry is an aggregation **name** (preferred) or **column** — e.g.
15420
+ * `['Authors', 'Sources', 'Geo']`. Although typed as `string[]`, this is the list of
15421
+ * aggregations to render, not a list of raw column keys.
15422
+ *
15423
+ * When provided, it fully bypasses the JSON `filters` configuration (neither
15424
+ * `position`, `getAuthorized`, nor the admin `hidden` rule are applied): the listed
15425
+ * aggregations are rendered as-is, in the given order, provided they exist in the
15426
+ * {@link AggregationsStore}. When empty (default), the component falls back to the
15427
+ * JSON-driven behavior (`position: 'left' | 'both'`, excluding `hidden` aggregations).
15428
+ *
15429
+ * Resolution tries by `name` first — the canonical, unambiguous identifier, since a
15430
+ * `column` can be shared by several aggregations with distinct names — then falls back
15431
+ * to the first aggregation matching the given column. Prefer passing names.
15432
+ */
15433
+ aggregations = input([], ...(ngDevMode ? [{ debugName: "aggregations" }] : []));
15434
+ /**
15435
+ * When `true` (default), every section shares the same `id`, producing a native
15436
+ * `<details name>` accordion where a single section is open at a time. When `false`,
15437
+ * each section gets a unique id (based on the aggregation `name`) and expands
15438
+ * independently — closer to a typical SBA v2 "Facets List".
15439
+ */
15440
+ exclusive = input(true, ...(ngDevMode ? [{ debugName: "exclusive", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
14584
15441
  appStore = inject(AppStore);
15442
+ aggregationsStore = inject(AggregationsStore);
15443
+ aggregationsService = inject(AggregationsService);
14585
15444
  asideFilters = computed(() => {
14586
- // `left` filters live only in the drawer, `both` filters live in the drawer and the bar
15445
+ const explicit = this.aggregations();
15446
+ // Explicit developer-defined list → render exactly as passed, bypassing the JSON config.
15447
+ if (explicit.length > 0) {
15448
+ return explicit
15449
+ .map((key) => this.aggregationsStore.getAggregation(key, "name") ?? this.aggregationsStore.getAggregation(key, "column"))
15450
+ .filter((agg) => !!agg)
15451
+ .map((agg) => ({ name: agg.name, column: agg.column }));
15452
+ }
15453
+ // Default JSON-driven behavior:
15454
+ // `left` filters live only in the drawer, `both` filters live in the drawer and the bar.
15455
+ // Admin-hidden aggregations are excluded (the rule lives in the service).
14587
15456
  const asideFilters = this.appStore.filters().filter((f) => f.position === "left" || f.position === "both");
14588
- return this.appStore.getAuthorized(asideFilters);
15457
+ return this.aggregationsService.rejectHidden(this.appStore.getAuthorized(asideFilters));
14589
15458
  }, ...(ngDevMode ? [{ debugName: "asideFilters" }] : []));
14590
- /**
14591
- * Checks if the specified column is a date filter.
14592
- *
14593
- * @param column The column name to check.
14594
- * @returns True if the column is a date filter, false otherwise.
14595
- */
14596
- isDate(column) {
14597
- return this.appStore.isDateColumn(column);
14598
- }
14599
15459
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: AsideFiltersComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
14600
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: AsideFiltersComponent, isStandalone: true, selector: "aside-filters, AsideFilters, asidefilters", inputs: { class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "cn('flex flex-col gap-2 overflow-auto', class())" } }, ngImport: i0, template: `
15460
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: AsideFiltersComponent, isStandalone: true, selector: "aside-filters, AsideFilters, asidefilters", inputs: { class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, aggregations: { classPropertyName: "aggregations", publicName: "aggregations", isSignal: true, isRequired: false, transformFunction: null }, exclusive: { classPropertyName: "exclusive", publicName: "exclusive", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "cn('flex flex-col gap-2 overflow-auto', class())" } }, ngImport: i0, template: `
14601
15461
  <div class="flex flex-col gap-2">
14602
15462
  @for (agg of asideFilters(); track agg.name) {
14603
15463
  <Aggregation
14604
- id="aside-filters"
15464
+ [id]="exclusive() ? 'aside-filters' : 'aside-filters-' + agg.name"
14605
15465
  [name]="agg.name"
14606
15466
  [column]="agg.column"
14607
15467
  [collapsible]="true"
@@ -14609,7 +15469,7 @@ class AsideFiltersComponent {
14609
15469
  showFiltersCount />
14610
15470
  }
14611
15471
  </div>
14612
- `, isInline: true, dependencies: [{ kind: "component", type: AggregationComponent, selector: "Aggregation, aggregation", inputs: ["class", "id", "name", "column", "showFiltersCount", "collapsible", "collapsed", "searchable", "expandedLevel", "syncUrl"], outputs: ["onSelect", "onApply", "onClear"] }] });
15472
+ `, isInline: true, dependencies: [{ kind: "component", type: AggregationComponent, selector: "Aggregation, aggregation", inputs: ["class", "id", "name", "column", "showFiltersCount", "collapsible", "collapsed", "searchable", "showCheckbox", "expandedLevel", "syncUrl", "query", "aggregation", "selection", "type"], outputs: ["onSelect", "onApply", "onClear", "selectionChange", "filtersChange"] }] });
14613
15473
  }
14614
15474
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: AsideFiltersComponent, decorators: [{
14615
15475
  type: Component,
@@ -14620,7 +15480,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
14620
15480
  <div class="flex flex-col gap-2">
14621
15481
  @for (agg of asideFilters(); track agg.name) {
14622
15482
  <Aggregation
14623
- id="aside-filters"
15483
+ [id]="exclusive() ? 'aside-filters' : 'aside-filters-' + agg.name"
14624
15484
  [name]="agg.name"
14625
15485
  [column]="agg.column"
14626
15486
  [collapsible]="true"
@@ -14633,7 +15493,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
14633
15493
  "[class]": "cn('flex flex-col gap-2 overflow-auto', class())"
14634
15494
  }
14635
15495
  }]
14636
- }], propDecorators: { class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }] } });
15496
+ }], propDecorators: { class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], aggregations: [{ type: i0.Input, args: [{ isSignal: true, alias: "aggregations", required: false }] }], exclusive: [{ type: i0.Input, args: [{ isSignal: true, alias: "exclusive", required: false }] }] } });
14637
15497
 
14638
15498
  class FilterButtonComponent {
14639
15499
  name = input(...(ngDevMode ? [undefined, { debugName: "name" }] : []));
@@ -14760,7 +15620,7 @@ class FilterButtonComponent {
14760
15620
  }
14761
15621
  </PopoverContent>
14762
15622
  </Popover>
14763
- `, 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", "expandedLevel", "syncUrl"], outputs: ["onSelect", "onApply", "onClear"] }, { 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" }] });
15623
+ `, 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", "expandedLevel", "syncUrl", "query", "aggregation", "selection", "type"], 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" }] });
14764
15624
  }
14765
15625
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: FilterButtonComponent, decorators: [{
14766
15626
  type: Component,
@@ -14844,92 +15704,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
14844
15704
  const FILTERS_BREAKPOINT = new InjectionToken('FILTERS_BREAKPOINT', { factory: () => 5 });
14845
15705
 
14846
15706
  class MoreComponent {
14847
- cn = cn;
14848
15707
  count = input(2, ...(ngDevMode ? [{ debugName: "count" }] : []));
14849
15708
  includedFilters = input([], ...(ngDevMode ? [{ debugName: "includedFilters" }] : []));
14850
15709
  excludedFilters = input([], ...(ngDevMode ? [{ debugName: "excludedFilters" }] : []));
14851
15710
  aggregations = input(...(ngDevMode ? [undefined, { debugName: "aggregations" }] : []));
14852
15711
  homepage = input(false, ...(ngDevMode ? [{ debugName: "homepage", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
14853
- appStore = inject(AppStore);
14854
- aggregationsStore = inject(AggregationsStore);
14855
- queryParamsStore = inject(QueryParamsStore);
14856
15712
  aggregationsService = inject(AggregationsService);
14857
15713
  /**
14858
- * Signal containing an array of extended filter objects.
15714
+ * The overflow filters rendered as accordions: the authorized filters beyond the
15715
+ * first `count` (which are already shown in the bar).
14859
15716
  *
14860
15717
  * @remarks
14861
- * This signal holds the current state of filters that can be applied to data.
14862
- * The filters are of type CFilterEx[], which extends the base filter functionality.
14863
- *
14864
- */
14865
- filters = signal([], ...(ngDevMode ? [{ debugName: "filters" }] : []));
14866
- /**
14867
- * Computed property that returns an array of filters that are currently visible.
14868
- * Filters out any filters where the `hidden` property is set to true.
14869
- *
14870
- * @returns An array of filters that are not hidden
15718
+ * Pure display wiring — authorization (and the admin `hidden` rule) is handled by
15719
+ * {@link AggregationsService.getAuthorizedFilters}; `toSpliced` just drops the
15720
+ * filters already shown in the bar.
14871
15721
  */
14872
- visibleFilters = computed(() => {
14873
- const filters = this.filters().filter((f) => !f.hidden);
14874
- return filters;
14875
- }, ...(ngDevMode ? [{ debugName: "visibleFilters" }] : []));
14876
- constructor() {
14877
- effect(() => {
14878
- const count = this.count();
14879
- const authorizedFilters = this.aggregationsService
14880
- .getAuthorizedFilters(this.aggregations(), this.includedFilters(), this.excludedFilters(), this.homepage())
14881
- .toSpliced(0, count);
14882
- const f = authorizedFilters.map((agg) => {
14883
- const { icon = "far fa-list", hidden = false } = this.appStore.getAggregationCustomization(agg.column, agg.name) || {};
14884
- return {
14885
- name: agg.name,
14886
- column: agg.column,
14887
- icon,
14888
- count: 0,
14889
- isTree: false,
14890
- disabled: false,
14891
- hidden
14892
- };
14893
- });
14894
- this.filters.set(f);
14895
- this.updateFilters();
14896
- });
14897
- }
14898
- clearFilter(field) {
14899
- this.queryParamsStore.removeFilter(field);
14900
- }
14901
- updateFilters() {
14902
- this.filters.update((filters) => {
14903
- return filters.map((filter) => {
14904
- const agg = this.aggregationsStore.getAggregation(filter.name);
14905
- const f = this.queryParamsStore.getFilter({ name: agg?.name, field: agg?.column });
14906
- const count = f?.count || 0;
14907
- const { display = agg?.isTree ? undefined : filter.display } = this.appStore.getAggregationCustomization(filter.column) || {};
14908
- return {
14909
- ...filter,
14910
- name: agg?.name || filter.name,
14911
- display,
14912
- isTree: agg?.isTree || false,
14913
- count,
14914
- disabled: !!(!agg?.items || agg.items?.length === 0) // Disable if no items
14915
- };
14916
- });
14917
- });
14918
- }
14919
- /**
14920
- * Checks whether there are active filters for the current aggregation column.
14921
- *
14922
- * if True, the clear button is shown.
14923
- *
14924
- * @returns {boolean} `true` if the filter count for the aggregation column is greater than 0, otherwise `false`.
14925
- */
14926
- hasFilters(aggregation) {
14927
- const { count = 0 } = this.queryParamsStore.getFilter({ field: aggregation.column, name: aggregation.name }) || {};
14928
- return count > 0;
14929
- }
15722
+ filters = computed(() => this.aggregationsService
15723
+ .getAuthorizedFilters(this.aggregations(), this.includedFilters(), this.excludedFilters(), this.homepage())
15724
+ .toSpliced(0, this.count()), ...(ngDevMode ? [{ debugName: "filters" }] : []));
14930
15725
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: MoreComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
14931
15726
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: MoreComponent, isStandalone: true, selector: "more, More", inputs: { count: { classPropertyName: "count", publicName: "count", isSignal: true, isRequired: false, transformFunction: null }, includedFilters: { classPropertyName: "includedFilters", publicName: "includedFilters", isSignal: true, isRequired: false, transformFunction: null }, excludedFilters: { classPropertyName: "excludedFilters", publicName: "excludedFilters", isSignal: true, isRequired: false, transformFunction: null }, aggregations: { classPropertyName: "aggregations", publicName: "aggregations", isSignal: true, isRequired: false, transformFunction: null }, homepage: { classPropertyName: "homepage", publicName: "homepage", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "divide-y divide-muted-foreground/18" }, ngImport: i0, template: `
14932
- @for (filter of visibleFilters(); track $index) {
15727
+ @for (filter of filters(); track $index) {
14933
15728
  <Aggregation
14934
15729
  class="w-60 max-w-80 px-1 [--height:15lh]"
14935
15730
  id="more-filters"
@@ -14940,12 +15735,12 @@ class MoreComponent {
14940
15735
  [name]="filter.name"
14941
15736
  [column]="filter.column" />
14942
15737
  }
14943
- `, 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", "expandedLevel", "syncUrl"], outputs: ["onSelect", "onApply", "onClear"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
15738
+ `, 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", "expandedLevel", "syncUrl", "query", "aggregation", "selection", "type"], outputs: ["onSelect", "onApply", "onClear", "selectionChange", "filtersChange"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
14944
15739
  }
14945
15740
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: MoreComponent, decorators: [{
14946
15741
  type: Component,
14947
15742
  args: [{ selector: "more, More", standalone: true, imports: [AggregationComponent, TranslocoPipe], template: `
14948
- @for (filter of visibleFilters(); track $index) {
15743
+ @for (filter of filters(); track $index) {
14949
15744
  <Aggregation
14950
15745
  class="w-60 max-w-80 px-1 [--height:15lh]"
14951
15746
  id="more-filters"
@@ -14959,7 +15754,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
14959
15754
  `, host: {
14960
15755
  class: "divide-y divide-muted-foreground/18"
14961
15756
  }, styles: [":host{scrollbar-width:none}\n"] }]
14962
- }], ctorParameters: () => [], propDecorators: { count: [{ type: i0.Input, args: [{ isSignal: true, alias: "count", required: false }] }], includedFilters: [{ type: i0.Input, args: [{ isSignal: true, alias: "includedFilters", required: false }] }], excludedFilters: [{ type: i0.Input, args: [{ isSignal: true, alias: "excludedFilters", required: false }] }], aggregations: [{ type: i0.Input, args: [{ isSignal: true, alias: "aggregations", required: false }] }], homepage: [{ type: i0.Input, args: [{ isSignal: true, alias: "homepage", required: false }] }] } });
15757
+ }], 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 }] }] } });
14963
15758
 
14964
15759
  class MoreButtonComponent {
14965
15760
  appStore = inject(AppStore);
@@ -16798,5 +17593,5 @@ const queryNameResolver = () => {
16798
17593
  * Generated bundle index. Do not edit.
16799
17594
  */
16800
17595
 
16801
- export { AGGREGATIONS_NAMES, AGGREGATIONS_NAMES_PRESET_DEFAULT, APP_FEATURES, AdvancedFiltersComponent, AdvancedSearch, AdvancedSearchComponent, AggregationComponent, AggregationDateComponent, AggregationDateRangeDialogComponent, AggregationListComponent, AggregationPanelComponent, AggregationTreeComponent, AggregationsService, AggregationsStore, Alert, AlertDialog, AlertsComponent, AppService, AppStore, ApplicationService, ApplicationStore, ArticleEntities, ArticleExtracts, ArticleLabels, ArticleSimilarDocuments, AsideFiltersComponent, AuditFeedbackType, AuditService, AuthGuard, AuthPageComponent, AutocompleteService, BOOKMARKS_CONFIG, BOOKMARKS_OPTIONS, BackdropComponent, BackdropService, BookmarkButtonComponent, BookmarksComponent, COLLECTIONS_CONFIG, COLLECTIONS_OPTIONS, COMPONENTS_FOR_DOCUMENT_TYPE, ChangePasswordComponent, ChildMarkerDirective, CollectionsComponent, CollectionsDialog, DRAWER_COMPONENT, DRAWER_STACK_MAX_COUNT, DateComponent, DeleteCollectionDialog, DidYouMeanComponent, DocumentLocatorComponent, DrawerAdvancedFiltersComponent, DrawerComponent, DrawerNavbarComponent, DrawerPreviewComponent, DrawerService, DrawerStackComponent, DrawerStackService, DropdownInputComponent, DropdownListComponent, ErrorComponent, ExportDialog, ExportService, FILTERS_BREAKPOINT, FILTER_DATE_ALLOW_CUSTOM_RANGE, FeatureFlagsDialogComponent, FeedbackDialogComponent, FileSizePipe, FilterButtonComponent, FiltersBarComponent, HIGHLIGHTS, HighlightWordPipe, InfinityScrollDirective, InlineWorker, JsonMethodPluginService, KeyboardNavigatorDirective, LabelService, LabelsEditDialog, LoadingComponent, MetadataComponent, MissingTermsComponent, MoreButtonComponent, MoreComponent, MultiSelectLabelsComponent, MultiSelectionToolbarComponent, NON_SEARCHABLE_COLUMNS, NON_SEARCHABLE_DEFAULTS, NavbarTabsComponent, NavigationService, NoResultComponent, OpenArticleOnCtrlEnterDirective, OperatorPipe, OverflowItemDirective, OverflowManagerDirective, OverflowStopDirective, OverrideUserDialogComponent, PREVIEW_CONFIG, PagerComponent, PreviewNavigator, PreviewService, PrincipalService, PrincipalStore, QueryParamsStore, QueryService, RECENT_SEARCHES_CONFIG, RECENT_SEARCHES_OPTIONS, ROUTE_COMPONENTS, RecentSearchesComponent, ResetUserSettingsDialogComponent, SAVED_SEARCHES_CONFIG, SAVED_SEARCHES_OPTIONS, SavedSearchDialog, SavedSearchesComponent, SavedSearchesService, SearchFeedbackComponent, SearchInputFooter, SearchService, SelectArticleDirective, SelectArticleOnClickDirective, SelectionHistoryService, SelectionService, SelectionStore, ShowBookmarkDirective, SidebarNavComponent, SignInComponent, SignedOutComponent, SortSelectorComponent, SourceComponent, SourceIconPipe, SponsoredResultsComponent, SyslangPipe, THEMES, TextChunkService, ThemeProviderDirective, ThemeSelectorComponent, ThemeStore, ThemeToggleComponent, TranslocoDateImpurePipe, UserProfileDialog, UserProfileFormComponent, UserProfileService, UserSettingsStore, applyThemeToNativeElement, auditInterceptorFn, authInterceptorFn, bodyInterceptorFn, bootstrapApp, buildQuery, debouncedSignal, errorInterceptorFn, getCurrentPath, getCurrentQueryName, getQueryNameFromRoute, injectRouteNavigation, processCssVars, queryNameResolver, signIn, themeColorNameToCssVariable, themeColorsToCssVariables, toastInterceptorFn, withAggregationsFeatures, withAlertsFeatures, withAppFeatures, withApplicationFeatures, withAssistantFeatures, withBasketsFeatures, withBookmarkFeatures, withBootstrapApp, withExtractsFeatures, withFetch, withMultiSelectionFeatures, withPrincipalFeatures, withQueryParamsFeatures, withRecentSearchesFeatures, withSavedSearchesFeatures, withSelectionFeatures, withThemeBodyHook, withThemes, withThemesFeatures, withUserSettingsFeatures };
17596
+ export { AGGREGATIONS_NAMES, AGGREGATIONS_NAMES_PRESET_DEFAULT, APP_FEATURES, AdvancedFiltersComponent, AdvancedSearch, AdvancedSearchComponent, AggregationComponent, AggregationDateComponent, AggregationDateRangeDialogComponent, AggregationItemDirective, AggregationListComponent, AggregationPanelComponent, AggregationTreeComponent, AggregationsService, AggregationsStore, Alert, AlertDialog, AlertsComponent, AppService, AppStore, ApplicationService, ApplicationStore, ArticleEntities, ArticleExtracts, ArticleLabels, ArticleSimilarDocuments, AsideFiltersComponent, AuditFeedbackType, AuditService, AuthGuard, AuthPageComponent, AutocompleteService, BOOKMARKS_CONFIG, BOOKMARKS_OPTIONS, BackdropComponent, BackdropService, BookmarkButtonComponent, BookmarksComponent, COLLECTIONS_CONFIG, COLLECTIONS_OPTIONS, COMPONENTS_FOR_DOCUMENT_TYPE, ChangePasswordComponent, ChildMarkerDirective, CollectionsComponent, CollectionsDialog, DRAWER_COMPONENT, DRAWER_STACK_MAX_COUNT, DateComponent, DeleteCollectionDialog, DidYouMeanComponent, DocumentLocatorComponent, DrawerAdvancedFiltersComponent, DrawerComponent, DrawerNavbarComponent, DrawerPreviewComponent, DrawerService, DrawerStackComponent, DrawerStackService, DropdownInputComponent, DropdownListComponent, ErrorComponent, ExportDialog, ExportService, FILTERS_BREAKPOINT, FILTER_DATE_ALLOW_CUSTOM_RANGE, FeatureFlagsDialogComponent, FeedbackDialogComponent, FileSizePipe, FilterButtonComponent, FiltersBarComponent, HIGHLIGHTS, HighlightWordPipe, InfinityScrollDirective, InlineWorker, JsonMethodPluginService, KeyboardNavigatorDirective, LabelService, LabelsEditDialog, LoadingComponent, MetadataComponent, MissingTermsComponent, MoreButtonComponent, MoreComponent, MultiSelectLabelsComponent, MultiSelectionToolbarComponent, NON_SEARCHABLE_COLUMNS, NON_SEARCHABLE_DEFAULTS, NavbarTabsComponent, NavigationService, NoResultComponent, OpenArticleOnCtrlEnterDirective, OperatorPipe, OverflowItemDirective, OverflowManagerDirective, OverflowStopDirective, OverrideUserDialogComponent, PREVIEW_CONFIG, PagerComponent, PreviewNavigator, PreviewService, PrincipalService, PrincipalStore, QueryParamsStore, QueryService, RECENT_SEARCHES_CONFIG, RECENT_SEARCHES_OPTIONS, ROUTE_COMPONENTS, RecentSearchesComponent, ResetUserSettingsDialogComponent, SAVED_SEARCHES_CONFIG, SAVED_SEARCHES_OPTIONS, SavedSearchDialog, SavedSearchesComponent, SavedSearchesService, SearchFeedbackComponent, SearchInputFooter, SearchService, SelectArticleDirective, SelectArticleOnClickDirective, SelectionHistoryService, SelectionService, SelectionStore, ShowBookmarkDirective, SidebarNavComponent, SignInComponent, SignedOutComponent, SortSelectorComponent, SourceComponent, SourceIconPipe, SponsoredResultsComponent, SyslangPipe, THEMES, TextChunkService, ThemeProviderDirective, ThemeSelectorComponent, ThemeStore, ThemeToggleComponent, TranslocoDateImpurePipe, UserProfileDialog, UserProfileFormComponent, UserProfileService, UserSettingsStore, applyThemeToNativeElement, auditInterceptorFn, authInterceptorFn, bodyInterceptorFn, bootstrapApp, buildQuery, debouncedSignal, errorInterceptorFn, getCurrentPath, getCurrentQueryName, getQueryNameFromRoute, injectRouteNavigation, legacyFilterToFilter, processCssVars, queryNameResolver, signIn, themeColorNameToCssVariable, themeColorsToCssVariables, toastInterceptorFn, withAggregationsFeatures, withAlertsFeatures, withAppFeatures, withApplicationFeatures, withAssistantFeatures, withBasketsFeatures, withBookmarkFeatures, withBootstrapApp, withExtractsFeatures, withFetch, withMultiSelectionFeatures, withPrincipalFeatures, withQueryParamsFeatures, withRecentSearchesFeatures, withSavedSearchesFeatures, withSelectionFeatures, withThemeBodyHook, withThemes, withThemesFeatures, withUserSettingsFeatures };
16802
17597
  //# sourceMappingURL=sinequa-atomic-angular.mjs.map