Merge remote-tracking branch 'origin/master' into loading
This commit is contained in:
commit
fcdf16c77a
@ -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",
|
||||
|
||||
12
src/assets/icons/help-outline.svg
Normal file
12
src/assets/icons/help-outline.svg
Normal 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 |
9
src/assets/icons/search.svg
Normal file
9
src/assets/icons/search.svg
Normal 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 |
40
src/assets/styles/_dialogs.scss
Normal file
40
src/assets/styles/_dialogs.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
267
src/assets/styles/_inputs.scss
Normal file
267
src/assets/styles/_inputs.scss
Normal 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%;
|
||||
}
|
||||
}
|
||||
3
src/assets/styles/_layout.scss
Normal file
3
src/assets/styles/_layout.scss
Normal file
@ -0,0 +1,3 @@
|
||||
.mt-0 {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
@import 'inputs';
|
||||
@import 'buttons';
|
||||
@import 'texts';
|
||||
@import 'tables';
|
||||
@import 'full-pages';
|
||||
@import 'layout';
|
||||
@import 'dialogs';
|
||||
|
||||
14
src/index.ts
14
src/index.ts
@ -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';
|
||||
|
||||
17
src/lib/buttons/buttons.module.ts
Normal file
17
src/lib/buttons/buttons.module.ts
Normal 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 {}
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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
5
src/lib/buttons/index.ts
Normal 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';
|
||||
@ -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`));
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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 {
|
||||
|
||||
109
src/lib/filtering/popup-filter/popup-filter.component.html
Normal file
109
src/lib/filtering/popup-filter/popup-filter.component.html
Normal 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"> </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>
|
||||
41
src/lib/filtering/popup-filter/popup-filter.component.scss
Normal file
41
src/lib/filtering/popup-filter/popup-filter.component.scss
Normal 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%;
|
||||
}
|
||||
}
|
||||
114
src/lib/filtering/popup-filter/popup-filter.component.ts
Normal file
114
src/lib/filtering/popup-filter/popup-filter.component.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
@import '../../../assets/styles/common';
|
||||
@import '../../../assets/styles/variables';
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
|
||||
@ -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>
|
||||
@ -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;
|
||||
}
|
||||
@ -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 {}
|
||||
34
src/lib/help-mode/help-mode.directive.ts
Normal file
34
src/lib/help-mode/help-mode.directive.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
20
src/lib/help-mode/help-mode.module.ts
Normal file
20
src/lib/help-mode/help-mode.module.ts
Normal 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 {}
|
||||
100
src/lib/help-mode/help-mode.service.ts
Normal file
100
src/lib/help-mode/help-mode.service.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
17
src/lib/help-mode/help-mode/help-mode.component.html
Normal file
17
src/lib/help-mode/help-mode/help-mode.component.html
Normal 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>
|
||||
72
src/lib/help-mode/help-mode/help-mode.component.scss
Normal file
72
src/lib/help-mode/help-mode/help-mode.component.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/lib/help-mode/help-mode/help-mode.component.ts
Normal file
30
src/lib/help-mode/help-mode/help-mode.component.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
6
src/lib/help-mode/index.ts
Normal file
6
src/lib/help-mode/index.ts
Normal 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';
|
||||
3
src/lib/help-mode/tokens.ts
Normal file
3
src/lib/help-mode/tokens.ts
Normal 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');
|
||||
@ -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>
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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>
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
19
src/lib/services/error-message.service.ts
Normal file
19
src/lib/services/error-message.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
85
src/lib/services/toaster.service.ts
Normal file
85
src/lib/services/toaster.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
|
||||
3
src/lib/tables/models/listable.ts
Normal file
3
src/lib/tables/models/listable.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export interface Listable {
|
||||
readonly id: string | number;
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
10
src/lib/utils/operators.ts
Normal file
10
src/lib/utils/operators.ts
Normal 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));
|
||||
}
|
||||
3
src/lib/utils/types/events.type.ts
Normal file
3
src/lib/utils/types/events.type.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export interface IqserEventTarget extends EventTarget {
|
||||
localName: string;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user