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/no-output-rename": "error",
|
||||||
"@angular-eslint/prefer-output-readonly": "error",
|
"@angular-eslint/prefer-output-readonly": "error",
|
||||||
"@typescript-eslint/unbound-method": "error",
|
"@typescript-eslint/unbound-method": "error",
|
||||||
|
"@typescript-eslint/no-floating-promises": "off",
|
||||||
"@typescript-eslint/lines-between-class-members": "off",
|
"@typescript-eslint/lines-between-class-members": "off",
|
||||||
"@typescript-eslint/naming-convention": [
|
"@typescript-eslint/naming-convention": [
|
||||||
"error",
|
"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
|
// 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-hover: #e2e4e9 !default;
|
||||||
$btn-bg: #f0f1f4 !default;
|
$btn-bg: #f0f1f4 !default;
|
||||||
@ -7,3 +7,4 @@ $warn: #fdbd00 !default;
|
|||||||
$white: white !default;
|
$white: white !default;
|
||||||
$separator: rgba(226, 228, 233, 0.9) !default;
|
$separator: rgba(226, 228, 233, 0.9) !default;
|
||||||
$quick-filter-border: #d3d5da !default;
|
$quick-filter-border: #d3d5da !default;
|
||||||
|
$filter-bg: #f4f5f7 !default;
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
|
@import 'inputs';
|
||||||
@import 'buttons';
|
@import 'buttons';
|
||||||
@import 'texts';
|
@import 'texts';
|
||||||
@import 'tables';
|
@import 'tables';
|
||||||
@import 'full-pages';
|
@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/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/functions';
|
||||||
|
export * from './lib/utils/operators';
|
||||||
export * from './lib/utils/auto-unsubscribe.directive';
|
export * from './lib/utils/auto-unsubscribe.directive';
|
||||||
export * from './lib/utils/pipes/humanize.pipe';
|
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/utility-types';
|
||||||
export * from './lib/utils/types/tooltip-positions.type';
|
export * from './lib/utils/types/tooltip-positions.type';
|
||||||
export * from './lib/utils/decorators/bind.decorator';
|
export * from './lib/utils/decorators/bind.decorator';
|
||||||
export * from './lib/utils/decorators/required.decorator';
|
export * from './lib/utils/decorators/required.decorator';
|
||||||
export * from './lib/utils/decorators/debounce.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-utils';
|
||||||
export * from './lib/filtering/filter.service';
|
export * from './lib/filtering/filter.service';
|
||||||
export * from './lib/filtering/models/filter.model';
|
export * from './lib/filtering/models/filter.model';
|
||||||
export * from './lib/filtering/models/filter-group.model';
|
export * from './lib/filtering/models/filter-group.model';
|
||||||
export * from './lib/filtering/models/nested-filter.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/filtering/quick-filters/quick-filters.component';
|
||||||
export * from './lib/sorting/sort-by.pipe';
|
export * from './lib/sorting/sort-by.pipe';
|
||||||
export * from './lib/sorting/sorting.service';
|
export * from './lib/sorting/sorting.service';
|
||||||
export * from './lib/sorting/models/sorting-option.model';
|
export * from './lib/sorting/models/sorting-option.model';
|
||||||
export * from './lib/sorting/models/sorting-order.type';
|
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/search/search.service';
|
||||||
export * from './lib/tables/entities.service';
|
export * from './lib/tables/entities.service';
|
||||||
export * from './lib/tables/listing-component.directive';
|
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/tables/table-header/table-header.component';
|
||||||
export * from './lib/misc/status-bar/status-bar.component';
|
export * from './lib/misc/status-bar/status-bar.component';
|
||||||
export * from './lib/misc/status-bar/status-bar-config.model';
|
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/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/loading.service';
|
||||||
export * from './lib/loading/full-page-loading-indicator/full-page-loading-indicator.component';
|
export * from './lib/loading/full-page-loading-indicator/full-page-loading-indicator.component';
|
||||||
export * from './lib/error/error.service';
|
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 { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
|
||||||
import { MatTooltip } from '@angular/material/tooltip';
|
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 { Required } from '../../utils/decorators/required.decorator';
|
||||||
import { IqserTooltipPosition, IqserTooltipPositions } from '../../utils/types/tooltip-positions.type';
|
import { IqserTooltipPosition, IqserTooltipPositions } from '../../utils/types/tooltip-positions.type';
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
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';
|
import { Required } from '../../utils/decorators/required.decorator';
|
||||||
|
|
||||||
@Component({
|
@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 { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
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 { 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 { RoundCheckboxComponent } from './inputs/round-checkbox/round-checkbox.component';
|
||||||
import { SortByPipe } from './sorting/sort-by.pipe';
|
import { SortByPipe } from './sorting/sort-by.pipe';
|
||||||
import { HumanizePipe } from './utils/pipes/humanize.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 { SyncWidthDirective } from './tables/sync-width.directive';
|
||||||
import { StatusBarComponent } from './misc/status-bar/status-bar.component';
|
import { StatusBarComponent } from './misc/status-bar/status-bar.component';
|
||||||
import { EditableInputComponent } from './inputs/editable-input/editable-input.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 { FullPageLoadingIndicatorComponent } from './loading/full-page-loading-indicator/full-page-loading-indicator.component';
|
||||||
import { FullPageErrorComponent } from './error/full-page-error/full-page-error.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, IqserButtonsModule];
|
||||||
|
|
||||||
const modules = [...matModules, FormsModule, TranslateModule];
|
|
||||||
|
|
||||||
const components = [
|
const components = [
|
||||||
...buttons,
|
|
||||||
...inputs,
|
...inputs,
|
||||||
TableColumnNameComponent,
|
TableColumnNameComponent,
|
||||||
QuickFiltersComponent,
|
QuickFiltersComponent,
|
||||||
@ -50,7 +50,7 @@ const utils = [SortByPipe, HumanizePipe, SyncWidthDirective];
|
|||||||
})
|
})
|
||||||
export class CommonUiModule {
|
export class CommonUiModule {
|
||||||
constructor(private readonly _iconRegistry: MatIconRegistry, private readonly _sanitizer: DomSanitizer) {
|
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 => {
|
icons.forEach(icon => {
|
||||||
_iconRegistry.addSvgIconInNamespace('iqser', icon, _sanitizer.bypassSecurityTrustResourceUrl(`/assets/icons/${icon}.svg`));
|
_iconRegistry.addSvgIconInNamespace('iqser', icon, _sanitizer.bypassSecurityTrustResourceUrl(`/assets/icons/${icon}.svg`));
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||||
|
import { IconButtonTypes } from '../../buttons';
|
||||||
import { ErrorService } from '../error.service';
|
import { ErrorService } from '../error.service';
|
||||||
import { IconButtonTypes } from '../../buttons/icon-button/icon-button.type';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'iqser-full-page-error',
|
selector: 'iqser-full-page-error',
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators'
|
|||||||
import { processFilters, toFlatFilters } from './filter-utils';
|
import { processFilters, toFlatFilters } from './filter-utils';
|
||||||
import { FilterGroup } from './models/filter-group.model';
|
import { FilterGroup } from './models/filter-group.model';
|
||||||
import { NestedFilter } from './models/nested-filter.model';
|
import { NestedFilter } from './models/nested-filter.model';
|
||||||
|
import { get } from '../utils/operators';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FilterService {
|
export class FilterService {
|
||||||
@ -61,7 +62,7 @@ export class FilterService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getGroup$(slug: string): Observable<FilterGroup | undefined> {
|
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 {
|
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 {
|
:host {
|
||||||
display: flex;
|
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">
|
<ng-container *ngIf="!editing">
|
||||||
|
<div *ngIf="showPreview">
|
||||||
{{ value }}
|
{{ value }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form (submit)="saveValue()" *ngIf="editing">
|
<iqser-circle-button
|
||||||
<div [class]="'red-input-group ' + class">
|
(action)="editing = true"
|
||||||
|
[tooltip]="editTooltip"
|
||||||
|
[type]="buttonsType"
|
||||||
|
class="edit-button"
|
||||||
|
icon="iqser:edit"
|
||||||
|
></iqser-circle-button>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="editing">
|
||||||
|
<form (submit)="saveValue()">
|
||||||
|
<div [class]="'iqser-input-group ' + class">
|
||||||
<input [(ngModel)]="newValue" [placeholder]="placeholder" name="name" />
|
<input [(ngModel)]="newValue" [placeholder]="placeholder" name="name" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<iqser-circle-button
|
<iqser-circle-button (action)="saveValue()" [tooltip]="saveTooltip" [type]="buttonsType" icon="iqser:check"></iqser-circle-button>
|
||||||
(action)="editing = true"
|
<iqser-circle-button (action)="editing = false" [tooltip]="cancelTooltip" [type]="buttonsType" icon="iqser:close"></iqser-circle-button>
|
||||||
*ngIf="!editing"
|
|
||||||
[tooltip]="editTooltip"
|
|
||||||
[type]="buttonsType"
|
|
||||||
class="edit-button"
|
|
||||||
icon="red:edit"
|
|
||||||
></iqser-circle-button>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { ChangeDetectionStrategy, Component, EventEmitter, HostBinding, Input, Output } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, EventEmitter, HostBinding, Input, Output } from '@angular/core';
|
||||||
import { Required } from '../../utils/decorators/required.decorator';
|
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({
|
@Component({
|
||||||
selector: 'iqser-editable-input',
|
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 { Injectable } from '@angular/core';
|
||||||
import { FormBuilder } from '@angular/forms';
|
import { BehaviorSubject } from 'rxjs';
|
||||||
import { map, startWith } from 'rxjs/operators';
|
|
||||||
import { KeysOf } from '../utils/types/utility-types';
|
import { KeysOf } from '../utils/types/utility-types';
|
||||||
|
|
||||||
const controlsConfig = {
|
|
||||||
query: ['']
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
type FormControls = { [key in KeysOf<typeof controlsConfig>]: string };
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SearchService<T> {
|
export class SearchService<T> {
|
||||||
readonly searchForm = this._formBuilder.group(controlsConfig);
|
private readonly _query$ = new BehaviorSubject('');
|
||||||
readonly valueChanges$ = this.searchForm.valueChanges.pipe(
|
readonly valueChanges$ = this._query$.asObservable();
|
||||||
startWith(''),
|
|
||||||
map((values: FormControls) => values.query)
|
|
||||||
);
|
|
||||||
private _searchKey!: KeysOf<T>;
|
private _searchKey!: KeysOf<T>;
|
||||||
|
|
||||||
constructor(private readonly _formBuilder: FormBuilder) {}
|
|
||||||
|
|
||||||
get searchValue(): string {
|
get searchValue(): string {
|
||||||
return this.searchForm.get('query')?.value as string;
|
return this._query$.getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
set searchValue(value: string) {
|
set searchValue(value: string) {
|
||||||
this.searchForm.patchValue({ query: value });
|
this._query$.next(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
searchIn(entities: T[]): T[] {
|
searchIn(entities: T[]): T[] {
|
||||||
@ -40,7 +28,7 @@ export class SearchService<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reset(): void {
|
reset(): void {
|
||||||
this.searchForm.reset({ query: '' }, { emitEvent: true });
|
this._query$.next('');
|
||||||
}
|
}
|
||||||
|
|
||||||
private _searchField(entity: T): string {
|
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 { FilterService } from '../filtering/filter.service';
|
||||||
import { SearchService } from '../search/search.service';
|
import { SearchService } from '../search/search.service';
|
||||||
import { getFilteredEntities } from '../filtering/filter-utils';
|
import { getFilteredEntities } from '../filtering/filter-utils';
|
||||||
|
import { Listable } from './models/listable';
|
||||||
|
|
||||||
const toLengthValue = (entities: unknown[]) => entities?.length ?? 0;
|
const toLengthValue = (entities: unknown[]) => entities?.length ?? 0;
|
||||||
const getLength = pipe(map(toLengthValue), distinctUntilChanged());
|
const getLength = pipe(map(toLengthValue), distinctUntilChanged());
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EntitiesService<T> {
|
export class EntitiesService<T extends Listable> {
|
||||||
private readonly _all$ = new BehaviorSubject<T[]>([]);
|
private readonly _all$ = new BehaviorSubject<T[]>([]);
|
||||||
readonly all$ = this._all$.asObservable();
|
readonly all$ = this._all$.asObservable();
|
||||||
readonly allLength$ = this._all$.pipe(getLength);
|
readonly allLength$ = this._all$.pipe(getLength);
|
||||||
@ -18,8 +19,9 @@ export class EntitiesService<T> {
|
|||||||
readonly displayed$ = this._getDisplayed$;
|
readonly displayed$ = this._getDisplayed$;
|
||||||
readonly displayedLength$ = this.displayed$.pipe(getLength);
|
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 selected$ = this._selected$.asObservable();
|
||||||
|
readonly selectedEntities$ = this._selected$.asObservable().pipe(map(() => this.selected));
|
||||||
readonly selectedLength$ = this._selected$.pipe(getLength);
|
readonly selectedLength$ = this._selected$.pipe(getLength);
|
||||||
|
|
||||||
readonly noData$ = this._noData$;
|
readonly noData$ = this._noData$;
|
||||||
@ -34,7 +36,8 @@ export class EntitiesService<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get selected(): 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[]> {
|
private get _getDisplayed$(): Observable<T[]> {
|
||||||
@ -87,7 +90,8 @@ export class EntitiesService<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setSelected(newEntities: T[]): void {
|
setSelected(newEntities: T[]): void {
|
||||||
this._selected$.next(newEntities);
|
const selectedIds = newEntities.map(e => e.id);
|
||||||
|
this._selected$.next(selectedIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
isSelected(entity: T): boolean {
|
isSelected(entity: T): boolean {
|
||||||
|
|||||||
@ -10,11 +10,12 @@ import { SearchService } from '../search/search.service';
|
|||||||
import { KeysOf } from '../utils/types/utility-types';
|
import { KeysOf } from '../utils/types/utility-types';
|
||||||
import { TableColumnConfig } from './models/table-column-config.model';
|
import { TableColumnConfig } from './models/table-column-config.model';
|
||||||
import { EntitiesService } from './entities.service';
|
import { EntitiesService } from './entities.service';
|
||||||
|
import { Listable } from './models/listable';
|
||||||
|
|
||||||
export const DefaultListingServices = [FilterService, SearchService, EntitiesService, SortingService] as const;
|
export const DefaultListingServices = [FilterService, SearchService, EntitiesService, SortingService] as const;
|
||||||
|
|
||||||
@Directive()
|
@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 filterService = this._injector.get(FilterService);
|
||||||
readonly searchService = this._injector.get<SearchService<T>>(SearchService);
|
readonly searchService = this._injector.get<SearchService<T>>(SearchService);
|
||||||
readonly sortingService = this._injector.get<SortingService<T>>(SortingService);
|
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>
|
<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-->
|
<!-- Custom content-->
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { Required } from '../../utils/decorators/required.decorator';
|
|||||||
import { FilterService } from '../../filtering/filter.service';
|
import { FilterService } from '../../filtering/filter.service';
|
||||||
import { TableColumnConfig } from '../models/table-column-config.model';
|
import { TableColumnConfig } from '../models/table-column-config.model';
|
||||||
import { EntitiesService } from '../entities.service';
|
import { EntitiesService } from '../entities.service';
|
||||||
|
import { Listable } from '../models/listable';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'iqser-table-header',
|
selector: 'iqser-table-header',
|
||||||
@ -10,7 +11,7 @@ import { EntitiesService } from '../entities.service';
|
|||||||
styleUrls: ['./table-header.component.scss'],
|
styleUrls: ['./table-header.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class TableHeaderComponent<T> {
|
export class TableHeaderComponent<T extends Listable> {
|
||||||
@Input() @Required() tableHeaderLabel!: string;
|
@Input() @Required() tableHeaderLabel!: string;
|
||||||
@Input() @Required() tableColumnConfigs!: readonly TableColumnConfig<T>[];
|
@Input() @Required() tableColumnConfigs!: readonly TableColumnConfig<T>[];
|
||||||
@Input() hasEmptyColumn = false;
|
@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