@sinequa/atomic-angular 0.4.37 → 0.4.45

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.
@@ -812,14 +812,19 @@ class OperatorPipe {
812
812
  return `> ${new Intl.DateTimeFormat(locale).format(new Date(start))} ≤ ${new Intl.DateTimeFormat(locale).format(new Date(end))}`;
813
813
  }
814
814
  if (filter?.operator === 'and') {
815
- const value = filter?.display ?? filter?.value ?? '';
816
- const date = new Date(value);
817
- if (isNaN(date.getTime())) {
818
- return value;
815
+ const filters = filter.filters;
816
+ const from = filters?.find((f) => f.operator === 'gte')?.value;
817
+ const to = filters?.find((f) => f.operator === 'lte')?.value;
818
+ if (from && to) {
819
+ return `≥ ${new Intl.DateTimeFormat(locale).format(new Date(from))} ≤ ${new Intl.DateTimeFormat(locale).format(new Date(to))}`;
819
820
  }
820
- else {
821
- return new Intl.DateTimeFormat(locale).format(date);
821
+ if (from) {
822
+ return `≥ ${new Intl.DateTimeFormat(locale).format(new Date(from))}`;
823
+ }
824
+ if (to) {
825
+ return `≤ ${new Intl.DateTimeFormat(locale).format(new Date(to))}`;
822
826
  }
827
+ return filter?.display ?? filter?.value ?? '';
823
828
  }
824
829
  const date = new Date(filter?.value || '');
825
830
  if (isNaN(date.getTime())) {
@@ -2814,13 +2819,34 @@ function withUserSettingsFeatures() {
2814
2819
  alerts: [],
2815
2820
  assistants: {},
2816
2821
  language: undefined,
2817
- collapseAssistant: undefined
2822
+ collapseAssistant: undefined,
2823
+ agents: { isDebugMode: false }
2818
2824
  });
2819
2825
  }
2820
2826
  })));
2821
2827
  }
2822
2828
 
2823
- const UserSettingsStore = signalStore({ providedIn: 'root' }, withDevtools('UserSettings'), withBookmarkFeatures(), withRecentSearchesFeatures(), withSavedSearchesFeatures(), withBasketsFeatures(), withAssistantFeatures(), withUserSettingsFeatures(), withAlertsFeatures(), withDarkModeFeatures());
2829
+ function withAgentsFeatures() {
2830
+ return signalStoreFeature(withState({
2831
+ agents: { isDebugMode: false }
2832
+ }), withComputed((store) => ({
2833
+ isDebugMode: computed(() => store.agents()?.isDebugMode ?? false)
2834
+ })), withMethods((store) => ({
2835
+ async setDebugMode(value) {
2836
+ try {
2837
+ await patchUserSettings({ agents: { ...store.agents(), isDebugMode: value } }, { type: 'Agent_DebugMode_Toggle' });
2838
+ patchState(store, (state) => ({
2839
+ agents: { ...state.agents, isDebugMode: value }
2840
+ }));
2841
+ }
2842
+ catch (e) {
2843
+ error('setDebugMode failed', e);
2844
+ }
2845
+ }
2846
+ })));
2847
+ }
2848
+
2849
+ const UserSettingsStore = signalStore({ providedIn: 'root' }, withDevtools('UserSettings'), withBookmarkFeatures(), withRecentSearchesFeatures(), withSavedSearchesFeatures(), withBasketsFeatures(), withAssistantFeatures(), withUserSettingsFeatures(), withAlertsFeatures(), withAgentsFeatures(), withDarkModeFeatures());
2824
2850
 
2825
2851
  class QueryService {
2826
2852
  http = inject(HttpClient);
@@ -3656,18 +3682,26 @@ class ApplicationService {
3656
3682
  }
3657
3683
  const { light, dark, alt } = general.logo || {};
3658
3684
  document.documentElement.style.setProperty("--logo-alt-text", `'${alt || general.name}'`);
3685
+ // light mode logo configuration
3659
3686
  if (light?.small) {
3660
3687
  document.documentElement.style.setProperty("--logo-light-small", `url('${light.small}')`);
3661
3688
  }
3662
3689
  if (light?.large) {
3663
3690
  document.documentElement.style.setProperty("--logo-light-large", `url('${light.large}')`);
3664
3691
  }
3692
+ if (light?.sidebar) {
3693
+ document.documentElement.style.setProperty("--logo-light-sidebar", `url('${light.sidebar}')`);
3694
+ }
3695
+ // dark mode logo configuration
3665
3696
  if (dark?.small) {
3666
3697
  document.documentElement.style.setProperty("--logo-dark-small", `url('${dark.small}')`);
3667
3698
  }
3668
3699
  if (dark?.large) {
3669
3700
  document.documentElement.style.setProperty("--logo-dark-large", `url('${dark.large}')`);
3670
3701
  }
3702
+ if (dark?.sidebar) {
3703
+ document.documentElement.style.setProperty("--logo-dark-sidebar", `url('${dark.sidebar}')`);
3704
+ }
3671
3705
  }
3672
3706
  /**
3673
3707
  * Sets the document title with optional application name prefix.
@@ -5397,10 +5431,13 @@ class MissingTermsComponent {
5397
5431
  return this.article()
5398
5432
  .termspresence?.filter(tp => tp.presence === 'missing')
5399
5433
  .map(tp => {
5400
- const text = (query.text || '').replace(new RegExp(`\\b${tp.term}\\b`, 'gi'), '');
5434
+ const bracketMatch = tp.term.match(/^\[(.+)\]$/);
5435
+ const cleanTerm = bracketMatch ? bracketMatch[1] : tp.term;
5436
+ const escapedTerm = cleanTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
5437
+ const text = (query.text || '').replace(new RegExp(`\\b${escapedTerm}\\b`, 'gi'), '').replace(/\s+/g, ' ').trim();
5401
5438
  return {
5402
- value: tp.term,
5403
- queryParams: { q: `${text} +[${tp.term}]` }
5439
+ value: cleanTerm,
5440
+ queryParams: { q: `${text} +[${cleanTerm}]` }
5404
5441
  };
5405
5442
  });
5406
5443
  }, ...(ngDevMode ? [{ debugName: "missingTerms" }] : []));
@@ -5532,7 +5569,7 @@ class CollectionsDialog {
5532
5569
  {{ collection.name }}
5533
5570
  </li>
5534
5571
  } @empty {
5535
- <li class="py-4 text-center text-neutral-500">
5572
+ <li class="list-none py-4 text-center text-neutral-500">
5536
5573
  {{ 'collections.noCollections' | transloco }}
5537
5574
  </li>
5538
5575
  }
@@ -5616,7 +5653,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
5616
5653
  {{ collection.name }}
5617
5654
  </li>
5618
5655
  } @empty {
5619
- <li class="py-4 text-center text-neutral-500">
5656
+ <li class="list-none py-4 text-center text-neutral-500">
5620
5657
  {{ 'collections.noCollections' | transloco }}
5621
5658
  </li>
5622
5659
  }
@@ -7147,6 +7184,7 @@ class NavbarTabsComponent {
7147
7184
  [noTruncate]="noTruncate()"
7148
7185
  [style.--tab-min-width]="minTabWidth()"
7149
7186
  [attr.aria-selected]="this.currentPath() === tab.path"
7187
+ [attr.disabled]="showCount() && tab.count === 0 ? '' : null"
7150
7188
  [active]="this.currentPath() === tab.path"
7151
7189
  [routerLink]="[tab.routerLink]"
7152
7190
  [queryParams]="{ n: tab.queryName, q: searchText(), t: tab.wsQueryTab, f: undefined, sort: undefined, id: undefined, page: undefined }"
@@ -7239,6 +7277,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
7239
7277
  [noTruncate]="noTruncate()"
7240
7278
  [style.--tab-min-width]="minTabWidth()"
7241
7279
  [attr.aria-selected]="this.currentPath() === tab.path"
7280
+ [attr.disabled]="showCount() && tab.count === 0 ? '' : null"
7242
7281
  [active]="this.currentPath() === tab.path"
7243
7282
  [routerLink]="[tab.routerLink]"
7244
7283
  [queryParams]="{ n: tab.queryName, q: searchText(), t: tab.wsQueryTab, f: undefined, sort: undefined, id: undefined, page: undefined }"
@@ -8486,6 +8525,7 @@ class AggregationListComponent {
8486
8525
  aggregationsService = inject(AggregationsService);
8487
8526
  el = inject(ElementRef);
8488
8527
  injector = inject(Injector);
8528
+ destroyRef = inject(DestroyRef);
8489
8529
  class = input("", ...(ngDevMode ? [{ debugName: "class" }] : []));
8490
8530
  /**
8491
8531
  * The name of the <details> element. When you provide the same id, the component work as an accordion
@@ -8653,6 +8693,15 @@ class AggregationListComponent {
8653
8693
  const suggests = (await withFetch(() => fetchSuggestField(this.normalizedSearchText(), [this.aggregation()?.column || ""], query), this.injector)) || [];
8654
8694
  this.suggests.set(suggests);
8655
8695
  });
8696
+ this.destroyRef.onDestroy(() => {
8697
+ // If the popover is closed with unapplied selections, reset $selected to undefined
8698
+ // so that processAggregation can recompute it from active filters on next open
8699
+ if (this.selection()) {
8700
+ this.aggregation()?.items?.forEach(item => {
8701
+ item.$selected = undefined;
8702
+ });
8703
+ }
8704
+ });
8656
8705
  }
8657
8706
  /**
8658
8707
  * Clears the current filter for the aggregation column.
@@ -8761,8 +8810,10 @@ class AggregationListComponent {
8761
8810
  * If the item is deselected, the selection count is decremented by 1.
8762
8811
  */
8763
8812
  select() {
8764
- this.onSelect.emit(this.items().filter(item => item.$selected));
8765
- this.selection.set(true);
8813
+ const selectedItems = this.items().filter(item => item.$selected);
8814
+ this.onSelect.emit(selectedItems);
8815
+ // Keep apply visible if items are selected, or if active filters exist (user may be deselecting to clear)
8816
+ this.selection.set(selectedItems.length > 0 || this.hasFilters());
8766
8817
  }
8767
8818
  /**
8768
8819
  * Updates the collapsed status on header click if the component is collapsible.
@@ -8882,12 +8933,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
8882
8933
  }], ctorParameters: () => [], propDecorators: { scrollElement: [{ type: i0.ViewChild, args: ["scrollElement", { isSignal: true }] }], searchInput: [{ type: i0.ViewChild, args: ["searchInput", { isSignal: true }] }], class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }], column: [{ type: i0.Input, args: [{ isSignal: true, alias: "column", required: true }] }], onSelect: [{ type: i0.Output, args: ["onSelect"] }], onApply: [{ type: i0.Output, args: ["onApply"] }], onClear: [{ type: i0.Output, args: ["onClear"] }], collapsible: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsible", required: false }] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], showFiltersCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFiltersCount", required: false }] }], searchText: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchText", required: false }] }, { type: i0.Output, args: ["searchTextChange"] }] } });
8883
8934
 
8884
8935
  class AggregationDateComponent extends AggregationListComponent {
8885
- destroyRef;
8886
8936
  dateRangeDialog = viewChild(AggregationDateRangeDialogComponent, ...(ngDevMode ? [{ debugName: "dateRangeDialog" }] : []));
8887
8937
  title = input({ label: "Date", icon: "far fa-calendar-day" }, ...(ngDevMode ? [{ debugName: "title" }] : []));
8888
8938
  displayEmptyDistributionIntervals = input(false, ...(ngDevMode ? [{ debugName: "displayEmptyDistributionIntervals" }] : []));
8889
8939
  allowCustomRange = inject(FILTER_DATE_ALLOW_CUSTOM_RANGE);
8890
8940
  transloco = inject(TranslocoService);
8941
+ name = input(null, ...(ngDevMode ? [{ debugName: "name" }] : []));
8891
8942
  dateOptions = computed(() => translateAggregationToDateOptions(this.aggregation(), this.displayEmptyDistributionIntervals()), ...(ngDevMode ? [{ debugName: "dateOptions" }] : []));
8892
8943
  form = new FormGroup({
8893
8944
  option: new FormControl(null),
@@ -8900,26 +8951,28 @@ class AggregationDateComponent extends AggregationListComponent {
8900
8951
  lang = signal(this.transloco.getActiveLang(), ...(ngDevMode ? [{ debugName: "lang" }] : []));
8901
8952
  validSelection = signal(false, ...(ngDevMode ? [{ debugName: "validSelection" }] : []));
8902
8953
  formValue = toSignal(this.form.valueChanges, { initialValue: this.form.value });
8903
- customRangeLabel = computed(() => {
8904
- const { from, to } = this.formValue().customRange ?? {};
8905
- if (!from && !to)
8906
- return null;
8907
- const locale = this.lang();
8908
- const fmt = (iso) => iso ? new Date(iso).toLocaleDateString(locale) : "…";
8909
- return `${fmt(from ?? null)} – ${fmt(to ?? null)}`;
8910
- }, ...(ngDevMode ? [{ debugName: "customRangeLabel" }] : []));
8911
- constructor(destroyRef) {
8954
+ customRangeFrom = computed(() => {
8955
+ const from = this.formValue().customRange?.from;
8956
+ return from ? new Date(from).toLocaleDateString(this.lang()) : "";
8957
+ }, ...(ngDevMode ? [{ debugName: "customRangeFrom" }] : []));
8958
+ customRangeTo = computed(() => {
8959
+ const to = this.formValue().customRange?.to;
8960
+ return to ? new Date(to).toLocaleDateString(this.lang()) : "";
8961
+ }, ...(ngDevMode ? [{ debugName: "customRangeTo" }] : []));
8962
+ constructor() {
8912
8963
  super();
8913
- this.destroyRef = destroyRef;
8914
8964
  this.transloco.langChanges$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((lang) => {
8915
8965
  this.lang.set(lang);
8916
8966
  });
8917
8967
  // apply current date filter from queryParamsStore
8918
8968
  effect(() => {
8919
- this.updateForm(this.queryParamsStore.getFilter({
8920
- field: this.aggregation()?.column,
8921
- name: this.aggregation()?.name
8922
- }));
8969
+ const agg = this.aggregation();
8970
+ if (agg) {
8971
+ this.updateForm(this.queryParamsStore.getFilter({
8972
+ field: agg.column,
8973
+ name: agg.name
8974
+ }));
8975
+ }
8923
8976
  });
8924
8977
  this.form.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((changes) => {
8925
8978
  this.validSelection.set(!!changes.option &&
@@ -8937,6 +8990,9 @@ class AggregationDateComponent extends AggregationListComponent {
8937
8990
  }
8938
8991
  return null;
8939
8992
  }, ...(ngDevMode ? [{ debugName: "aggregation" }] : []));
8993
+ select() {
8994
+ this.selection.set(true);
8995
+ }
8940
8996
  apply() {
8941
8997
  try {
8942
8998
  const filter = this.getFormValueFilter();
@@ -8965,6 +9021,10 @@ class AggregationDateComponent extends AggregationListComponent {
8965
9021
  this.onClear.emit();
8966
9022
  }
8967
9023
  }
9024
+ selectAndOpenDialog() {
9025
+ this.select();
9026
+ this.dateRangeDialog()?.open();
9027
+ }
8968
9028
  onRangeSelected(range) {
8969
9029
  this.form.patchValue({
8970
9030
  option: "custom-range",
@@ -8993,6 +9053,12 @@ class AggregationDateComponent extends AggregationListComponent {
8993
9053
  from = filter.start;
8994
9054
  to = filter.end;
8995
9055
  break;
9056
+ case "and": {
9057
+ const filters = filter.filters;
9058
+ from = filters?.find((f) => f.operator === "gte")?.value ?? null;
9059
+ to = filters?.find((f) => f.operator === "lte")?.value ?? null;
9060
+ break;
9061
+ }
8996
9062
  }
8997
9063
  }
8998
9064
  const formValue = {
@@ -9013,13 +9079,15 @@ class AggregationDateComponent extends AggregationListComponent {
9013
9079
  if (value.option !== "custom-range") {
9014
9080
  const dateOption = this.dateOptions().find((option) => option.display === value.option);
9015
9081
  const aggregation = this.aggregation();
9016
- if (!aggregation) {
9082
+ const name = aggregation?.name ?? this.name() ?? undefined;
9083
+ const column = aggregation?.column ?? this.column() ?? undefined;
9084
+ if (!name && !column) {
9017
9085
  throw new Error("filters.aggregationNotFound");
9018
9086
  }
9019
9087
  return {
9020
- name: aggregation.name,
9088
+ name: name,
9021
9089
  operator: dateOption?.operator || "eq",
9022
- field: aggregation.column,
9090
+ field: column,
9023
9091
  display: dateOption?.display ?? "",
9024
9092
  filters: dateOption?.filters,
9025
9093
  value: dateOption?.value
@@ -9030,18 +9098,30 @@ class AggregationDateComponent extends AggregationListComponent {
9030
9098
  // if from is null, operator is lte
9031
9099
  // if both are not null, operator is between
9032
9100
  const aggregation = this.aggregation();
9033
- if (!aggregation) {
9101
+ const name = aggregation?.name ?? this.name() ?? undefined;
9102
+ const column = aggregation?.column ?? this.column() ?? undefined;
9103
+ if (!name && !column) {
9034
9104
  throw new Error("filters.aggregationNotFound");
9035
9105
  }
9036
9106
  const filter = {
9037
- name: aggregation.name,
9038
- field: aggregation.column,
9107
+ name: name,
9108
+ field: column,
9039
9109
  display: value.option
9040
9110
  };
9041
9111
  if (value.customRange.from && value.customRange.to) {
9042
- filter.operator = "between";
9043
- filter.start = value.customRange.from;
9044
- filter.end = value.customRange.to;
9112
+ filter.operator = "and";
9113
+ filter.filters = [
9114
+ {
9115
+ field: column,
9116
+ operator: "gte",
9117
+ value: value.customRange.from,
9118
+ },
9119
+ {
9120
+ field: column,
9121
+ operator: "lte",
9122
+ value: value.customRange.to
9123
+ }
9124
+ ];
9045
9125
  }
9046
9126
  else if (value.customRange.from) {
9047
9127
  filter.operator = "gte";
@@ -9058,12 +9138,13 @@ class AggregationDateComponent extends AggregationListComponent {
9058
9138
  }
9059
9139
  throw new Error("filters.filterInvalid");
9060
9140
  }
9061
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AggregationDateComponent, deps: [{ token: i0.DestroyRef }], target: i0.ɵɵFactoryTarget.Component });
9062
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: AggregationDateComponent, isStandalone: true, selector: "aggregation-date, AggregationDate, aggregationdate", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, displayEmptyDistributionIntervals: { classPropertyName: "displayEmptyDistributionIntervals", publicName: "displayEmptyDistributionIntervals", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "@container" }, providers: [provideTranslocoScope("filters")], viewQueries: [{ propertyName: "dateRangeDialog", first: true, predicate: AggregationDateRangeDialogComponent, descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<details [attr.open]=\"expanded()\" [attr.name]=\"id()\" class=\"group space-y-2\">\r\n <summary\r\n [class.cursor-pointer]=\"collapsible()\"\r\n class=\"m-0 flex h-8 w-full items-center pl-1 font-semibold select-none\"\r\n (click)=\"onHeaderClick($event)\">\r\n <ng-content select=\"label\">\r\n @if (aggregation()?.icon) {\r\n <i class=\"fa-fw {{ aggregation()?.icon }} mr-1\" aria-hidden=\"true\"></i>\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 <button\r\n variant=\"ghost\"\r\n size=\"icon\"\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 <i class=\"fa-fw far fa-filter-circle-xmark\" aria-hidden=\"true\"></i>\r\n <span class=\"sr-only\">{{ \"filters.clearFilters\" | transloco }}</span>\r\n </button>\r\n }\r\n\r\n @if (selection() && validSelection()) {\r\n <button\r\n variant=\"ghost\"\r\n size=\"icon\"\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 <i class=\"fa-fw far fa-filter\" aria-hidden=\"true\"></i>\r\n <span class=\"sr-only\">{{ \"filters.applyFilters\" | transloco }}</span>\r\n </button>\r\n }\r\n @if (collapsible()) {\r\n <button\r\n variant=\"none\"\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 </button>\r\n }\r\n </summary>\r\n\r\n <!-- content wrapper -->\r\n <form [formGroup]=\"form\">\r\n <ul\r\n class=\"scrollbar-thin flex max-h-[calc(var(--height,100%)-100px)] snap-y snap-start flex-col gap-1 overflow-auto pt-2\"\r\n role=\"list\">\r\n @for (option of dateOptions(); track $index) {\r\n <li\r\n role=\"listitem\"\r\n tabindex=\"0\"\r\n (click)=\"radio.click()\"\r\n [attr.aria-label]=\"option.display | syslang | transloco\"\r\n [class]=\"\r\n cn(\r\n 'flex p-0 px-2 leading-7',\r\n form.get('option')?.value === option.display && 'bg-accent',\r\n option.hidden && 'hidden',\r\n option.disabled && 'disabled pointer-events-none text-neutral-300'\r\n )\r\n \"\r\n [attr.aria-hidden]=\"option.disabled\">\r\n <input\r\n #radio\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-{{ option.display }}\"\r\n [attr.disabled]=\"option.disabled ? true : null\"\r\n [attr.aria-disabled]=\"option.disabled\"\r\n (click)=\"select()\"\r\n value=\"{{ option.display }}\" />\r\n\r\n <label\r\n for=\"date-filter-{{ option.display }}\"\r\n class=\"grow cursor-pointer p-1\">\r\n {{ option.display | syslang | transloco }}\r\n </label>\r\n </li>\r\n }\r\n\r\n @if (allowCustomRange) {\r\n <li\r\n role=\"listitem\"\r\n aria-label=\"open date range picker\"\r\n class=\"flex px-2 leading-7\"\r\n [class.select]=\"form.get('option')?.value === 'custom-range'\">\r\n <input\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-range-dialog\"\r\n value=\"custom-range\"\r\n (click)=\"select()\" />\r\n\r\n <label\r\n for=\"date-filter-range-dialog\"\r\n class=\"flex grow cursor-pointer items-center gap-1 p-1\"\r\n (click)=\"dateRangeDialog()?.open()\">\r\n @let rangeLabel = customRangeLabel();\r\n @if (rangeLabel) {\r\n <span class=\"w-full\">{{ rangeLabel }}</span>\r\n } @else {\r\n <span>{{ \"filters.selectDateRange\" | transloco }}</span>\r\n }\r\n </label>\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: ButtonComponent, selector: "button", inputs: ["class", "variant", "decoration", "scheme", "iconOnly", "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", inputs: ["class"] }, { kind: "component", type: AggregationDateRangeDialogComponent, selector: "aggregation-date-range-dialog", inputs: ["min", "max", "lang", "useDateRange"], outputs: ["rangeSelected"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }, { kind: "pipe", type: SyslangPipe, name: "syslang" }] });
9141
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AggregationDateComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
9142
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: AggregationDateComponent, isStandalone: true, selector: "aggregation-date, AggregationDate, aggregationdate", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, displayEmptyDistributionIntervals: { classPropertyName: "displayEmptyDistributionIntervals", publicName: "displayEmptyDistributionIntervals", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "@container" }, providers: [provideTranslocoScope("filters")], viewQueries: [{ propertyName: "dateRangeDialog", first: true, predicate: AggregationDateRangeDialogComponent, descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<details [attr.open]=\"expanded()\" [attr.name]=\"id()\" class=\"group space-y-2\">\r\n <summary\r\n [class.cursor-pointer]=\"collapsible()\"\r\n class=\"m-0 flex h-8 w-full items-center pl-1 font-semibold select-none\"\r\n (click)=\"onHeaderClick($event)\">\r\n <ng-content select=\"label\">\r\n @if (aggregation()?.icon) {\r\n <i class=\"fa-fw {{ aggregation()?.icon }} mr-1\" aria-hidden=\"true\"></i>\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 <button\r\n variant=\"ghost\"\r\n size=\"icon\"\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 <i class=\"fa-fw far fa-filter-circle-xmark\" aria-hidden=\"true\"></i>\r\n <span class=\"sr-only\">{{ \"filters.clearFilters\" | transloco }}</span>\r\n </button>\r\n }\r\n\r\n @if (selection() && validSelection()) {\r\n <button\r\n variant=\"ghost\"\r\n size=\"icon\"\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 <i class=\"fa-fw far fa-filter\" aria-hidden=\"true\"></i>\r\n <span class=\"sr-only\">{{ \"filters.applyFilters\" | transloco }}</span>\r\n </button>\r\n }\r\n @if (collapsible()) {\r\n <button\r\n variant=\"none\"\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 </button>\r\n }\r\n </summary>\r\n\r\n <!-- content wrapper -->\r\n <form [formGroup]=\"form\">\r\n <ul\r\n class=\"scrollbar-thin flex max-h-[calc(var(--height,100%)-100px)] snap-y snap-start flex-col gap-1 overflow-auto pt-2\"\r\n role=\"list\">\r\n @for (option of dateOptions(); track $index) {\r\n <li\r\n role=\"listitem\"\r\n tabindex=\"0\"\r\n (click)=\"radio.click()\"\r\n [attr.aria-label]=\"option.display | syslang | transloco\"\r\n [class]=\"\r\n cn(\r\n 'flex p-0 px-2 leading-7',\r\n form.get('option')?.value === option.display && 'bg-accent',\r\n option.hidden && 'hidden',\r\n option.disabled && 'disabled pointer-events-none text-neutral-300'\r\n )\r\n \"\r\n [attr.aria-hidden]=\"option.disabled\">\r\n <input\r\n #radio\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-{{ option.display }}\"\r\n [attr.disabled]=\"option.disabled ? true : null\"\r\n [attr.aria-disabled]=\"option.disabled\"\r\n (click)=\"select()\"\r\n value=\"{{ option.display }}\" />\r\n\r\n <label\r\n for=\"date-filter-{{ option.display }}\"\r\n class=\"grow cursor-pointer p-1\">\r\n {{ option.display | syslang | transloco }}\r\n </label>\r\n </li>\r\n }\r\n\r\n @if (allowCustomRange) {\r\n <li\r\n role=\"listitem\"\r\n aria-label=\"open date range picker\"\r\n class=\"flex px-2 leading-7\"\r\n [class.select]=\"form.get('option')?.value === 'custom-range'\">\r\n <input\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-range-dialog\"\r\n value=\"custom-range\"\r\n (click)=\"select()\" />\r\n <div\r\n class=\"@container flex grow justify-end gap-1 p-1 @max-[340px]:flex-wrap\">\r\n <div class=\"flex gap-1\">\r\n <label for=\"datepicker-range-start\" class=\"min-w-10 truncate\">{{\r\n \"filters.from\" | transloco\r\n }}</label>\r\n <input\r\n id=\"datepicker-range-start\"\r\n name=\"start\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeFrom()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n <div class=\"flex gap-1\">\r\n <label\r\n for=\"datepicker-range-end\"\r\n class=\"min-w-10 truncate text-right\"\r\n >{{ \"filters.to\" | transloco }}</label\r\n >\r\n <input\r\n id=\"datepicker-range-end\"\r\n name=\"end\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeTo()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n </div>\r\n </li>\r\n }\r\n </ul>\r\n </form>\r\n</details>\r\n\r\n<aggregation-date-range-dialog\r\n [lang]=\"lang()\"\r\n [useDateRange]=\"false\"\r\n [min]=\"form.get('customRange.from')?.value || undefined\"\r\n [max]=\"form.get('customRange.to')?.value || undefined\"\r\n (rangeSelected)=\"onRangeSelected($event)\" />\r\n", styles: [":host{display:block;min-width:200px}ul[role=list]{scrollbar-width:thin}\n"], dependencies: [{ kind: "directive", type: InputComponent, selector: "input[type=\"text\"], input[type=\"email\"], input[type=\"number\"], input[type=\"password\"], input[type=\"tel\"], input[type=\"url\"], input[type=\"time\"], input[type=\"file\"]", inputs: ["class", "variant", "decoration"] }, { kind: "directive", type: ButtonComponent, selector: "button", inputs: ["class", "variant", "decoration", "scheme", "iconOnly", "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", inputs: ["class"] }, { kind: "component", type: AggregationDateRangeDialogComponent, selector: "aggregation-date-range-dialog", inputs: ["min", "max", "lang", "useDateRange"], outputs: ["rangeSelected"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }, { kind: "pipe", type: SyslangPipe, name: "syslang" }] });
9063
9143
  }
9064
9144
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AggregationDateComponent, decorators: [{
9065
9145
  type: Component,
9066
9146
  args: [{ selector: "aggregation-date, AggregationDate, aggregationdate", standalone: true, providers: [provideTranslocoScope("filters")], imports: [
9147
+ InputComponent,
9067
9148
  ButtonComponent,
9068
9149
  ListItemComponent,
9069
9150
  ReactiveFormsModule,
@@ -9073,8 +9154,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
9073
9154
  AggregationDateRangeDialogComponent
9074
9155
  ], host: {
9075
9156
  class: "@container"
9076
- }, 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 @if (aggregation()?.icon) {\r\n <i class=\"fa-fw {{ aggregation()?.icon }} mr-1\" aria-hidden=\"true\"></i>\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 <button\r\n variant=\"ghost\"\r\n size=\"icon\"\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 <i class=\"fa-fw far fa-filter-circle-xmark\" aria-hidden=\"true\"></i>\r\n <span class=\"sr-only\">{{ \"filters.clearFilters\" | transloco }}</span>\r\n </button>\r\n }\r\n\r\n @if (selection() && validSelection()) {\r\n <button\r\n variant=\"ghost\"\r\n size=\"icon\"\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 <i class=\"fa-fw far fa-filter\" aria-hidden=\"true\"></i>\r\n <span class=\"sr-only\">{{ \"filters.applyFilters\" | transloco }}</span>\r\n </button>\r\n }\r\n @if (collapsible()) {\r\n <button\r\n variant=\"none\"\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 </button>\r\n }\r\n </summary>\r\n\r\n <!-- content wrapper -->\r\n <form [formGroup]=\"form\">\r\n <ul\r\n class=\"scrollbar-thin flex max-h-[calc(var(--height,100%)-100px)] snap-y snap-start flex-col gap-1 overflow-auto pt-2\"\r\n role=\"list\">\r\n @for (option of dateOptions(); track $index) {\r\n <li\r\n role=\"listitem\"\r\n tabindex=\"0\"\r\n (click)=\"radio.click()\"\r\n [attr.aria-label]=\"option.display | syslang | transloco\"\r\n [class]=\"\r\n cn(\r\n 'flex p-0 px-2 leading-7',\r\n form.get('option')?.value === option.display && 'bg-accent',\r\n option.hidden && 'hidden',\r\n option.disabled && 'disabled pointer-events-none text-neutral-300'\r\n )\r\n \"\r\n [attr.aria-hidden]=\"option.disabled\">\r\n <input\r\n #radio\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-{{ option.display }}\"\r\n [attr.disabled]=\"option.disabled ? true : null\"\r\n [attr.aria-disabled]=\"option.disabled\"\r\n (click)=\"select()\"\r\n value=\"{{ option.display }}\" />\r\n\r\n <label\r\n for=\"date-filter-{{ option.display }}\"\r\n class=\"grow cursor-pointer p-1\">\r\n {{ option.display | syslang | transloco }}\r\n </label>\r\n </li>\r\n }\r\n\r\n @if (allowCustomRange) {\r\n <li\r\n role=\"listitem\"\r\n aria-label=\"open date range picker\"\r\n class=\"flex px-2 leading-7\"\r\n [class.select]=\"form.get('option')?.value === 'custom-range'\">\r\n <input\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-range-dialog\"\r\n value=\"custom-range\"\r\n (click)=\"select()\" />\r\n\r\n <label\r\n for=\"date-filter-range-dialog\"\r\n class=\"flex grow cursor-pointer items-center gap-1 p-1\"\r\n (click)=\"dateRangeDialog()?.open()\">\r\n @let rangeLabel = customRangeLabel();\r\n @if (rangeLabel) {\r\n <span class=\"w-full\">{{ rangeLabel }}</span>\r\n } @else {\r\n <span>{{ \"filters.selectDateRange\" | transloco }}</span>\r\n }\r\n </label>\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"] }]
9077
- }], ctorParameters: () => [{ type: i0.DestroyRef }], propDecorators: { dateRangeDialog: [{ type: i0.ViewChild, args: [i0.forwardRef(() => AggregationDateRangeDialogComponent), { isSignal: true }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], displayEmptyDistributionIntervals: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayEmptyDistributionIntervals", required: false }] }] } });
9157
+ }, 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 @if (aggregation()?.icon) {\r\n <i class=\"fa-fw {{ aggregation()?.icon }} mr-1\" aria-hidden=\"true\"></i>\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 <button\r\n variant=\"ghost\"\r\n size=\"icon\"\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 <i class=\"fa-fw far fa-filter-circle-xmark\" aria-hidden=\"true\"></i>\r\n <span class=\"sr-only\">{{ \"filters.clearFilters\" | transloco }}</span>\r\n </button>\r\n }\r\n\r\n @if (selection() && validSelection()) {\r\n <button\r\n variant=\"ghost\"\r\n size=\"icon\"\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 <i class=\"fa-fw far fa-filter\" aria-hidden=\"true\"></i>\r\n <span class=\"sr-only\">{{ \"filters.applyFilters\" | transloco }}</span>\r\n </button>\r\n }\r\n @if (collapsible()) {\r\n <button\r\n variant=\"none\"\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 </button>\r\n }\r\n </summary>\r\n\r\n <!-- content wrapper -->\r\n <form [formGroup]=\"form\">\r\n <ul\r\n class=\"scrollbar-thin flex max-h-[calc(var(--height,100%)-100px)] snap-y snap-start flex-col gap-1 overflow-auto pt-2\"\r\n role=\"list\">\r\n @for (option of dateOptions(); track $index) {\r\n <li\r\n role=\"listitem\"\r\n tabindex=\"0\"\r\n (click)=\"radio.click()\"\r\n [attr.aria-label]=\"option.display | syslang | transloco\"\r\n [class]=\"\r\n cn(\r\n 'flex p-0 px-2 leading-7',\r\n form.get('option')?.value === option.display && 'bg-accent',\r\n option.hidden && 'hidden',\r\n option.disabled && 'disabled pointer-events-none text-neutral-300'\r\n )\r\n \"\r\n [attr.aria-hidden]=\"option.disabled\">\r\n <input\r\n #radio\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-{{ option.display }}\"\r\n [attr.disabled]=\"option.disabled ? true : null\"\r\n [attr.aria-disabled]=\"option.disabled\"\r\n (click)=\"select()\"\r\n value=\"{{ option.display }}\" />\r\n\r\n <label\r\n for=\"date-filter-{{ option.display }}\"\r\n class=\"grow cursor-pointer p-1\">\r\n {{ option.display | syslang | transloco }}\r\n </label>\r\n </li>\r\n }\r\n\r\n @if (allowCustomRange) {\r\n <li\r\n role=\"listitem\"\r\n aria-label=\"open date range picker\"\r\n class=\"flex px-2 leading-7\"\r\n [class.select]=\"form.get('option')?.value === 'custom-range'\">\r\n <input\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-range-dialog\"\r\n value=\"custom-range\"\r\n (click)=\"select()\" />\r\n <div\r\n class=\"@container flex grow justify-end gap-1 p-1 @max-[340px]:flex-wrap\">\r\n <div class=\"flex gap-1\">\r\n <label for=\"datepicker-range-start\" class=\"min-w-10 truncate\">{{\r\n \"filters.from\" | transloco\r\n }}</label>\r\n <input\r\n id=\"datepicker-range-start\"\r\n name=\"start\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeFrom()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n <div class=\"flex gap-1\">\r\n <label\r\n for=\"datepicker-range-end\"\r\n class=\"min-w-10 truncate text-right\"\r\n >{{ \"filters.to\" | transloco }}</label\r\n >\r\n <input\r\n id=\"datepicker-range-end\"\r\n name=\"end\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeTo()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n </div>\r\n </li>\r\n }\r\n </ul>\r\n </form>\r\n</details>\r\n\r\n<aggregation-date-range-dialog\r\n [lang]=\"lang()\"\r\n [useDateRange]=\"false\"\r\n [min]=\"form.get('customRange.from')?.value || undefined\"\r\n [max]=\"form.get('customRange.to')?.value || undefined\"\r\n (rangeSelected)=\"onRangeSelected($event)\" />\r\n", styles: [":host{display:block;min-width:200px}ul[role=list]{scrollbar-width:thin}\n"] }]
9158
+ }], ctorParameters: () => [], propDecorators: { dateRangeDialog: [{ type: i0.ViewChild, args: [i0.forwardRef(() => AggregationDateRangeDialogComponent), { isSignal: true }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], displayEmptyDistributionIntervals: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayEmptyDistributionIntervals", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }] } });
9078
9159
 
9079
9160
  /**
9080
9161
  * Component that allows users to select a date or a date range for filtering search results.
@@ -9083,7 +9164,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
9083
9164
  */
9084
9165
  class DateComponent extends AggregationDateComponent {
9085
9166
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: DateComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
9086
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: DateComponent, isStandalone: true, selector: "date-filter,DateFilter", host: { classAttribute: "@container" }, providers: [provideTranslocoScope("filters")], usesInheritance: true, ngImport: i0, template: "<details [attr.open]=\"expanded()\" [attr.name]=\"id()\" class=\"group space-y-2\">\r\n <summary\r\n [class.cursor-pointer]=\"collapsible()\"\r\n class=\"m-0 flex h-8 w-full items-center pl-1 font-semibold select-none\"\r\n (click)=\"onHeaderClick($event)\">\r\n <ng-content select=\"label\">\r\n @if (aggregation()?.icon) {\r\n <i class=\"fa-fw {{ aggregation()?.icon }} mr-1\" aria-hidden=\"true\"></i>\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 <button\r\n variant=\"ghost\"\r\n size=\"icon\"\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 <i class=\"fa-fw far fa-filter-circle-xmark\" aria-hidden=\"true\"></i>\r\n <span class=\"sr-only\">{{ \"filters.clearFilters\" | transloco }}</span>\r\n </button>\r\n }\r\n\r\n @if (selection() && validSelection()) {\r\n <button\r\n variant=\"ghost\"\r\n size=\"icon\"\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 <i class=\"fa-fw far fa-filter\" aria-hidden=\"true\"></i>\r\n <span class=\"sr-only\">{{ \"filters.applyFilters\" | transloco }}</span>\r\n </button>\r\n }\r\n @if (collapsible()) {\r\n <button\r\n variant=\"none\"\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 </button>\r\n }\r\n </summary>\r\n\r\n <!-- content wrapper -->\r\n <form [formGroup]=\"form\">\r\n <ul\r\n class=\"scrollbar-thin flex max-h-[calc(var(--height,100%)-100px)] snap-y snap-start flex-col gap-1 overflow-auto pt-2\"\r\n role=\"list\">\r\n @for (option of dateOptions(); track $index) {\r\n <li\r\n role=\"listitem\"\r\n tabindex=\"0\"\r\n (click)=\"radio.click()\"\r\n [attr.aria-label]=\"option.display | syslang | transloco\"\r\n [class]=\"\r\n cn(\r\n 'flex p-0 px-2 leading-7',\r\n form.get('option')?.value === option.display && 'bg-accent',\r\n option.hidden && 'hidden',\r\n option.disabled && 'disabled pointer-events-none text-neutral-300'\r\n )\r\n \"\r\n [attr.aria-hidden]=\"option.disabled\">\r\n <input\r\n #radio\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-{{ option.display }}\"\r\n [attr.disabled]=\"option.disabled ? true : null\"\r\n [attr.aria-disabled]=\"option.disabled\"\r\n (click)=\"select()\"\r\n value=\"{{ option.display }}\" />\r\n\r\n <label\r\n for=\"date-filter-{{ option.display }}\"\r\n class=\"grow cursor-pointer p-1\">\r\n {{ option.display | syslang | transloco }}\r\n </label>\r\n </li>\r\n }\r\n\r\n @if (allowCustomRange) {\r\n <li\r\n role=\"listitem\"\r\n aria-label=\"open date range picker\"\r\n class=\"flex px-2 leading-7\"\r\n [class.select]=\"form.get('option')?.value === 'custom-range'\">\r\n <input\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-range-dialog\"\r\n value=\"custom-range\"\r\n (click)=\"select()\" />\r\n\r\n <label\r\n for=\"date-filter-range-dialog\"\r\n class=\"flex grow cursor-pointer items-center gap-1 p-1\"\r\n (click)=\"dateRangeDialog()?.open()\">\r\n @let rangeLabel = customRangeLabel();\r\n @if (rangeLabel) {\r\n <span class=\"w-full\">{{ rangeLabel }}</span>\r\n } @else {\r\n <span>{{ \"filters.selectDateRange\" | transloco }}</span>\r\n }\r\n </label>\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: ButtonComponent, selector: "button", inputs: ["class", "variant", "decoration", "scheme", "iconOnly", "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", inputs: ["class"] }, { kind: "component", type: AggregationDateRangeDialogComponent, selector: "aggregation-date-range-dialog", inputs: ["min", "max", "lang", "useDateRange"], outputs: ["rangeSelected"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }, { kind: "pipe", type: SyslangPipe, name: "syslang" }] });
9167
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: DateComponent, isStandalone: true, selector: "date-filter,DateFilter", host: { classAttribute: "@container" }, providers: [provideTranslocoScope("filters")], usesInheritance: true, ngImport: i0, template: "<details [attr.open]=\"expanded()\" [attr.name]=\"id()\" class=\"group space-y-2\">\r\n <summary\r\n [class.cursor-pointer]=\"collapsible()\"\r\n class=\"m-0 flex h-8 w-full items-center pl-1 font-semibold select-none\"\r\n (click)=\"onHeaderClick($event)\">\r\n <ng-content select=\"label\">\r\n @if (aggregation()?.icon) {\r\n <i class=\"fa-fw {{ aggregation()?.icon }} mr-1\" aria-hidden=\"true\"></i>\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 <button\r\n variant=\"ghost\"\r\n size=\"icon\"\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 <i class=\"fa-fw far fa-filter-circle-xmark\" aria-hidden=\"true\"></i>\r\n <span class=\"sr-only\">{{ \"filters.clearFilters\" | transloco }}</span>\r\n </button>\r\n }\r\n\r\n @if (selection() && validSelection()) {\r\n <button\r\n variant=\"ghost\"\r\n size=\"icon\"\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 <i class=\"fa-fw far fa-filter\" aria-hidden=\"true\"></i>\r\n <span class=\"sr-only\">{{ \"filters.applyFilters\" | transloco }}</span>\r\n </button>\r\n }\r\n @if (collapsible()) {\r\n <button\r\n variant=\"none\"\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 </button>\r\n }\r\n </summary>\r\n\r\n <!-- content wrapper -->\r\n <form [formGroup]=\"form\">\r\n <ul\r\n class=\"scrollbar-thin flex max-h-[calc(var(--height,100%)-100px)] snap-y snap-start flex-col gap-1 overflow-auto pt-2\"\r\n role=\"list\">\r\n @for (option of dateOptions(); track $index) {\r\n <li\r\n role=\"listitem\"\r\n tabindex=\"0\"\r\n (click)=\"radio.click()\"\r\n [attr.aria-label]=\"option.display | syslang | transloco\"\r\n [class]=\"\r\n cn(\r\n 'flex p-0 px-2 leading-7',\r\n form.get('option')?.value === option.display && 'bg-accent',\r\n option.hidden && 'hidden',\r\n option.disabled && 'disabled pointer-events-none text-neutral-300'\r\n )\r\n \"\r\n [attr.aria-hidden]=\"option.disabled\">\r\n <input\r\n #radio\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-{{ option.display }}\"\r\n [attr.disabled]=\"option.disabled ? true : null\"\r\n [attr.aria-disabled]=\"option.disabled\"\r\n (click)=\"select()\"\r\n value=\"{{ option.display }}\" />\r\n\r\n <label\r\n for=\"date-filter-{{ option.display }}\"\r\n class=\"grow cursor-pointer p-1\">\r\n {{ option.display | syslang | transloco }}\r\n </label>\r\n </li>\r\n }\r\n\r\n @if (allowCustomRange) {\r\n <li\r\n role=\"listitem\"\r\n aria-label=\"open date range picker\"\r\n class=\"flex px-2 leading-7\"\r\n [class.select]=\"form.get('option')?.value === 'custom-range'\">\r\n <input\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-range-dialog\"\r\n value=\"custom-range\"\r\n (click)=\"select()\" />\r\n <div\r\n class=\"@container flex grow justify-end gap-1 p-1 @max-[340px]:flex-wrap\">\r\n <div class=\"flex gap-1\">\r\n <label for=\"datepicker-range-start\" class=\"min-w-10 truncate\">{{\r\n \"filters.from\" | transloco\r\n }}</label>\r\n <input\r\n id=\"datepicker-range-start\"\r\n name=\"start\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeFrom()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n <div class=\"flex gap-1\">\r\n <label\r\n for=\"datepicker-range-end\"\r\n class=\"min-w-10 truncate text-right\"\r\n >{{ \"filters.to\" | transloco }}</label\r\n >\r\n <input\r\n id=\"datepicker-range-end\"\r\n name=\"end\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeTo()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n </div>\r\n </li>\r\n }\r\n </ul>\r\n </form>\r\n</details>\r\n\r\n<aggregation-date-range-dialog\r\n [lang]=\"lang()\"\r\n [useDateRange]=\"false\"\r\n [min]=\"form.get('customRange.from')?.value || undefined\"\r\n [max]=\"form.get('customRange.to')?.value || undefined\"\r\n (rangeSelected)=\"onRangeSelected($event)\" />\r\n", styles: [":host{display:block;min-width:200px}ul[role=list]{scrollbar-width:thin}\n"], dependencies: [{ kind: "directive", type: ButtonComponent, selector: "button", inputs: ["class", "variant", "decoration", "scheme", "iconOnly", "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", inputs: ["class"] }, { kind: "component", type: AggregationDateRangeDialogComponent, selector: "aggregation-date-range-dialog", inputs: ["min", "max", "lang", "useDateRange"], outputs: ["rangeSelected"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }, { kind: "pipe", type: SyslangPipe, name: "syslang" }] });
9087
9168
  }
9088
9169
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: DateComponent, decorators: [{
9089
9170
  type: Component,
@@ -9097,7 +9178,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
9097
9178
  AggregationDateRangeDialogComponent
9098
9179
  ], host: {
9099
9180
  class: "@container"
9100
- }, 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 @if (aggregation()?.icon) {\r\n <i class=\"fa-fw {{ aggregation()?.icon }} mr-1\" aria-hidden=\"true\"></i>\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 <button\r\n variant=\"ghost\"\r\n size=\"icon\"\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 <i class=\"fa-fw far fa-filter-circle-xmark\" aria-hidden=\"true\"></i>\r\n <span class=\"sr-only\">{{ \"filters.clearFilters\" | transloco }}</span>\r\n </button>\r\n }\r\n\r\n @if (selection() && validSelection()) {\r\n <button\r\n variant=\"ghost\"\r\n size=\"icon\"\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 <i class=\"fa-fw far fa-filter\" aria-hidden=\"true\"></i>\r\n <span class=\"sr-only\">{{ \"filters.applyFilters\" | transloco }}</span>\r\n </button>\r\n }\r\n @if (collapsible()) {\r\n <button\r\n variant=\"none\"\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 </button>\r\n }\r\n </summary>\r\n\r\n <!-- content wrapper -->\r\n <form [formGroup]=\"form\">\r\n <ul\r\n class=\"scrollbar-thin flex max-h-[calc(var(--height,100%)-100px)] snap-y snap-start flex-col gap-1 overflow-auto pt-2\"\r\n role=\"list\">\r\n @for (option of dateOptions(); track $index) {\r\n <li\r\n role=\"listitem\"\r\n tabindex=\"0\"\r\n (click)=\"radio.click()\"\r\n [attr.aria-label]=\"option.display | syslang | transloco\"\r\n [class]=\"\r\n cn(\r\n 'flex p-0 px-2 leading-7',\r\n form.get('option')?.value === option.display && 'bg-accent',\r\n option.hidden && 'hidden',\r\n option.disabled && 'disabled pointer-events-none text-neutral-300'\r\n )\r\n \"\r\n [attr.aria-hidden]=\"option.disabled\">\r\n <input\r\n #radio\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-{{ option.display }}\"\r\n [attr.disabled]=\"option.disabled ? true : null\"\r\n [attr.aria-disabled]=\"option.disabled\"\r\n (click)=\"select()\"\r\n value=\"{{ option.display }}\" />\r\n\r\n <label\r\n for=\"date-filter-{{ option.display }}\"\r\n class=\"grow cursor-pointer p-1\">\r\n {{ option.display | syslang | transloco }}\r\n </label>\r\n </li>\r\n }\r\n\r\n @if (allowCustomRange) {\r\n <li\r\n role=\"listitem\"\r\n aria-label=\"open date range picker\"\r\n class=\"flex px-2 leading-7\"\r\n [class.select]=\"form.get('option')?.value === 'custom-range'\">\r\n <input\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-range-dialog\"\r\n value=\"custom-range\"\r\n (click)=\"select()\" />\r\n\r\n <label\r\n for=\"date-filter-range-dialog\"\r\n class=\"flex grow cursor-pointer items-center gap-1 p-1\"\r\n (click)=\"dateRangeDialog()?.open()\">\r\n @let rangeLabel = customRangeLabel();\r\n @if (rangeLabel) {\r\n <span class=\"w-full\">{{ rangeLabel }}</span>\r\n } @else {\r\n <span>{{ \"filters.selectDateRange\" | transloco }}</span>\r\n }\r\n </label>\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"] }]
9181
+ }, 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 @if (aggregation()?.icon) {\r\n <i class=\"fa-fw {{ aggregation()?.icon }} mr-1\" aria-hidden=\"true\"></i>\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 <button\r\n variant=\"ghost\"\r\n size=\"icon\"\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 <i class=\"fa-fw far fa-filter-circle-xmark\" aria-hidden=\"true\"></i>\r\n <span class=\"sr-only\">{{ \"filters.clearFilters\" | transloco }}</span>\r\n </button>\r\n }\r\n\r\n @if (selection() && validSelection()) {\r\n <button\r\n variant=\"ghost\"\r\n size=\"icon\"\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 <i class=\"fa-fw far fa-filter\" aria-hidden=\"true\"></i>\r\n <span class=\"sr-only\">{{ \"filters.applyFilters\" | transloco }}</span>\r\n </button>\r\n }\r\n @if (collapsible()) {\r\n <button\r\n variant=\"none\"\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 </button>\r\n }\r\n </summary>\r\n\r\n <!-- content wrapper -->\r\n <form [formGroup]=\"form\">\r\n <ul\r\n class=\"scrollbar-thin flex max-h-[calc(var(--height,100%)-100px)] snap-y snap-start flex-col gap-1 overflow-auto pt-2\"\r\n role=\"list\">\r\n @for (option of dateOptions(); track $index) {\r\n <li\r\n role=\"listitem\"\r\n tabindex=\"0\"\r\n (click)=\"radio.click()\"\r\n [attr.aria-label]=\"option.display | syslang | transloco\"\r\n [class]=\"\r\n cn(\r\n 'flex p-0 px-2 leading-7',\r\n form.get('option')?.value === option.display && 'bg-accent',\r\n option.hidden && 'hidden',\r\n option.disabled && 'disabled pointer-events-none text-neutral-300'\r\n )\r\n \"\r\n [attr.aria-hidden]=\"option.disabled\">\r\n <input\r\n #radio\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-{{ option.display }}\"\r\n [attr.disabled]=\"option.disabled ? true : null\"\r\n [attr.aria-disabled]=\"option.disabled\"\r\n (click)=\"select()\"\r\n value=\"{{ option.display }}\" />\r\n\r\n <label\r\n for=\"date-filter-{{ option.display }}\"\r\n class=\"grow cursor-pointer p-1\">\r\n {{ option.display | syslang | transloco }}\r\n </label>\r\n </li>\r\n }\r\n\r\n @if (allowCustomRange) {\r\n <li\r\n role=\"listitem\"\r\n aria-label=\"open date range picker\"\r\n class=\"flex px-2 leading-7\"\r\n [class.select]=\"form.get('option')?.value === 'custom-range'\">\r\n <input\r\n type=\"radio\"\r\n formControlName=\"option\"\r\n id=\"date-filter-range-dialog\"\r\n value=\"custom-range\"\r\n (click)=\"select()\" />\r\n <div\r\n class=\"@container flex grow justify-end gap-1 p-1 @max-[340px]:flex-wrap\">\r\n <div class=\"flex gap-1\">\r\n <label for=\"datepicker-range-start\" class=\"min-w-10 truncate\">{{\r\n \"filters.from\" | transloco\r\n }}</label>\r\n <input\r\n id=\"datepicker-range-start\"\r\n name=\"start\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeFrom()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n <div class=\"flex gap-1\">\r\n <label\r\n for=\"datepicker-range-end\"\r\n class=\"min-w-10 truncate text-right\"\r\n >{{ \"filters.to\" | transloco }}</label\r\n >\r\n <input\r\n id=\"datepicker-range-end\"\r\n name=\"end\"\r\n type=\"text\"\r\n readonly\r\n class=\"h-8 max-w-[13ch] min-w-[13ch]\"\r\n [value]=\"customRangeTo()\"\r\n (click)=\"selectAndOpenDialog()\" />\r\n </div>\r\n </div>\r\n </li>\r\n }\r\n </ul>\r\n </form>\r\n</details>\r\n\r\n<aggregation-date-range-dialog\r\n [lang]=\"lang()\"\r\n [useDateRange]=\"false\"\r\n [min]=\"form.get('customRange.from')?.value || undefined\"\r\n [max]=\"form.get('customRange.to')?.value || undefined\"\r\n (rangeSelected)=\"onRangeSelected($event)\" />\r\n", styles: [":host{display:block;min-width:200px}ul[role=list]{scrollbar-width:thin}\n"] }]
9101
9182
  }] });
9102
9183
 
9103
9184
  class ArticleEntities {
@@ -10050,6 +10131,7 @@ class ChangePasswordComponent {
10050
10131
  principalStore = inject(PrincipalStore);
10051
10132
  applicationService = inject(ApplicationService);
10052
10133
  location = inject(Location);
10134
+ auditService = inject(AuditService);
10053
10135
  currentPassword = model("", ...(ngDevMode ? [{ debugName: "currentPassword" }] : []));
10054
10136
  newPassword = model("", ...(ngDevMode ? [{ debugName: "newPassword" }] : []));
10055
10137
  confirmPassword = model("", ...(ngDevMode ? [{ debugName: "confirmPassword" }] : []));
@@ -10095,11 +10177,13 @@ class ChangePasswordComponent {
10095
10177
  const msg = res.message ?? this.transloco.translate("login.passwordChangeFailed");
10096
10178
  this.errorMsg.set(msg);
10097
10179
  notify.error(msg, { duration: 2500 });
10180
+ this.auditService.notify({ type: 'Change_Password_Failed' });
10098
10181
  return;
10099
10182
  }
10100
10183
  notify.success(this.transloco.translate("login.passwordChanged"), {
10101
10184
  duration: 2000
10102
10185
  });
10186
+ this.auditService.notify({ type: 'Change_Password_Success_Form' });
10103
10187
  const username = this.effectiveUsername();
10104
10188
  if (!username) {
10105
10189
  notify.info(this.transloco.translate("login.loginAfterPasswordChangeMissingUser"), { duration: 3000 });
@@ -10388,6 +10472,7 @@ class ForgotPasswordComponent {
10388
10472
  cancel = output();
10389
10473
  success = output();
10390
10474
  transloco = inject(TranslocoService);
10475
+ auditService = inject(AuditService);
10391
10476
  userName = model("", ...(ngDevMode ? [{ debugName: "userName" }] : []));
10392
10477
  pending = signal(false, ...(ngDevMode ? [{ debugName: "pending" }] : []));
10393
10478
  errorMsg = signal(null, ...(ngDevMode ? [{ debugName: "errorMsg" }] : []));
@@ -10400,6 +10485,9 @@ class ForgotPasswordComponent {
10400
10485
  notify.success(this.transloco.translate("login.resetEmailSent", {
10401
10486
  email: res.email ?? ""
10402
10487
  }));
10488
+ this.auditService.notify({
10489
+ type: 'Send_Reset_Password_Link'
10490
+ });
10403
10491
  this.success.emit();
10404
10492
  }
10405
10493
  catch (e) {
@@ -10536,6 +10624,7 @@ class SignInComponent {
10536
10624
  applicationService = inject(ApplicationService);
10537
10625
  principalStore = inject(PrincipalStore);
10538
10626
  transloco = inject(TranslocoService);
10627
+ auditService = inject(AuditService);
10539
10628
  authenticated = signal(isAuthenticated(), ...(ngDevMode ? [{ debugName: "authenticated" }] : []));
10540
10629
  user = signal(getState(this.principalStore), ...(ngDevMode ? [{ debugName: "user" }] : []));
10541
10630
  expiresSoonNotified = signal(false, ...(ngDevMode ? [{ debugName: "expiresSoonNotified" }] : []));
@@ -10586,8 +10675,13 @@ class SignInComponent {
10586
10675
  this.router.navigate(["/login"]);
10587
10676
  }
10588
10677
  async handleLogin() {
10589
- login().catch(error => {
10678
+ login().then((result) => {
10679
+ if (result) {
10680
+ this.auditService.notifyLogin();
10681
+ }
10682
+ }).catch(error => {
10590
10683
  warn("An error occurred while logging in", error);
10684
+ this.auditService.notify({ type: 'Login_Denied' });
10591
10685
  this.router.navigate(["error"]);
10592
10686
  });
10593
10687
  }
@@ -10596,8 +10690,11 @@ class SignInComponent {
10596
10690
  return;
10597
10691
  try {
10598
10692
  const response = await login(this.credentials());
10599
- if (!response)
10693
+ if (!response) {
10694
+ this.auditService.notify({ type: 'Login_Denied' });
10600
10695
  return;
10696
+ }
10697
+ this.auditService.notifyLogin();
10601
10698
  const { createRoutes = false } = globalConfig;
10602
10699
  await this.applicationService.initialize(createRoutes);
10603
10700
  this.checkPasswordExpiresSoon();
@@ -10605,7 +10702,7 @@ class SignInComponent {
10605
10702
  this.router.navigateByUrl(url);
10606
10703
  }
10607
10704
  catch (err) {
10608
- const { status, errorMessage } = err;
10705
+ const { status, errorMessage, message } = err;
10609
10706
  if (status === 401 &&
10610
10707
  errorMessage?.toLowerCase().includes("password expired")) {
10611
10708
  sessionStorage.setItem("passwordExpiredFlow", "true");
@@ -10618,7 +10715,8 @@ class SignInComponent {
10618
10715
  });
10619
10716
  return;
10620
10717
  }
10621
- notify.error("Login", { description: errorMessage });
10718
+ // For other errors, show a generic error message
10719
+ notify.error("Login", { description: message ?? errorMessage });
10622
10720
  }
10623
10721
  }
10624
10722
  handleBack() {
@@ -10634,7 +10732,7 @@ class SignInComponent {
10634
10732
  cdkTrapFocusAutoCapture="true"
10635
10733
  class="bg-card rounded-xl shadow-sm">
10636
10734
  <CardHeader class="flex flex-col items-center gap-3 text-center">
10637
- <img class="h-12 content-(--logo-large-alt)" alt="logo" />
10735
+ <img class="h-12 content-(--logo-large)" alt="logo" />
10638
10736
  </CardHeader>
10639
10737
 
10640
10738
  <CardContent class="grid gap-4">
@@ -10714,7 +10812,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
10714
10812
  cdkTrapFocusAutoCapture="true"
10715
10813
  class="bg-card rounded-xl shadow-sm">
10716
10814
  <CardHeader class="flex flex-col items-center gap-3 text-center">
10717
- <img class="h-12 content-(--logo-large-alt)" alt="logo" />
10815
+ <img class="h-12 content-(--logo-large)" alt="logo" />
10718
10816
  </CardHeader>
10719
10817
 
10720
10818
  <CardContent class="grid gap-4">
@@ -10986,7 +11084,7 @@ class BookmarksComponent {
10986
11084
  this.range.set(this.range() + (this.config.itemsPerPage ?? 10));
10987
11085
  }
10988
11086
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: BookmarksComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
10989
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: BookmarksComponent, isStandalone: true, selector: "bookmarks, Bookmarks", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null } }, providers: [provideTranslocoScope("bookmarks")], ngImport: i0, template: "@if (floating) {\r\n <div class=\"p-2\">\r\n <label class=\"text-xl font-bold\">{{ \"bookmarks.label\" | transloco }}</label>\r\n <Separator />\r\n </div>\r\n}\r\n\r\n<ul class=\"flex max-h-[460px] flex-col overflow-auto\" role=\"list\">\r\n @for (bookmark of paginatedBookmarks(); track $index) {\r\n <li\r\n role=\"listitem\"\r\n class=\"group h-10\"\r\n tabindex=\"0\"\r\n (click)=\"onClick(bookmark)\"\r\n (keydown.enter)=\"onClick(bookmark)\">\r\n <BookmarkIcon solid class=\"shrink-0\" />\r\n\r\n <p class=\"line-clamp-1\">{{ bookmark.label }}</p>\r\n\r\n @if (bookmark.author) {\r\n <Badge variant=\"ghost\" class=\"text-grey-500 text-xs\">\r\n <UserIcon class=\"size-3\" />\r\n <span class=\"line-clamp-1\">{{ bookmark.author }}</span>\r\n </Badge>\r\n }\r\n @if (bookmark.parentFolder) {\r\n <Badge variant=\"ghost\" class=\"text-grey-500 text-xs\">\r\n <FolderIcon class=\"size-3\" />\r\n <span class=\"line-clamp-1\">{{ bookmark.parentFolder }}</span>\r\n </Badge>\r\n }\r\n\r\n <button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n title=\"{{ 'bookmarks.openBookmark' | transloco }}\"\r\n class=\"text-destructive ms-auto group-hover:visible\"\r\n [attr.title]=\"'bookmarks.removeBookmark' | transloco\"\r\n [attr.aria-label]=\"'bookmarks.removeBookmark' | transloco\"\r\n (click)=\"onDelete(bookmark, $event)\">\r\n <TrashIcon />\r\n </button>\r\n </li>\r\n } @empty {\r\n <li class=\"py-4 text-center text-neutral-500\">\r\n {{ \"bookmarks.noBookmarks\" | transloco }}\r\n </li>\r\n }\r\n</ul>\r\n\r\n<div class=\"flex flex-col px-2\">\r\n @if (hasMore() && config.showLoadMore) {\r\n <button\r\n variant=\"ghost\"\r\n class=\"w-full\"\r\n tabindex=\"0\"\r\n [attr.title]=\"'loadMore' | transloco\"\r\n (click)=\"loadMore($event)\">\r\n {{ \"loadMore\" | transloco }}\r\n </button>\r\n }\r\n <button\r\n variant=\"link\"\r\n class=\"ml-auto\"\r\n [attr.title]=\"'seeMore' | transloco\"\r\n [routerLink]=\"[config.routerLink]\">\r\n {{ \"seeMore\" | transloco }}\r\n </button>\r\n</div>\r\n", styles: [":host ul{scrollbar-width:thin}\n"], dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: ListItemComponent, selector: "[role=\"listitem\"], [role=\"option\"]", inputs: ["class", "variant", "decoration"] }, { kind: "directive", type: ButtonComponent, selector: "button", inputs: ["class", "variant", "decoration", "scheme", "iconOnly", "size"] }, { kind: "directive", type: Separator, selector: "separator, Separator", inputs: ["class", "orientation"] }, { kind: "component", type: BookmarkIcon, selector: "bookmark-icon, BookmarkIcon", inputs: ["class", "solid"] }, { kind: "component", type: UserIcon, selector: "user-icon, UserIcon", inputs: ["class"] }, { kind: "component", type: TrashIcon, selector: "trash-icon, TrashIcon", inputs: ["class"] }, { kind: "component", type: FolderIcon, selector: "folder-icon, FolderIcon", inputs: ["class"] }, { kind: "directive", type: BadgeComponent, selector: "badge, Badge", inputs: ["class", "variant", "size"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
11087
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: BookmarksComponent, isStandalone: true, selector: "bookmarks, Bookmarks", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null } }, providers: [provideTranslocoScope("bookmarks")], ngImport: i0, template: "@if (floating) {\r\n <div class=\"p-2\">\r\n <label class=\"text-xl font-bold\">{{ \"bookmarks.label\" | transloco }}</label>\r\n <Separator />\r\n </div>\r\n}\r\n\r\n<ul class=\"flex max-h-[460px] flex-col overflow-auto\" role=\"list\">\r\n @for (bookmark of paginatedBookmarks(); track $index) {\r\n <li\r\n role=\"listitem\"\r\n class=\"group h-10\"\r\n tabindex=\"0\"\r\n (click)=\"onClick(bookmark)\"\r\n (keydown.enter)=\"onClick(bookmark)\">\r\n <BookmarkIcon solid class=\"shrink-0\" />\r\n\r\n <p class=\"line-clamp-1\">{{ bookmark.label }}</p>\r\n\r\n @if (bookmark.author) {\r\n <Badge variant=\"ghost\" class=\"text-grey-500 text-xs\">\r\n <UserIcon class=\"size-3\" />\r\n <span class=\"line-clamp-1\">{{ bookmark.author }}</span>\r\n </Badge>\r\n }\r\n @if (bookmark.parentFolder) {\r\n <Badge variant=\"ghost\" class=\"text-grey-500 text-xs\">\r\n <FolderIcon class=\"size-3\" />\r\n <span class=\"line-clamp-1\">{{ bookmark.parentFolder }}</span>\r\n </Badge>\r\n }\r\n\r\n <button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n title=\"{{ 'bookmarks.openBookmark' | transloco }}\"\r\n class=\"text-destructive ms-auto group-hover:visible\"\r\n [attr.title]=\"'bookmarks.removeBookmark' | transloco\"\r\n [attr.aria-label]=\"'bookmarks.removeBookmark' | transloco\"\r\n (click)=\"onDelete(bookmark, $event)\">\r\n <TrashIcon />\r\n </button>\r\n </li>\r\n } @empty {\r\n <li class=\"list-none py-4 text-center text-neutral-500\">\r\n {{ \"bookmarks.noBookmarks\" | transloco }}\r\n </li>\r\n }\r\n</ul>\r\n\r\n<div class=\"flex flex-col px-2\">\r\n @if (hasMore() && config.showLoadMore) {\r\n <button\r\n variant=\"ghost\"\r\n class=\"w-full\"\r\n tabindex=\"0\"\r\n [attr.title]=\"'loadMore' | transloco\"\r\n (click)=\"loadMore($event)\">\r\n {{ \"loadMore\" | transloco }}\r\n </button>\r\n }\r\n <button\r\n variant=\"link\"\r\n class=\"ml-auto\"\r\n [attr.title]=\"'seeMore' | transloco\"\r\n [routerLink]=\"[config.routerLink]\">\r\n {{ \"seeMore\" | transloco }}\r\n </button>\r\n</div>\r\n", styles: [":host ul{scrollbar-width:thin}\n"], dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: ListItemComponent, selector: "[role=\"listitem\"], [role=\"option\"]", inputs: ["class", "variant", "decoration"] }, { kind: "directive", type: ButtonComponent, selector: "button", inputs: ["class", "variant", "decoration", "scheme", "iconOnly", "size"] }, { kind: "directive", type: Separator, selector: "separator, Separator", inputs: ["class", "orientation"] }, { kind: "component", type: BookmarkIcon, selector: "bookmark-icon, BookmarkIcon", inputs: ["class", "solid"] }, { kind: "component", type: UserIcon, selector: "user-icon, UserIcon", inputs: ["class"] }, { kind: "component", type: TrashIcon, selector: "trash-icon, TrashIcon", inputs: ["class"] }, { kind: "component", type: FolderIcon, selector: "folder-icon, FolderIcon", inputs: ["class"] }, { kind: "directive", type: BadgeComponent, selector: "badge, Badge", inputs: ["class", "variant", "size"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
10990
11088
  }
10991
11089
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: BookmarksComponent, decorators: [{
10992
11090
  type: Component,
@@ -11001,7 +11099,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
11001
11099
  TrashIcon,
11002
11100
  FolderIcon,
11003
11101
  BadgeComponent
11004
- ], providers: [provideTranslocoScope("bookmarks")], template: "@if (floating) {\r\n <div class=\"p-2\">\r\n <label class=\"text-xl font-bold\">{{ \"bookmarks.label\" | transloco }}</label>\r\n <Separator />\r\n </div>\r\n}\r\n\r\n<ul class=\"flex max-h-[460px] flex-col overflow-auto\" role=\"list\">\r\n @for (bookmark of paginatedBookmarks(); track $index) {\r\n <li\r\n role=\"listitem\"\r\n class=\"group h-10\"\r\n tabindex=\"0\"\r\n (click)=\"onClick(bookmark)\"\r\n (keydown.enter)=\"onClick(bookmark)\">\r\n <BookmarkIcon solid class=\"shrink-0\" />\r\n\r\n <p class=\"line-clamp-1\">{{ bookmark.label }}</p>\r\n\r\n @if (bookmark.author) {\r\n <Badge variant=\"ghost\" class=\"text-grey-500 text-xs\">\r\n <UserIcon class=\"size-3\" />\r\n <span class=\"line-clamp-1\">{{ bookmark.author }}</span>\r\n </Badge>\r\n }\r\n @if (bookmark.parentFolder) {\r\n <Badge variant=\"ghost\" class=\"text-grey-500 text-xs\">\r\n <FolderIcon class=\"size-3\" />\r\n <span class=\"line-clamp-1\">{{ bookmark.parentFolder }}</span>\r\n </Badge>\r\n }\r\n\r\n <button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n title=\"{{ 'bookmarks.openBookmark' | transloco }}\"\r\n class=\"text-destructive ms-auto group-hover:visible\"\r\n [attr.title]=\"'bookmarks.removeBookmark' | transloco\"\r\n [attr.aria-label]=\"'bookmarks.removeBookmark' | transloco\"\r\n (click)=\"onDelete(bookmark, $event)\">\r\n <TrashIcon />\r\n </button>\r\n </li>\r\n } @empty {\r\n <li class=\"py-4 text-center text-neutral-500\">\r\n {{ \"bookmarks.noBookmarks\" | transloco }}\r\n </li>\r\n }\r\n</ul>\r\n\r\n<div class=\"flex flex-col px-2\">\r\n @if (hasMore() && config.showLoadMore) {\r\n <button\r\n variant=\"ghost\"\r\n class=\"w-full\"\r\n tabindex=\"0\"\r\n [attr.title]=\"'loadMore' | transloco\"\r\n (click)=\"loadMore($event)\">\r\n {{ \"loadMore\" | transloco }}\r\n </button>\r\n }\r\n <button\r\n variant=\"link\"\r\n class=\"ml-auto\"\r\n [attr.title]=\"'seeMore' | transloco\"\r\n [routerLink]=\"[config.routerLink]\">\r\n {{ \"seeMore\" | transloco }}\r\n </button>\r\n</div>\r\n", styles: [":host ul{scrollbar-width:thin}\n"] }]
11102
+ ], providers: [provideTranslocoScope("bookmarks")], template: "@if (floating) {\r\n <div class=\"p-2\">\r\n <label class=\"text-xl font-bold\">{{ \"bookmarks.label\" | transloco }}</label>\r\n <Separator />\r\n </div>\r\n}\r\n\r\n<ul class=\"flex max-h-[460px] flex-col overflow-auto\" role=\"list\">\r\n @for (bookmark of paginatedBookmarks(); track $index) {\r\n <li\r\n role=\"listitem\"\r\n class=\"group h-10\"\r\n tabindex=\"0\"\r\n (click)=\"onClick(bookmark)\"\r\n (keydown.enter)=\"onClick(bookmark)\">\r\n <BookmarkIcon solid class=\"shrink-0\" />\r\n\r\n <p class=\"line-clamp-1\">{{ bookmark.label }}</p>\r\n\r\n @if (bookmark.author) {\r\n <Badge variant=\"ghost\" class=\"text-grey-500 text-xs\">\r\n <UserIcon class=\"size-3\" />\r\n <span class=\"line-clamp-1\">{{ bookmark.author }}</span>\r\n </Badge>\r\n }\r\n @if (bookmark.parentFolder) {\r\n <Badge variant=\"ghost\" class=\"text-grey-500 text-xs\">\r\n <FolderIcon class=\"size-3\" />\r\n <span class=\"line-clamp-1\">{{ bookmark.parentFolder }}</span>\r\n </Badge>\r\n }\r\n\r\n <button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n title=\"{{ 'bookmarks.openBookmark' | transloco }}\"\r\n class=\"text-destructive ms-auto group-hover:visible\"\r\n [attr.title]=\"'bookmarks.removeBookmark' | transloco\"\r\n [attr.aria-label]=\"'bookmarks.removeBookmark' | transloco\"\r\n (click)=\"onDelete(bookmark, $event)\">\r\n <TrashIcon />\r\n </button>\r\n </li>\r\n } @empty {\r\n <li class=\"list-none py-4 text-center text-neutral-500\">\r\n {{ \"bookmarks.noBookmarks\" | transloco }}\r\n </li>\r\n }\r\n</ul>\r\n\r\n<div class=\"flex flex-col px-2\">\r\n @if (hasMore() && config.showLoadMore) {\r\n <button\r\n variant=\"ghost\"\r\n class=\"w-full\"\r\n tabindex=\"0\"\r\n [attr.title]=\"'loadMore' | transloco\"\r\n (click)=\"loadMore($event)\">\r\n {{ \"loadMore\" | transloco }}\r\n </button>\r\n }\r\n <button\r\n variant=\"link\"\r\n class=\"ml-auto\"\r\n [attr.title]=\"'seeMore' | transloco\"\r\n [routerLink]=\"[config.routerLink]\">\r\n {{ \"seeMore\" | transloco }}\r\n </button>\r\n</div>\r\n", styles: [":host ul{scrollbar-width:thin}\n"] }]
11005
11103
  }], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }] } });
11006
11104
 
11007
11105
  class DeleteCollectionDialog {
@@ -11126,11 +11224,11 @@ class CollectionsComponent {
11126
11224
  this.range.set(this.range() + (this.config.itemsPerPage ?? 10));
11127
11225
  }
11128
11226
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: CollectionsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
11129
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: CollectionsComponent, isStandalone: true, selector: "app-collections", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null } }, providers: [provideTranslocoScope("collections")], viewQueries: [{ propertyName: "deleteCollectionDialog", first: true, predicate: DeleteCollectionDialog, descendants: true, isSignal: true }], ngImport: i0, template: "@if (floating) {\r\n <div class=\"p-2\">\r\n <label class=\"text-xl font-bold\">{{\r\n \"collections.label\" | transloco\r\n }}</label>\r\n <Separator />\r\n </div>\r\n}\r\n\r\n<ul class=\"flex max-h-[460px] flex-col overflow-auto\">\r\n @for (collection of paginatedCollections(); track $index) {\r\n <li\r\n role=\"listitem\"\r\n class=\"group grid grid-cols-[min-content_auto_min-content] items-center\"\r\n tabindex=\"0\"\r\n (click)=\"onClick(collection)\"\r\n (keydown.enter)=\"onClick(collection)\">\r\n <i class=\"fas fa-inbox ps-2\"></i>\r\n\r\n <p class=\"mx-2 line-clamp-1\">{{ collection.name }}</p>\r\n\r\n <button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n class=\"text-destructive invisible group-hover:visible\"\r\n title=\"{{ 'collections.deleteCollection' | transloco }}\"\r\n [attr.aria-label]=\"'collections.deleteCollection' | transloco\"\r\n (click)=\"onDelete(collection, $index, $event)\">\r\n <TrashIcon />\r\n </button>\r\n </li>\r\n } @empty {\r\n <li class=\"list-none py-4 text-center text-neutral-500\">\r\n {{ \"collections.noCollections\" | transloco }}\r\n </li>\r\n }\r\n</ul>\r\n\r\n<div class=\"flex flex-col px-2\">\r\n @if (hasMore() && config.showLoadMore) {\r\n <button\r\n variant=\"outline\"\r\n class=\"w-full\"\r\n tabindex=\"0\"\r\n [attr.title]=\"'loadMore' | transloco\"\r\n (click)=\"loadMore($event)\">\r\n {{ \"loadMore\" | transloco }}\r\n </button>\r\n }\r\n <button\r\n variant=\"link\"\r\n class=\"ml-auto\"\r\n [attr.title]=\"'seeMore' | transloco\"\r\n [routerLink]=\"[config.routerLink]\">\r\n {{ \"seeMore\" | transloco }}\r\n </button>\r\n</div>\r\n\r\n<delete-collection-dialog />\r\n", dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: DeleteCollectionDialog, selector: "delete-collection-dialog" }, { kind: "directive", type: ButtonComponent, selector: "button", inputs: ["class", "variant", "decoration", "scheme", "iconOnly", "size"] }, { kind: "directive", type: ListItemComponent, selector: "[role=\"listitem\"], [role=\"option\"]", inputs: ["class", "variant", "decoration"] }, { kind: "directive", type: Separator, selector: "separator, Separator", inputs: ["class", "orientation"] }, { kind: "component", type: TrashIcon, selector: "trash-icon, TrashIcon", inputs: ["class"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
11227
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: CollectionsComponent, isStandalone: true, selector: "app-collections, collections", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null } }, providers: [provideTranslocoScope("collections")], viewQueries: [{ propertyName: "deleteCollectionDialog", first: true, predicate: DeleteCollectionDialog, descendants: true, isSignal: true }], ngImport: i0, template: "@if (floating) {\r\n <div class=\"p-2\">\r\n <label class=\"text-xl font-bold\">{{\r\n \"collections.label\" | transloco\r\n }}</label>\r\n <Separator />\r\n </div>\r\n}\r\n\r\n<ul class=\"flex max-h-[460px] flex-col overflow-auto\">\r\n @for (collection of paginatedCollections(); track $index) {\r\n <li\r\n role=\"listitem\"\r\n class=\"group grid grid-cols-[min-content_auto_min-content] items-center\"\r\n tabindex=\"0\"\r\n (click)=\"onClick(collection)\"\r\n (keydown.enter)=\"onClick(collection)\">\r\n <i class=\"fas fa-inbox ps-2\"></i>\r\n\r\n <p class=\"mx-2 line-clamp-1\">{{ collection.name }}</p>\r\n\r\n <button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n class=\"text-destructive invisible group-hover:visible\"\r\n title=\"{{ 'collections.deleteCollection' | transloco }}\"\r\n [attr.aria-label]=\"'collections.deleteCollection' | transloco\"\r\n (click)=\"onDelete(collection, $index, $event)\">\r\n <TrashIcon />\r\n </button>\r\n </li>\r\n } @empty {\r\n <li class=\"list-none py-4 text-center text-neutral-500\">\r\n {{ \"collections.noCollections\" | transloco }}\r\n </li>\r\n }\r\n</ul>\r\n\r\n<div class=\"flex flex-col px-2\">\r\n @if (hasMore() && config.showLoadMore) {\r\n <button\r\n variant=\"outline\"\r\n class=\"w-full\"\r\n tabindex=\"0\"\r\n [attr.title]=\"'loadMore' | transloco\"\r\n (click)=\"loadMore($event)\">\r\n {{ \"loadMore\" | transloco }}\r\n </button>\r\n }\r\n <button\r\n variant=\"link\"\r\n class=\"ml-auto\"\r\n [attr.title]=\"'seeMore' | transloco\"\r\n [routerLink]=\"[config.routerLink]\">\r\n {{ \"seeMore\" | transloco }}\r\n </button>\r\n</div>\r\n\r\n<delete-collection-dialog />\r\n", dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: DeleteCollectionDialog, selector: "delete-collection-dialog" }, { kind: "directive", type: ButtonComponent, selector: "button", inputs: ["class", "variant", "decoration", "scheme", "iconOnly", "size"] }, { kind: "directive", type: ListItemComponent, selector: "[role=\"listitem\"], [role=\"option\"]", inputs: ["class", "variant", "decoration"] }, { kind: "directive", type: Separator, selector: "separator, Separator", inputs: ["class", "orientation"] }, { kind: "component", type: TrashIcon, selector: "trash-icon, TrashIcon", inputs: ["class"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
11130
11228
  }
11131
11229
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: CollectionsComponent, decorators: [{
11132
11230
  type: Component,
11133
- args: [{ selector: "app-collections", standalone: true, imports: [
11231
+ args: [{ selector: "app-collections, collections", standalone: true, imports: [
11134
11232
  TranslocoPipe,
11135
11233
  RouterLink,
11136
11234
  DeleteCollectionDialog,
@@ -13012,6 +13110,7 @@ class AggregationTreeComponent {
13012
13110
  aggregationsService = inject(AggregationsService);
13013
13111
  el = inject(ElementRef);
13014
13112
  injector = inject(Injector);
13113
+ destroyRef = inject(DestroyRef);
13015
13114
  class = input("", ...(ngDevMode ? [{ debugName: "class" }] : []));
13016
13115
  /**
13017
13116
  * The name of the <details> element. When you provide the same id, the component work as an accordion
@@ -13200,6 +13299,21 @@ class AggregationTreeComponent {
13200
13299
  effect(() => {
13201
13300
  this.filters.set(this.getFilters());
13202
13301
  });
13302
+ this.destroyRef.onDestroy(() => {
13303
+ // If the popover is closed with unapplied selections, reset state so it doesn't persist when reopening
13304
+ if (this.selection()) {
13305
+ sessionStorage.removeItem(`agg-${this.aggregation()?.column}`);
13306
+ const unselect = (items) => {
13307
+ items.forEach((item) => {
13308
+ item.$selected = false;
13309
+ item.$selectedVisually = false;
13310
+ if (item.items)
13311
+ unselect(item.items);
13312
+ });
13313
+ };
13314
+ unselect(this.items());
13315
+ }
13316
+ });
13203
13317
  }
13204
13318
  // compare currentItems and updatedItems to add the new sub items
13205
13319
  // from updatedItems into currentItems to not alter the already loaded items
@@ -13349,8 +13463,10 @@ class AggregationTreeComponent {
13349
13463
  * If the item is deselected, the selection count is decremented by 1.
13350
13464
  */
13351
13465
  select() {
13352
- this.onSelect.emit(this.getFlattenTreeItems().filter(item => item.$selected));
13353
- this.selection.set(true);
13466
+ const selectedItems = this.getFlattenTreeItems().filter(item => item.$selected);
13467
+ this.onSelect.emit(selectedItems);
13468
+ // Keep apply visible if items are selected, or if active filters exist (user may be deselecting to clear)
13469
+ this.selection.set(selectedItems.length > 0 || this.hasFilters());
13354
13470
  this.verifySelected();
13355
13471
  sessionStorage.setItem(`agg-${this.aggregation()?.column}`, JSON.stringify([...this.items()]));
13356
13472
  }
@@ -13805,7 +13921,7 @@ class AggregationComponent {
13805
13921
  (onClear)="onClear.emit($event)"
13806
13922
  />
13807
13923
  }
13808
- `, isInline: true, dependencies: [{ kind: "component", type: AggregationListComponent, selector: "AggregationList, aggregation-list, aggregationlist", inputs: ["class", "id", "name", "column", "collapsible", "collapsed", "searchable", "showFiltersCount", "searchText"], outputs: ["onSelect", "onApply", "onClear", "searchTextChange"] }, { kind: "component", type: AggregationTreeComponent, selector: "AggregationTree, aggregation-tree, aggregationtree", inputs: ["class", "id", "name", "column", "expandedLevel", "collapsible", "collapsed", "searchable", "showFiltersCount", "searchText"], outputs: ["onSelect", "onApply", "onClear", "searchTextChange"] }, { kind: "component", type: AggregationDateComponent, selector: "aggregation-date, AggregationDate, aggregationdate", inputs: ["title", "displayEmptyDistributionIntervals"] }] });
13924
+ `, isInline: true, dependencies: [{ kind: "component", type: AggregationListComponent, selector: "AggregationList, aggregation-list, aggregationlist", inputs: ["class", "id", "name", "column", "collapsible", "collapsed", "searchable", "showFiltersCount", "searchText"], outputs: ["onSelect", "onApply", "onClear", "searchTextChange"] }, { kind: "component", type: AggregationTreeComponent, selector: "AggregationTree, aggregation-tree, aggregationtree", inputs: ["class", "id", "name", "column", "expandedLevel", "collapsible", "collapsed", "searchable", "showFiltersCount", "searchText"], outputs: ["onSelect", "onApply", "onClear", "searchTextChange"] }, { kind: "component", type: AggregationDateComponent, selector: "aggregation-date, AggregationDate, aggregationdate", inputs: ["title", "displayEmptyDistributionIntervals", "name"] }] });
13809
13925
  }
13810
13926
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AggregationComponent, decorators: [{
13811
13927
  type: Component,
@@ -13911,7 +14027,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
13911
14027
  }], propDecorators: { class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }] } });
13912
14028
 
13913
14029
  class FilterButtonComponent {
13914
- name = input.required(...(ngDevMode ? [{ debugName: "name" }] : []));
14030
+ name = input(...(ngDevMode ? [undefined, { debugName: "name" }] : []));
13915
14031
  column = input.required(...(ngDevMode ? [{ debugName: "column" }] : []));
13916
14032
  position = input("bottom-start", ...(ngDevMode ? [{ debugName: "position" }] : []));
13917
14033
  offset = input(8, ...(ngDevMode ? [{ debugName: "offset" }] : []));
@@ -13934,15 +14050,20 @@ class FilterButtonComponent {
13934
14050
  appStore = inject(AppStore);
13935
14051
  constructor() {
13936
14052
  effect(() => {
13937
- const agg = this.aggregationsStore.getAggregation(this.name());
13938
- const f = this.queryParamsStore.getFilter({ name: agg?.name, field: agg?.column });
14053
+ const name = this.name();
14054
+ const column = this.column();
14055
+ let agg = undefined;
14056
+ if (name) {
14057
+ agg = this.aggregationsStore.getAggregation(name);
14058
+ if (!agg)
14059
+ return;
14060
+ }
14061
+ const f = this.queryParamsStore.getFilter({ name: name, field: column });
13939
14062
  const count = f?.count || 0;
13940
- if (!agg)
13941
- return;
13942
14063
  // retrieve customization fron the custom JSON provided by the server
13943
- const { icon, hidden = false, display } = this.appStore.getAggregationCustomization(this.column(), this.name()) || {};
14064
+ const { icon, hidden = false, display } = this.appStore.getAggregationCustomization(column, name) || {};
13944
14065
  const r = {
13945
- name: agg?.name,
14066
+ name,
13946
14067
  column: this.column(),
13947
14068
  icon,
13948
14069
  hidden,
@@ -13954,6 +14075,10 @@ class FilterButtonComponent {
13954
14075
  // to be able to display the date range
13955
14076
  legacyFilter: this.isDate(this.column()) ? f : undefined
13956
14077
  };
14078
+ if (this.isDate(this.column())) {
14079
+ // for date filters, we want to display the date range in the button
14080
+ r.disabled = false; // date filters should never be disabled, even if they have no items, because they can still be applied with a custom range
14081
+ }
13957
14082
  this.filter.set(r);
13958
14083
  });
13959
14084
  effect(() => {
@@ -13971,7 +14096,7 @@ class FilterButtonComponent {
13971
14096
  return this.appStore.isDateColumn(column);
13972
14097
  }
13973
14098
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: FilterButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
13974
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: FilterButtonComponent, isStandalone: true, selector: "filter-button, FilterButton", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null }, column: { classPropertyName: "column", publicName: "column", isSignal: true, isRequired: true, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, offset: { classPropertyName: "offset", publicName: "offset", isSignal: true, isRequired: false, transformFunction: null }, expandedLevel: { classPropertyName: "expandedLevel", publicName: "expandedLevel", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "listitem" }, properties: { "class.hidden": "filter().hidden" } }, viewQueries: [{ propertyName: "popoverRef", first: true, predicate: PopoverComponent, descendants: true, isSignal: true }], ngImport: i0, template: `
14099
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: FilterButtonComponent, isStandalone: true, selector: "filter-button, FilterButton", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, column: { classPropertyName: "column", publicName: "column", isSignal: true, isRequired: true, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, offset: { classPropertyName: "offset", publicName: "offset", isSignal: true, isRequired: false, transformFunction: null }, expandedLevel: { classPropertyName: "expandedLevel", publicName: "expandedLevel", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "listitem" }, properties: { "class.hidden": "filter().hidden" } }, viewQueries: [{ propertyName: "popoverRef", first: true, predicate: PopoverComponent, descendants: true, isSignal: true }], ngImport: i0, template: `
13975
14100
  <Popover [disabled]="filter().disabled" class="group">
13976
14101
  <button
13977
14102
  [variant]="variant()"
@@ -13994,11 +14119,7 @@ class FilterButtonComponent {
13994
14119
  }
13995
14120
 
13996
14121
  <!-- show the count of selected items -->
13997
- @if (filter().isTree && filter().count > 0) {
13998
- <Badge size="xs" >
13999
- {{ filter().count }}
14000
- </Badge>
14001
- } @else if (filter().count > 1) {
14122
+ @if (filter().count > 1) {
14002
14123
  <Badge size="xs" class="pb-0.5">+{{ filter().count - 1 }}</Badge>
14003
14124
  }
14004
14125
  </button>
@@ -14009,15 +14130,23 @@ class FilterButtonComponent {
14009
14130
  [offset]="offset()"
14010
14131
  >
14011
14132
  @if(contentRef.isVisible) {
14133
+ @let name = filter().name;
14134
+ @let column = filter().column;
14135
+ @if(name) {
14012
14136
  <Aggregation
14013
14137
  class="w-60 [--height:19lh]"
14014
14138
  id="filter-{{ filter().column }}"
14015
- [name]="filter().name"
14016
- [column]="filter().column"
14139
+ [name]="name"
14140
+ [column]="column"
14017
14141
  [expandedLevel]="expandedLevel()"
14018
14142
  (onApply)="popoverRef()?.close()"
14019
14143
  (onClear)="popoverRef()?.close()"
14020
14144
  />
14145
+ } @else {
14146
+ <div class="p-4 text-sm text-foreground/60">
14147
+ {{ 'INVALID_FILTER_NAME' | transloco }}
14148
+ </div>
14149
+ }
14021
14150
  }
14022
14151
  </PopoverContent>
14023
14152
  </Popover>
@@ -14061,11 +14190,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
14061
14190
  }
14062
14191
 
14063
14192
  <!-- show the count of selected items -->
14064
- @if (filter().isTree && filter().count > 0) {
14065
- <Badge size="xs" >
14066
- {{ filter().count }}
14067
- </Badge>
14068
- } @else if (filter().count > 1) {
14193
+ @if (filter().count > 1) {
14069
14194
  <Badge size="xs" class="pb-0.5">+{{ filter().count - 1 }}</Badge>
14070
14195
  }
14071
14196
  </button>
@@ -14076,15 +14201,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
14076
14201
  [offset]="offset()"
14077
14202
  >
14078
14203
  @if(contentRef.isVisible) {
14204
+ @let name = filter().name;
14205
+ @let column = filter().column;
14206
+ @if(name) {
14079
14207
  <Aggregation
14080
14208
  class="w-60 [--height:19lh]"
14081
14209
  id="filter-{{ filter().column }}"
14082
- [name]="filter().name"
14083
- [column]="filter().column"
14210
+ [name]="name"
14211
+ [column]="column"
14084
14212
  [expandedLevel]="expandedLevel()"
14085
14213
  (onApply)="popoverRef()?.close()"
14086
14214
  (onClear)="popoverRef()?.close()"
14087
14215
  />
14216
+ } @else {
14217
+ <div class="p-4 text-sm text-foreground/60">
14218
+ {{ 'INVALID_FILTER_NAME' | transloco }}
14219
+ </div>
14220
+ }
14088
14221
  }
14089
14222
  </PopoverContent>
14090
14223
  </Popover>
@@ -14094,7 +14227,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
14094
14227
  role: "listitem"
14095
14228
  }
14096
14229
  }]
14097
- }], ctorParameters: () => [], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }], column: [{ type: i0.Input, args: [{ isSignal: true, alias: "column", required: true }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], offset: [{ type: i0.Input, args: [{ isSignal: true, alias: "offset", required: false }] }], expandedLevel: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandedLevel", required: false }] }], popoverRef: [{ type: i0.ViewChild, args: [i0.forwardRef(() => PopoverComponent), { isSignal: true }] }] } });
14230
+ }], ctorParameters: () => [], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], column: [{ type: i0.Input, args: [{ isSignal: true, alias: "column", required: true }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], offset: [{ type: i0.Input, args: [{ isSignal: true, alias: "offset", required: false }] }], expandedLevel: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandedLevel", required: false }] }], popoverRef: [{ type: i0.ViewChild, args: [i0.forwardRef(() => PopoverComponent), { isSignal: true }] }] } });
14098
14231
 
14099
14232
  const FILTERS_BREAKPOINT = new InjectionToken('FILTERS_BREAKPOINT', { factory: () => 5 });
14100
14233