Merge remote-tracking branch 'origin/master' into loading

This commit is contained in:
Adina Țeudan 2021-08-25 12:17:40 +03:00
commit fcdf16c77a
48 changed files with 1204 additions and 64 deletions

View File

@ -44,6 +44,7 @@
"@angular-eslint/no-output-rename": "error",
"@angular-eslint/prefer-output-readonly": "error",
"@typescript-eslint/unbound-method": "error",
"@typescript-eslint/no-floating-promises": "off",
"@typescript-eslint/lines-between-class-members": "off",
"@typescript-eslint/naming-convention": [
"error",

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Help-Mode" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="01.-Help-button" transform="translate(-1408.000000, -645.000000)" fill="#283241" fill-rule="nonzero">
<g id="help-button" transform="translate(1294.000000, 635.000000)">
<g id="help" transform="translate(114.000000, 10.000000)">
<path d="M10,0 C15.5,1.01033361e-15 20,4.5 20,10 C20,15.5 15.5,20 10,20 C4.5,20 3.55271368e-15,15.5 3.55271368e-15,10 C7.10542736e-15,4.5 4.5,-1.01033361e-15 10,0 Z M10,2 C5.6,2 2,5.6 2,10 C2,14.4 5.6,18 10,18 C14.4,18 18,14.4 18,10 C18,5.6 14.4,2 10,2 Z M10.86,12.9 L10.86,14.9 L8.86,14.9 L8.86,12.9 L10.86,12.9 Z M9.86,4.9 C11.56,4.9 12.86,6.2 12.86,7.9 C12.86,8.8 12.36,9.7 11.66,10.3 C11.3830769,10.4846154 10.9357396,10.839645 10.8685571,11.4437415 L10.86,11.6 L10.86,11.9 L8.86,11.9 L8.86,11.6 C8.86,10.5 9.46,9.4 10.46,8.7 C10.76,8.5 10.86,8.2 10.86,7.9 C10.86,7.3 10.46,6.9 9.86,6.9 C9.30285714,6.9 8.91816327,7.24489796 8.86604956,7.77456268 L8.86,7.9 L6.86,7.9 C6.86,6.2 8.16,4.9 9.86,4.9 Z"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="100px" version="1.1" viewBox="0 0 100 100" width="100px"
xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd" id="search" stroke="none" stroke-width="1">
<path
d="M95.5,74.5 L76.5,55.5 C78.5,50.5 79.5,45.5 79.5,40 C79.5,18 61.5,0 39.5,0 C18,0 0,18 0,40 C0,62 18,80 40,80 C45.5,80 51,79 55.5,77 L74.5,96 C77.5,99 81.5,100.5 85,100.5 C89,100.5 92.5,99 95.5,96 C101.5,90 101.5,80 95.5,74.5 Z M10,40 C10,23.5 23.5,10 40,10 C56.5,10 70,23.5 70,40 C70,56.5 56.5,70 40,70 C23.5,70 10,56.5 10,40 Z M88.5,88.5 C86.5,90.5 83.5,90.5 81.5,88.5 L64.5,71.5 C67,69.5 69.5,67 71.5,64.5 L88.5,81.5 C90.5,83.5 90.5,86.5 88.5,88.5 Z"
fill="currentColor" fill-rule="nonzero" id="Shape"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 801 B

View File

@ -0,0 +1,40 @@
@import 'apps/red-ui/src/assets/styles/variables';
.mat-dialog-container {
color: $accent;
padding: 0 !important;
border-radius: 8px !important;
}
.dialog {
position: relative;
min-height: 80px;
.dialog-close {
position: absolute;
top: 16px;
right: 16px;
}
.dialog-header {
padding: 32px 60px 0 32px;
}
.dialog-content {
padding: 24px 32px 40px;
}
.dialog-actions {
height: 81px;
box-sizing: border-box;
border-top: 1px solid $separator;
padding: 0 32px;
align-items: center;
display: flex;
> * {
margin-right: 16px;
}
}
}

View File

@ -0,0 +1,267 @@
@import 'variables';
@import 'mixins';
form .iqser-input-group:not(first-of-type) {
margin-top: 14px;
}
.iqser-input-group {
display: flex;
flex-direction: column;
position: relative;
height: fit-content;
.hint {
margin-top: 5px;
font-size: 11px;
line-height: 14px;
opacity: 0.7;
}
.input-icon {
position: absolute;
right: 1px;
bottom: 1px;
background: $quick-filter-border;
height: 34px;
width: 34px;
border-left: 1px solid $quick-filter-border;
border-top-right-radius: 7px;
border-bottom-right-radius: 7px;
cursor: pointer;
transition: background-color 0.25s ease;
display: flex;
align-items: center;
justify-content: center;
&:hover {
background: $btn-bg;
}
mat-icon {
width: 14px;
height: 14px;
color: $accent;
}
&.disabled {
cursor: default;
}
}
.mat-form-field-underline {
display: none;
}
.mat-form-field-wrapper,
.mat-form-field-infix {
padding-bottom: 0;
}
.mat-form-field-label {
opacity: 0.7 !important;
color: $accent !important;
transform: translateY(-1.34em) !important;
}
&:first-child {
margin-top: 0;
}
.icon-right {
width: 14px;
height: 14px;
position: absolute;
top: 10px;
right: 10px;
}
.slider-row {
display: flex;
flex-direction: row;
align-items: center;
}
.mat-button-toggle-checked {
background: $primary;
transition: background-color 0.25s ease;
color: $white;
}
input,
textarea,
mat-select {
box-sizing: border-box;
padding-left: 11px;
padding-right: 11px;
border: 1px solid $quick-filter-border;
font-family: Inter, sans-serif;
font-size: 13px;
background-color: #ffffff;
border-radius: 8px;
outline: none;
margin-top: 3px;
min-height: 36px;
&.with-icon {
padding-right: 34px;
}
&:focus:not(:disabled):not(.mat-select-disabled) {
border-color: $accent;
}
&::placeholder {
color: $accent;
opacity: 0.7;
}
&.ng-invalid.ng-touched {
border-color: rgba($primary, 0.3);
&:focus {
border-color: $primary;
}
}
&:disabled,
&.mat-select-disabled {
background-color: $filter-bg;
color: rgba($accent, 0.3);
}
}
textarea {
line-height: 18px;
}
.hex-color-input {
width: 150px;
max-width: 150px;
}
mat-select {
.mat-select-trigger {
height: 32px;
}
.mat-select-value {
vertical-align: middle;
}
}
textarea {
resize: vertical;
padding-top: 7px;
padding-bottom: 7px;
@include scroll-bar;
&.has-scrollbar {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
label:not(.mat-slide-toggle-label) {
opacity: 0.7;
font-size: 11px;
letter-spacing: 0;
line-height: 14px;
margin-bottom: 2px;
color: $accent;
&.mat-checkbox-layout {
opacity: 1;
margin-bottom: 0;
}
}
&.required label:after {
content: ' *';
color: $primary;
}
&.datepicker-wrapper {
position: relative;
display: flex;
margin-top: 0;
width: 120px;
.mat-datepicker-input {
margin-top: 0;
width: 120px;
}
.mat-datepicker-toggle {
position: absolute;
right: 0;
bottom: 0;
color: $accent;
&.mat-datepicker-toggle-active {
color: $primary;
}
.mat-icon-button {
width: 34px;
height: 34px;
line-height: 34px;
}
mat-icon {
width: 14px;
height: 17px;
}
}
}
&.w-75 {
width: 75px;
max-width: 75px;
}
&.w-110 {
width: 110px;
max-width: 110px;
}
&.w-150 {
max-width: 150px;
width: 150px;
}
&.w-160 {
width: 160px;
max-width: 160px;
}
&.w-200 {
width: 200px;
max-width: 200px;
}
&.w-250 {
width: 250px;
max-width: 250px;
}
&.w-300 {
width: 300px;
max-width: 300px;
}
&.w-400 {
width: 400px;
max-width: 400px;
}
&.w-450 {
width: 450px;
max-width: 450px;
}
&.w-full {
width: 100%;
max-width: 100%;
}
}

View File

@ -0,0 +1,3 @@
.mt-0 {
margin-top: 0 !important;
}

View File

@ -1,5 +1,5 @@
// This rebel line is crying (in WebStorm) but it actually works
@import '~/src/assets/styles/variables';
@import '../../../../../apps/red-ui/src/assets/styles/variables';
$btn-bg-hover: #e2e4e9 !default;
$btn-bg: #f0f1f4 !default;
@ -7,3 +7,4 @@ $warn: #fdbd00 !default;
$white: white !default;
$separator: rgba(226, 228, 233, 0.9) !default;
$quick-filter-border: #d3d5da !default;
$filter-bg: #f4f5f7 !default;

View File

@ -1,4 +1,7 @@
@import 'inputs';
@import 'buttons';
@import 'texts';
@import 'tables';
@import 'full-pages';
@import 'layout';
@import 'dialogs';

View File

@ -1,26 +1,27 @@
export * from './lib/common-ui.module';
export * from './lib/buttons/icon-button/icon-button.type';
export * from './lib/buttons/icon-button/icon-button.component';
export * from './lib/utils/functions';
export * from './lib/utils/operators';
export * from './lib/utils/auto-unsubscribe.directive';
export * from './lib/utils/pipes/humanize.pipe';
export * from './lib/utils/types/events.type';
export * from './lib/utils/types/utility-types';
export * from './lib/utils/types/tooltip-positions.type';
export * from './lib/utils/decorators/bind.decorator';
export * from './lib/utils/decorators/required.decorator';
export * from './lib/utils/decorators/debounce.decorator';
export * from './lib/buttons/circle-button/circle-button.type';
export * from './lib/buttons/circle-button/circle-button.component';
export * from './lib/filtering/filter-utils';
export * from './lib/filtering/filter.service';
export * from './lib/filtering/models/filter.model';
export * from './lib/filtering/models/filter-group.model';
export * from './lib/filtering/models/nested-filter.model';
export * from './lib/filtering/popup-filter/popup-filter.component';
export * from './lib/filtering/quick-filters/quick-filters.component';
export * from './lib/sorting/sort-by.pipe';
export * from './lib/sorting/sorting.service';
export * from './lib/sorting/models/sorting-option.model';
export * from './lib/sorting/models/sorting-order.type';
export * from './lib/services/toaster.service';
export * from './lib/services/error-message.service';
export * from './lib/search/search.service';
export * from './lib/tables/entities.service';
export * from './lib/tables/listing-component.directive';
@ -29,7 +30,12 @@ export * from './lib/tables/table-column-name/table-column-name.component';
export * from './lib/tables/table-header/table-header.component';
export * from './lib/misc/status-bar/status-bar.component';
export * from './lib/misc/status-bar/status-bar-config.model';
export * from './lib/inputs/round-checkbox/round-checkbox.component';
export * from './lib/inputs/editable-input/editable-input.component';
export * from './lib/inputs/input-with-action/input-with-action.component';
export * from './lib/buttons';
export * from './lib/help-mode';
export * from './lib/tables/models/listable';
export * from './lib/loading/loading.service';
export * from './lib/loading/full-page-loading-indicator/full-page-loading-indicator.component';
export * from './lib/error/error.service';

View File

@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatTooltipModule } from '@angular/material/tooltip';
import { ChevronButtonComponent } from './chevron-button/chevron-button.component';
import { CircleButtonComponent } from './circle-button/circle-button.component';
import { IconButtonComponent } from './icon-button/icon-button.component';
const components = [ChevronButtonComponent, CircleButtonComponent, IconButtonComponent];
@NgModule({
declarations: [...components],
imports: [CommonModule, MatIconModule, MatButtonModule, MatTooltipModule],
exports: [...components]
})
export class IqserButtonsModule {}

View File

@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { MatTooltip } from '@angular/material/tooltip';
import { CircleButtonType, CircleButtonTypes } from './circle-button.type';
import { CircleButtonType, CircleButtonTypes } from '../types/circle-button.type';
import { Required } from '../../utils/decorators/required.decorator';
import { IqserTooltipPosition, IqserTooltipPositions } from '../../utils/types/tooltip-positions.type';

View File

@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { IconButtonType, IconButtonTypes } from './icon-button.type';
import { IconButtonType, IconButtonTypes } from '../types/icon-button.type';
import { Required } from '../../utils/decorators/required.decorator';
@Component({

5
src/lib/buttons/index.ts Normal file
View File

@ -0,0 +1,5 @@
export * from './buttons.module';
export * from './types/icon-button.type';
export * from './icon-button/icon-button.component';
export * from './types/circle-button.type';
export * from './circle-button/circle-button.component';

View File

@ -6,10 +6,10 @@ import { DomSanitizer } from '@angular/platform-browser';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { FormsModule } from '@angular/forms';
import { MatMenuModule } from '@angular/material/menu';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDialogModule } from '@angular/material/dialog';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { IconButtonComponent } from './buttons/icon-button/icon-button.component';
import { ChevronButtonComponent } from './buttons/chevron-button/chevron-button.component';
import { CircleButtonComponent } from './buttons/circle-button/circle-button.component';
import { RoundCheckboxComponent } from './inputs/round-checkbox/round-checkbox.component';
import { SortByPipe } from './sorting/sort-by.pipe';
import { HumanizePipe } from './utils/pipes/humanize.pipe';
@ -19,19 +19,19 @@ import { TableHeaderComponent } from './tables/table-header/table-header.compone
import { SyncWidthDirective } from './tables/sync-width.directive';
import { StatusBarComponent } from './misc/status-bar/status-bar.component';
import { EditableInputComponent } from './inputs/editable-input/editable-input.component';
import { PopupFilterComponent } from './filtering/popup-filter/popup-filter.component';
import { InputWithActionComponent } from './inputs/input-with-action/input-with-action.component';
import { IqserButtonsModule } from './buttons';
import { FullPageLoadingIndicatorComponent } from './loading/full-page-loading-indicator/full-page-loading-indicator.component';
import { FullPageErrorComponent } from './error/full-page-error/full-page-error.component';
const buttons = [IconButtonComponent, ChevronButtonComponent, CircleButtonComponent];
const inputs = [RoundCheckboxComponent, EditableInputComponent, InputWithActionComponent];
const inputs = [RoundCheckboxComponent, EditableInputComponent];
const matModules = [MatIconModule, MatButtonModule, MatTooltipModule, MatMenuModule, MatCheckboxModule, MatDialogModule, MatProgressSpinnerModule];
const matModules = [MatIconModule, MatButtonModule, MatTooltipModule, MatProgressSpinnerModule];
const modules = [...matModules, FormsModule, TranslateModule];
const modules = [...matModules, FormsModule, TranslateModule, IqserButtonsModule];
const components = [
...buttons,
...inputs,
TableColumnNameComponent,
QuickFiltersComponent,
@ -50,7 +50,7 @@ const utils = [SortByPipe, HumanizePipe, SyncWidthDirective];
})
export class CommonUiModule {
constructor(private readonly _iconRegistry: MatIconRegistry, private readonly _sanitizer: DomSanitizer) {
const icons = ['arrow-down', 'check', 'close', 'edit', 'error', 'refresh', 'sort-asc', 'sort-desc'];
const icons = ['arrow-down', 'check', 'close', 'edit', 'error', 'refresh', 'sort-asc', 'sort-desc', 'search', 'help-outline'];
icons.forEach(icon => {
_iconRegistry.addSvgIconInNamespace('iqser', icon, _sanitizer.bypassSecurityTrustResourceUrl(`/assets/icons/${icon}.svg`));

View File

@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { IconButtonTypes } from '../../buttons';
import { ErrorService } from '../error.service';
import { IconButtonTypes } from '../../buttons/icon-button/icon-button.type';
@Component({
selector: 'iqser-full-page-error',

View File

@ -4,6 +4,7 @@ import { distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators'
import { processFilters, toFlatFilters } from './filter-utils';
import { FilterGroup } from './models/filter-group.model';
import { NestedFilter } from './models/nested-filter.model';
import { get } from '../utils/operators';
@Injectable()
export class FilterService {
@ -61,7 +62,7 @@ export class FilterService {
}
getGroup$(slug: string): Observable<FilterGroup | undefined> {
return this.filterGroups$.pipe(map(all => all.find(f => f.slug === slug)));
return this.filterGroups$.pipe(get(group => group.slug === slug));
}
reset(): void {

View File

@ -0,0 +1,109 @@
<ng-container *ngIf="primaryFilterGroup$ | async as primaryGroup">
<iqser-icon-button
*ngIf="primaryGroup.icon"
[icon]="primaryGroup.icon"
[label]="primaryGroup.label || ('filter-menu.label' | translate)"
[matMenuTriggerFor]="filterMenu"
[showDot]="hasActiveFilters$ | async"
[attr.aria-expanded]="expanded$ | async"
></iqser-icon-button>
<iqser-chevron-button
*ngIf="!primaryGroup.icon"
[label]="primaryGroup.label || ('filter-menu.label' | translate)"
[matMenuTriggerFor]="filterMenu"
[showDot]="hasActiveFilters$ | async"
[attr.aria-expanded]="expanded$ | async"
></iqser-chevron-button>
<mat-menu
#filterMenu="matMenu"
(close)="expanded.next(false)"
[class]="(secondaryFilterGroup$ | async)?.filters.length > 0 ? 'padding-bottom-0' : ''"
xPosition="before"
>
<ng-template matMenuContent>
<div class="filter-menu-header">
<div class="all-caps-label" translate="filter-menu.filter-types"></div>
<div class="actions">
<div
(click)="activatePrimaryFilters(); $event.stopPropagation()"
class="all-caps-label primary pointer"
translate="actions.all"
></div>
<div
(click)="deactivateFilters(); $event.stopPropagation()"
class="all-caps-label primary pointer"
translate="actions.none"
></div>
</div>
</div>
<div class="filter-content">
<ng-container
*ngFor="let filter of primaryGroup.filters"
[ngTemplateOutletContext]="{
filter: filter,
atLeastOneIsExpandable: atLeastOneFilterIsExpandable$ | async
}"
[ngTemplateOutlet]="defaultFilterTemplate"
></ng-container>
</div>
<div *ngIf="secondaryFilterGroup$ | async as secondaryGroup" class="filter-options">
<div class="filter-menu-options">
<div class="all-caps-label" translate="filter-menu.filter-options"></div>
</div>
<div *ngFor="let filter of secondaryGroup.filters">
<ng-container
[ngTemplateOutletContext]="{
filter: filter,
atLeastOneIsExpandable: atLeastOneSecondaryFilterIsExpandable$ | async
}"
[ngTemplateOutlet]="defaultFilterTemplate"
></ng-container>
</div>
</div>
</ng-template>
</mat-menu>
<ng-template #defaultFilterLabelTemplate let-filter="filter">
{{ filter?.label }}
</ng-template>
<ng-template #defaultFilterTemplate let-atLeastOneIsExpandable="atLeastOneIsExpandable" let-filter="filter">
<div (click)="toggleFilterExpanded($event, filter)" class="mat-menu-item flex">
<div *ngIf="filter.children?.length > 0" class="arrow-wrapper">
<mat-icon *ngIf="filter.expanded" color="accent" svgIcon="iqser:arrow-down"></mat-icon>
<mat-icon *ngIf="!filter.expanded" color="accent" svgIcon="red:arrow-right"></mat-icon>
</div>
<div *ngIf="atLeastOneIsExpandable && filter.children?.length === 0" class="arrow-wrapper spacer">&nbsp;</div>
<mat-checkbox
(click)="filterCheckboxClicked($event, filter)"
[checked]="filter.checked"
[indeterminate]="filter.indeterminate"
class="filter-menu-checkbox"
>
<ng-container
[ngTemplateOutletContext]="{ filter: filter }"
[ngTemplateOutlet]="primaryGroup.filterTemplate ?? defaultFilterLabelTemplate"
></ng-container>
</mat-checkbox>
<ng-container [ngTemplateOutletContext]="{ filter: filter }" [ngTemplateOutlet]="actionsTemplate"></ng-container>
</div>
<div *ngIf="filter.children?.length && filter.expanded">
<div (click)="$event.stopPropagation()" *ngFor="let child of filter.children" class="padding-left mat-menu-item">
<mat-checkbox (click)="filterCheckboxClicked($event, child, filter)" [checked]="child.checked">
<ng-container
[ngTemplateOutletContext]="{ filter: child }"
[ngTemplateOutlet]="primaryGroup.filterTemplate ?? defaultFilterLabelTemplate"
></ng-container>
</mat-checkbox>
<ng-container [ngTemplateOutletContext]="{ filter: child }" [ngTemplateOutlet]="actionsTemplate"></ng-container>
</div>
</div>
</ng-template>
</ng-container>

View File

@ -0,0 +1,41 @@
@import '../../../assets/styles/variables';
.filter-menu-options,
.filter-menu-header {
display: flex;
justify-content: space-between;
padding: 8px 16px 16px 16px;
width: 350px;
.actions {
display: flex;
> *:not(:last-child) {
margin-right: 8px;
}
}
}
.filter-content {
max-height: 570px;
overflow: auto;
}
.filter-menu-options {
margin-top: 8px;
padding: 16px 16px 3px;
}
.filter-options {
background-color: $filter-bg;
padding-bottom: 8px;
}
::ng-deep .filter-menu-checkbox {
width: 100%;
label {
width: 100%;
height: 100%;
}
}

View File

@ -0,0 +1,114 @@
import { ChangeDetectionStrategy, Component, Input, OnInit, TemplateRef } from '@angular/core';
import { MAT_CHECKBOX_DEFAULT_OPTIONS } from '@angular/material/checkbox';
import { BehaviorSubject, combineLatest, Observable, pipe } from 'rxjs';
import { delay, distinctUntilChanged, map, shareReplay } from 'rxjs/operators';
import { any } from '../../utils/operators';
import { handleCheckedValue } from '../filter-utils';
import { FilterService } from '../filter.service';
import { FilterGroup } from '../models/filter-group.model';
import { NestedFilter } from '../models/nested-filter.model';
const areExpandable = (nestedFilter: NestedFilter) => !!nestedFilter?.children?.length;
const atLeastOneIsExpandable = pipe(
map<FilterGroup | undefined, boolean>(group => !!group?.filters.some(areExpandable)),
distinctUntilChanged(),
shareReplay()
);
@Component({
selector: 'iqser-popup-filter [primaryFiltersSlug]',
templateUrl: './popup-filter.component.html',
styleUrls: ['./popup-filter.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: MAT_CHECKBOX_DEFAULT_OPTIONS,
useValue: {
clickAction: 'noop',
color: 'primary'
}
}
]
})
export class PopupFilterComponent implements OnInit {
@Input() primaryFiltersSlug!: string;
@Input() actionsTemplate?: TemplateRef<unknown>;
@Input() secondaryFiltersSlug = '';
atLeastOneFilterIsExpandable$?: Observable<boolean>;
atLeastOneSecondaryFilterIsExpandable$?: Observable<boolean>;
hasActiveFilters$?: Observable<boolean>;
readonly expanded = new BehaviorSubject<boolean>(false);
readonly expanded$ = this.expanded.asObservable().pipe(delay(200));
primaryFilterGroup$!: Observable<FilterGroup | undefined>;
secondaryFilterGroup$!: Observable<FilterGroup | undefined>;
constructor(readonly filterService: FilterService) {}
ngOnInit(): void {
this.primaryFilterGroup$ = this.filterService.getGroup$(this.primaryFiltersSlug);
this.secondaryFilterGroup$ = this.filterService.getGroup$(this.secondaryFiltersSlug);
this.hasActiveFilters$ = this._hasActiveFilters$;
this.atLeastOneFilterIsExpandable$ = atLeastOneIsExpandable(this.primaryFilterGroup$);
this.atLeastOneSecondaryFilterIsExpandable$ = atLeastOneIsExpandable(this.secondaryFilterGroup$);
}
filterCheckboxClicked($event: MouseEvent, nestedFilter: NestedFilter, parent?: NestedFilter): void {
$event.stopPropagation();
// eslint-disable-next-line no-param-reassign
nestedFilter.checked = !nestedFilter.checked;
if (parent) {
handleCheckedValue(parent);
} else {
// eslint-disable-next-line no-param-reassign
if (nestedFilter.indeterminate) nestedFilter.checked = false;
// eslint-disable-next-line no-param-reassign
nestedFilter.indeterminate = false;
// eslint-disable-next-line no-return-assign,no-param-reassign
nestedFilter.children?.forEach(f => (f.checked = nestedFilter.checked));
}
this.filterService.refresh();
}
activatePrimaryFilters(): void {
this._setFilters(this.primaryFiltersSlug, true);
}
deactivateFilters(): void {
this._setFilters(this.primaryFiltersSlug);
if (this.secondaryFiltersSlug) this._setFilters(this.secondaryFiltersSlug);
}
toggleFilterExpanded($event: MouseEvent, nestedFilter: NestedFilter): void {
$event.stopPropagation();
// eslint-disable-next-line no-param-reassign
nestedFilter.expanded = !nestedFilter.expanded;
this.filterService.refresh();
}
private get _hasActiveFilters$() {
return combineLatest([this.primaryFilterGroup$, this.secondaryFilterGroup$]).pipe(
map(([primary, secondary]) => [...(primary?.filters || []), ...(secondary?.filters || [])]),
any(f => f.checked || !!f.indeterminate),
distinctUntilChanged()
);
}
private _setFilters(filterGroup: string, checked = false) {
const filters = this.filterService.getGroup(filterGroup)?.filters;
filters?.forEach(f => {
// eslint-disable-next-line no-param-reassign
f.checked = checked;
// eslint-disable-next-line no-param-reassign
f.indeterminate = false;
// eslint-disable-next-line no-return-assign,no-param-reassign
f.children?.forEach(ff => (ff.checked = checked));
});
this.filterService.refresh();
}
}

View File

@ -1,4 +1,4 @@
@import '../../../assets/styles/common';
@import '../../../assets/styles/variables';
:host {
display: flex;

View File

@ -0,0 +1,8 @@
<section class="dialog">
<div class="content">
<p class="heading-l pre" [innerHTML]="'help-mode.welcome-to-help-mode' | translate"></p>
<img src="assets/illustrations/illustration.gif" alt="" width="335" />
<p class="pre" [innerHTML]="'help-mode.clicking-anywhere-on' | translate"></p>
</div>
<iqser-circle-button class="dialog-close" icon="iqser:close" mat-dialog-close></iqser-circle-button>
</section>

View File

@ -0,0 +1,16 @@
section {
background: #ecedf0;
display: flex;
justify-content: center;
}
.content {
width: 440px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding-top: 20px;
padding-bottom: 30px;
line-height: 18px;
}

View File

@ -0,0 +1,8 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
templateUrl: './help-mode-dialog.component.html',
styleUrls: ['./help-mode-dialog.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HelpModeDialogComponent {}

View File

@ -0,0 +1,34 @@
import { Directive, ElementRef, HostListener, Input, OnInit, Renderer2 } from '@angular/core';
import { HelpModeService } from './help-mode.service';
@Directive({
selector: '[iqserHelpMode]',
exportAs: 'iqserHelpMode'
})
export class HelpModeDirective implements OnInit {
@Input('iqserHelpMode') elementName!: string;
constructor(
private readonly _elementRef: ElementRef,
private readonly _renderer: Renderer2,
private readonly _helpModeService: HelpModeService
) {}
ngOnInit(): void {
this._createHelperElement();
}
private _createHelperElement() {
const element = this._elementRef.nativeElement as HTMLElement;
const helperElement = this._renderer.createElement('div') as HTMLElement;
this._renderer.addClass(helperElement, 'help-mode-on-mouse-over');
this._renderer.addClass(helperElement, `help-mode-on-mouse-over-${this.elementName}`);
this._helpModeService.addElement(this.elementName, element, helperElement);
}
@HostListener('click') onClick(): void {
this._helpModeService.openDocsFor(this.elementName);
}
}

View File

@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatIconModule } from '@angular/material/icon';
import { TranslateModule } from '@ngx-translate/core';
import { MatDialogModule } from '@angular/material/dialog';
import { HelpModeDialogComponent } from './help-mode-dialog/help-mode-dialog.component';
import { HelpModeComponent } from './help-mode/help-mode.component';
import { HelpModeDirective } from './help-mode.directive';
import { IqserButtonsModule } from '../buttons';
import { HelpModeService } from './help-mode.service';
const components = [HelpModeComponent, HelpModeDialogComponent, HelpModeDirective];
@NgModule({
declarations: [...components],
imports: [CommonModule, MatIconModule, MatDialogModule, TranslateModule, IqserButtonsModule],
exports: [...components],
providers: [HelpModeService]
})
export class IqserHelpModeModule {}

View File

@ -0,0 +1,100 @@
import { Inject, Injectable, Renderer2, RendererFactory2 } from '@angular/core';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject } from 'rxjs';
import { HelpModeDialogComponent } from './help-mode-dialog/help-mode-dialog.component';
import { HELP_DOCS } from './tokens';
interface Helper {
readonly element: HTMLElement;
readonly helperElement: HTMLElement;
}
@Injectable()
export class HelpModeService {
private readonly _isHelpModeActive$ = new BehaviorSubject(false);
readonly isHelpModeActive$ = this._isHelpModeActive$.asObservable();
private readonly _helpModeDialogIsOpened$ = new BehaviorSubject(false);
readonly helpModeDialogIsOpened$ = this._helpModeDialogIsOpened$.asObservable();
private readonly _elements: Record<string, Helper> = {};
private readonly _renderer: Renderer2;
constructor(
@Inject(HELP_DOCS) private readonly _docs: Record<string, Record<string, string>>,
private readonly _dialog: MatDialog,
private readonly _rendererFactory: RendererFactory2,
private readonly _translateService: TranslateService
) {
this._renderer = this._rendererFactory.createRenderer(null, null);
}
get isHelpModeActive(): boolean {
return this._isHelpModeActive$.getValue();
}
get helpModeDialogIsOpened(): boolean {
return this._helpModeDialogIsOpened$.getValue();
}
openHelpModeDialog(): MatDialogRef<HelpModeDialogComponent> {
this._helpModeDialogIsOpened$.next(true);
const ref = this._dialog.open(HelpModeDialogComponent, {
width: '600px'
});
ref.afterClosed()
.toPromise()
.then(() => {
this._helpModeDialogIsOpened$.next(false);
});
return ref;
}
openDocsFor(elementName: string): void {
if (this.isHelpModeActive) {
window.open(this._docs[elementName][this._translateService.currentLang]);
}
}
activateHelpMode(): void {
this._isHelpModeActive$.next(true);
this.openHelpModeDialog();
this._enableHelperElements();
}
deactivateHelpMode(): void {
this._isHelpModeActive$.next(false);
this._disableHelperElements();
}
highlightHelperElements(): void {
if (!this.isHelpModeActive) return;
Object.values(this._elements).forEach(({ helperElement }) => {
this._renderer.addClass(helperElement, 'highlight');
setTimeout(() => {
this._renderer.removeClass(helperElement, 'highlight');
}, 500);
});
}
addElement(elementName: string, element: HTMLElement, helperElement: HTMLElement): void {
this._elements[elementName] = { element, helperElement };
}
private _enableHelperElements() {
Object.values(this._elements).forEach(({ element, helperElement }) => {
this._renderer.setStyle(element, 'position', 'relative');
this._renderer.appendChild(element, helperElement);
});
}
private _disableHelperElements() {
Object.values(this._elements).forEach(({ element, helperElement }) => {
this._renderer.removeStyle(element, 'position');
this._renderer.removeChild(element, helperElement);
});
}
}

View File

@ -0,0 +1,17 @@
<div class="help-button" *ngIf="(helpModeService.isHelpModeActive$ | async) === false" (click)="helpModeService.activateHelpMode()">
<mat-icon svgIcon="iqser:help-outline"></mat-icon>
<div class="text">{{ 'help-mode.button-text' | translate }}</div>
</div>
<div class="help-mode-border" *ngIf="helpModeService.isHelpModeActive$ | async">
<div class="bottom">
<p class="heading">{{ 'help-mode.text' | translate }}</p>
<a *ngIf="(helpModeService.helpModeDialogIsOpened$ | async) === false" (click)="helpModeService.openHelpModeDialog()">
{{ 'help-mode.instructions' | translate }}
</a>
<div class="close">
(esc)
<iqser-circle-button icon="iqser:close" (click)="helpModeService.deactivateHelpMode()"></iqser-circle-button>
</div>
</div>
</div>

View File

@ -0,0 +1,72 @@
@import '../../../../../../apps/red-ui/src/assets/styles/variables';
.help-button {
width: 44px;
height: 40px;
position: absolute;
bottom: 20px;
right: 0;
z-index: 1;
background: $green-2;
border-top-left-radius: 8px;
border-bottom-left-radius: 8px;
box-shadow: -1px 1px 5px 0 rgba(40, 50, 65, 0.25);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.25s;
}
.help-button:hover {
cursor: pointer;
width: fit-content;
padding-left: 10px;
padding-right: 10px;
.text {
display: block;
}
mat-icon {
padding-right: 8px;
}
}
.text {
display: none;
}
.help-mode-border {
box-sizing: border-box;
height: 100%;
width: 100%;
border-left: 8px solid $green-2;
border-right: 8px solid $green-2;
border-top: 8px solid $green-2;
border-bottom: 60px solid $green-2;
z-index: 10;
position: absolute;
display: flex;
justify-content: center;
.bottom {
position: fixed;
height: 60px;
width: 95%;
bottom: 0;
display: flex;
justify-content: space-between;
align-items: center;
pointer-events: visiblePainted;
a {
color: black;
text-decoration: underline;
}
.close {
display: flex;
align-items: center;
}
}
}

View File

@ -0,0 +1,30 @@
import { ChangeDetectionStrategy, Component, HostListener } from '@angular/core';
import { HelpModeService } from '../help-mode.service';
import { IqserEventTarget } from '../../utils/types/events.type';
@Component({
selector: 'iqser-help-mode',
templateUrl: './help-mode.component.html',
styleUrls: ['./help-mode.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HelpModeComponent {
constructor(readonly helpModeService: HelpModeService) {}
@HostListener('document:keydown.escape') onEscKeydownHandler(): void {
if (!this.helpModeService.helpModeDialogIsOpened) {
this.helpModeService.deactivateHelpMode();
}
}
@HostListener('document:keydown.h', ['$event']) onHKeydownHandler(event: KeyboardEvent): void {
const node = (event.target as IqserEventTarget).localName;
if (!this.helpModeService.isHelpModeActive && node !== 'input' && node !== 'textarea') {
this.helpModeService.activateHelpMode();
}
}
@HostListener('click') onClick(): void {
this.helpModeService.highlightHelperElements();
}
}

View File

@ -0,0 +1,6 @@
export * from './tokens';
export * from './help-mode.module';
export * from './help-mode.service';
export * from './help-mode.directive';
export * from './help-mode/help-mode.component';
export * from './help-mode-dialog/help-mode-dialog.component';

View File

@ -0,0 +1,3 @@
import { InjectionToken } from '@angular/core';
export const HELP_DOCS = new InjectionToken<Record<string, Record<string, string>>>('Links to user manual or help docs');

View File

@ -1,23 +1,24 @@
<div *ngIf="showPreview && !editing">
{{ value }}
</div>
<form (submit)="saveValue()" *ngIf="editing">
<div [class]="'red-input-group ' + class">
<input [(ngModel)]="newValue" [placeholder]="placeholder" name="name" />
<ng-container *ngIf="!editing">
<div *ngIf="showPreview">
{{ value }}
</div>
</form>
<iqser-circle-button
(action)="editing = true"
*ngIf="!editing"
[tooltip]="editTooltip"
[type]="buttonsType"
class="edit-button"
icon="red:edit"
></iqser-circle-button>
<iqser-circle-button
(action)="editing = true"
[tooltip]="editTooltip"
[type]="buttonsType"
class="edit-button"
icon="iqser:edit"
></iqser-circle-button>
</ng-container>
<ng-container *ngIf="editing">
<iqser-circle-button (action)="saveValue()" [tooltip]="saveTooltip" [type]="buttonsType" icon="red:check"></iqser-circle-button>
<iqser-circle-button (action)="editing = false" [tooltip]="cancelTooltip" [type]="buttonsType" icon="red:close"></iqser-circle-button>
<form (submit)="saveValue()">
<div [class]="'iqser-input-group ' + class">
<input [(ngModel)]="newValue" [placeholder]="placeholder" name="name" />
</div>
</form>
<iqser-circle-button (action)="saveValue()" [tooltip]="saveTooltip" [type]="buttonsType" icon="iqser:check"></iqser-circle-button>
<iqser-circle-button (action)="editing = false" [tooltip]="cancelTooltip" [type]="buttonsType" icon="iqser:close"></iqser-circle-button>
</ng-container>

View File

@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, EventEmitter, HostBinding, Input, Output } from '@angular/core';
import { Required } from '../../utils/decorators/required.decorator';
import { CircleButtonType } from '../../buttons/circle-button/circle-button.type';
import { CircleButtonType } from '../../buttons/types/circle-button.type';
@Component({
selector: 'iqser-editable-input',

View File

@ -0,0 +1,25 @@
<div [style.max-width]="computedWidth" [style.width]="computedWidth" class="iqser-input-group">
<input
[(ngModel)]="value"
(ngModelChange)="valueChange.emit($event)"
[autocomplete]="autocomplete"
[placeholder]="placeholder"
class="with-icon mt-0"
type="text"
/>
<span *ngIf="hint" class="hint">{{ hint }}</span>
<mat-icon *ngIf="isSearch && !hasContent" class="icon-right" svgIcon="iqser:search"></mat-icon>
<iqser-circle-button (action)="reset()" *ngIf="isSearch && hasContent" [size]="25" icon="iqser:close"></iqser-circle-button>
<iqser-circle-button
(action)="executeAction($event)"
*ngIf="!isSearch"
[disabled]="!hasContent"
[icon]="icon"
[isSubmit]="true"
[size]="25"
></iqser-circle-button>
</div>

View File

@ -0,0 +1,14 @@
:host {
display: block;
}
mat-icon.disabled {
opacity: 0.7;
cursor: not-allowed;
}
iqser-circle-button {
position: absolute;
top: 4px;
right: 5px;
}

View File

@ -0,0 +1,43 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'iqser-input-with-action',
templateUrl: './input-with-action.component.html',
styleUrls: ['./input-with-action.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class InputWithActionComponent {
@Input() placeholder = '';
@Input() hint?: string;
@Input() width: number | 'full' = 250;
@Input() icon?: string;
@Input() autocomplete: 'on' | 'off' = 'on';
@Input() value = '';
@Output() readonly action = new EventEmitter<string>();
@Output() readonly valueChange = new EventEmitter<string>();
get hasContent(): boolean {
return !!this.value?.length;
}
get computedWidth(): string {
return this.width === 'full' ? '100%' : `${this.width}px`;
}
reset(): void {
this.value = '';
this.valueChange.emit(this.value);
}
get isSearch(): boolean {
return this.action.observers.length === 0;
}
executeAction($event: MouseEvent): void {
$event.stopPropagation();
if (this.hasContent) {
this.action.emit(this.value);
}
}
}

View File

@ -1,31 +1,19 @@
import { Injectable } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { map, startWith } from 'rxjs/operators';
import { BehaviorSubject } from 'rxjs';
import { KeysOf } from '../utils/types/utility-types';
const controlsConfig = {
query: ['']
} as const;
type FormControls = { [key in KeysOf<typeof controlsConfig>]: string };
@Injectable()
export class SearchService<T> {
readonly searchForm = this._formBuilder.group(controlsConfig);
readonly valueChanges$ = this.searchForm.valueChanges.pipe(
startWith(''),
map((values: FormControls) => values.query)
);
private readonly _query$ = new BehaviorSubject('');
readonly valueChanges$ = this._query$.asObservable();
private _searchKey!: KeysOf<T>;
constructor(private readonly _formBuilder: FormBuilder) {}
get searchValue(): string {
return this.searchForm.get('query')?.value as string;
return this._query$.getValue();
}
set searchValue(value: string) {
this.searchForm.patchValue({ query: value });
this._query$.next(value);
}
searchIn(entities: T[]): T[] {
@ -40,7 +28,7 @@ export class SearchService<T> {
}
reset(): void {
this.searchForm.reset({ query: '' }, { emitEvent: true });
this._query$.next('');
}
private _searchField(entity: T): string {

View File

@ -0,0 +1,19 @@
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { HttpErrorResponse } from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class ErrorMessageService {
constructor(private readonly _translateService: TranslateService) {}
private _parseErrorResponse(err: HttpErrorResponse): string {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/restrict-template-expressions
return err?.error?.message?.includes('message') ? ` ${err.error.message.match('"message":"(.*?)\\"')[1]}` : '';
}
getMessage(error: HttpErrorResponse, defaultMessage: string): string {
return (this._translateService.instant(defaultMessage) as string) + this._parseErrorResponse(error);
}
}

View File

@ -0,0 +1,85 @@
import { Injectable } from '@angular/core';
import { ActiveToast, ToastrService } from 'ngx-toastr';
import { IndividualConfig } from 'ngx-toastr/toastr/toastr-config';
import { NavigationStart, Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { HttpErrorResponse } from '@angular/common/http';
import { filter } from 'rxjs/operators';
import { ErrorMessageService } from './error-message.service';
const enum NotificationType {
SUCCESS = 'SUCCESS',
WARNING = 'WARNING',
INFO = 'INFO'
}
export interface ToasterOptions extends IndividualConfig {
readonly title?: string;
/**
* These params are used as interpolateParams for translate service
*/
// eslint-disable-next-line @typescript-eslint/ban-types
readonly params?: object;
readonly actions?: { readonly title?: string; readonly action: () => void }[];
}
export interface ErrorToasterOptions extends ToasterOptions {
/**
* Pass an http error that will be processed by error message service and shown in toast
*/
readonly error?: HttpErrorResponse;
}
@Injectable({
providedIn: 'root'
})
export class Toaster {
constructor(
private readonly _toastr: ToastrService,
private readonly _router: Router,
private readonly _translateService: TranslateService,
private readonly _errorMessageService: ErrorMessageService
) {
_router.events.pipe(filter(event => event instanceof NavigationStart)).subscribe(() => {
_toastr.clear();
});
}
error(message: string, options?: Partial<ErrorToasterOptions>): ActiveToast<unknown> {
let resultedMsg;
if (options?.error) resultedMsg = this._errorMessageService.getMessage(options.error, message);
else resultedMsg = this._translateService.instant(message, options?.params) as string;
return this._toastr.error(resultedMsg, options?.title, options);
}
info(message: string, options?: Partial<ToasterOptions>): ActiveToast<unknown> {
return this._showToastNotification(message, NotificationType.INFO, options);
}
success(message: string, options?: Partial<ToasterOptions>): ActiveToast<unknown> {
return this._showToastNotification(message, NotificationType.SUCCESS, options);
}
warning(message: string, options?: Partial<ToasterOptions>): ActiveToast<unknown> {
return this._showToastNotification(message, NotificationType.WARNING, options);
}
private _showToastNotification(
message: string,
notificationType = NotificationType.INFO,
options?: Partial<ToasterOptions>
): ActiveToast<unknown> {
const translatedMsg = this._translateService.instant(message, options?.params) as string;
switch (notificationType) {
case NotificationType.SUCCESS:
return this._toastr.success(translatedMsg, options?.title, options);
case NotificationType.WARNING:
return this._toastr.warning(translatedMsg, options?.title, options);
case NotificationType.INFO:
default:
return this._toastr.info(translatedMsg, options?.title, options);
}
}
}

View File

@ -4,12 +4,13 @@ import { distinctUntilChanged, map, tap } from 'rxjs/operators';
import { FilterService } from '../filtering/filter.service';
import { SearchService } from '../search/search.service';
import { getFilteredEntities } from '../filtering/filter-utils';
import { Listable } from './models/listable';
const toLengthValue = (entities: unknown[]) => entities?.length ?? 0;
const getLength = pipe(map(toLengthValue), distinctUntilChanged());
@Injectable()
export class EntitiesService<T> {
export class EntitiesService<T extends Listable> {
private readonly _all$ = new BehaviorSubject<T[]>([]);
readonly all$ = this._all$.asObservable();
readonly allLength$ = this._all$.pipe(getLength);
@ -18,8 +19,9 @@ export class EntitiesService<T> {
readonly displayed$ = this._getDisplayed$;
readonly displayedLength$ = this.displayed$.pipe(getLength);
private readonly _selected$ = new BehaviorSubject<T[]>([]);
private readonly _selected$ = new BehaviorSubject<(string | number)[]>([]);
readonly selected$ = this._selected$.asObservable();
readonly selectedEntities$ = this._selected$.asObservable().pipe(map(() => this.selected));
readonly selectedLength$ = this._selected$.pipe(getLength);
readonly noData$ = this._noData$;
@ -34,7 +36,8 @@ export class EntitiesService<T> {
}
get selected(): T[] {
return Object.values(this._selected$.getValue());
const selectedIds = Object.values(this._selected$.getValue());
return this.all.filter(a => selectedIds.indexOf(a.id) !== -1);
}
private get _getDisplayed$(): Observable<T[]> {
@ -87,7 +90,8 @@ export class EntitiesService<T> {
}
setSelected(newEntities: T[]): void {
this._selected$.next(newEntities);
const selectedIds = newEntities.map(e => e.id);
this._selected$.next(selectedIds);
}
isSelected(entity: T): boolean {

View File

@ -10,11 +10,12 @@ import { SearchService } from '../search/search.service';
import { KeysOf } from '../utils/types/utility-types';
import { TableColumnConfig } from './models/table-column-config.model';
import { EntitiesService } from './entities.service';
import { Listable } from './models/listable';
export const DefaultListingServices = [FilterService, SearchService, EntitiesService, SortingService] as const;
@Directive()
export abstract class ListingComponent<T> extends AutoUnsubscribe implements OnDestroy {
export abstract class ListingComponent<T extends Listable> extends AutoUnsubscribe implements OnDestroy {
readonly filterService = this._injector.get(FilterService);
readonly searchService = this._injector.get<SearchService<T>>(SearchService);
readonly sortingService = this._injector.get<SortingService<T>>(SortingService);

View File

@ -0,0 +1,3 @@
export interface Listable {
readonly id: string | number;
}

View File

@ -12,7 +12,7 @@
<ng-container [ngTemplateOutlet]="bulkActions"></ng-container>
<iqser-quick-filters *ngIf="filterService.filterGroups.length"></iqser-quick-filters>
<iqser-quick-filters *ngIf="filterService.getFilterModels$('quickFilters') | async"></iqser-quick-filters>
<!-- Custom content-->
<ng-content></ng-content>

View File

@ -3,6 +3,7 @@ import { Required } from '../../utils/decorators/required.decorator';
import { FilterService } from '../../filtering/filter.service';
import { TableColumnConfig } from '../models/table-column-config.model';
import { EntitiesService } from '../entities.service';
import { Listable } from '../models/listable';
@Component({
selector: 'iqser-table-header',
@ -10,7 +11,7 @@ import { EntitiesService } from '../entities.service';
styleUrls: ['./table-header.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TableHeaderComponent<T> {
export class TableHeaderComponent<T extends Listable> {
@Input() @Required() tableHeaderLabel!: string;
@Input() @Required() tableColumnConfigs!: readonly TableColumnConfig<T>[];
@Input() hasEmptyColumn = false;

View File

@ -0,0 +1,10 @@
import { map } from 'rxjs/operators';
import { OperatorFunction } from 'rxjs';
export function get<T>(predicate: (value: T, index: number) => boolean): OperatorFunction<T[], T | undefined> {
return map(entities => entities.find(predicate));
}
export function any<T>(predicate: (value: T, index: number) => boolean): OperatorFunction<T[], boolean> {
return map(entities => entities.some(predicate));
}

View File

@ -0,0 +1,3 @@
export interface IqserEventTarget extends EventTarget {
localName: string;
}