diff --git a/src/assets/icons/offline.svg b/src/assets/icons/offline.svg new file mode 100644 index 0000000..d5c6610 --- /dev/null +++ b/src/assets/icons/offline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/styles/_common-functions.scss b/src/assets/styles/_common-functions.scss new file mode 100644 index 0000000..efdd0c0 --- /dev/null +++ b/src/assets/styles/_common-functions.scss @@ -0,0 +1,3 @@ +@function hexToRgb($color) { + @return #{red($color) + ', ' + green($color) + ', ' + blue($color)}; +} diff --git a/src/assets/styles/_mixins.scss b/src/assets/styles/_common-mixins.scss similarity index 73% rename from src/assets/styles/_mixins.scss rename to src/assets/styles/_common-mixins.scss index a49c0b9..e20c3d3 100644 --- a/src/assets/styles/_mixins.scss +++ b/src/assets/styles/_common-mixins.scss @@ -1,5 +1,3 @@ -@import 'variables'; - @mixin line-clamp($lines) { display: -webkit-box; -webkit-line-clamp: $lines; @@ -25,7 +23,7 @@ } @mixin scroll-bar { - scrollbar-color: $grey-5 $grey-2; + scrollbar-color: var(--iqser-quick-filter-border) var(--iqser-grey-2); scrollbar-width: thin; &::-webkit-scrollbar { @@ -34,19 +32,19 @@ /* Track */ &::-webkit-scrollbar-track { - background: $grey-2; + background: var(--iqser-grey-2); } /* Handle */ &::-webkit-scrollbar-thumb { - background: $grey-5; + background: var(--iqser-quick-filter-border); } } @mixin inset-shadow { - box-shadow: inset 0 4px 3px -2px $grey-4; + box-shadow: inset 0 4px 3px -2px var(--iqser-btn-bg-hover); } @mixin drop-shadow { - box-shadow: 0 4px 3px 2px $grey-4; + box-shadow: 0 4px 3px 2px var(--iqser-btn-bg-hover); } diff --git a/src/assets/styles/_common-variables.scss b/src/assets/styles/_common-variables.scss new file mode 100644 index 0000000..d62662f --- /dev/null +++ b/src/assets/styles/_common-variables.scss @@ -0,0 +1,31 @@ +@use 'sass:meta'; + +:root { + --iqser-primary: lightblue; + --iqser-primary-rgb: 220, 230, 234; + --iqser-primary-2: orange; + --iqser-accent: blue; + --iqser-accent-rgb: 123, 234, 111; + --iqser-disabled: #9398a0; + --iqser-not-disabled-table-item: #f9fafb; + --iqser-btn-bg-hover: #e2e4e9; + --iqser-btn-bg: #f0f1f4; + --iqser-warn: #fdbd00; + --iqser-white: white; + --iqser-separator: rgba(226, 228, 233, 0.9); + --iqser-quick-filter-border: #d3d5da; + --iqser-grey-2: #f4f5f7; + --iqser-grey-3: #aaacb3; + --iqser-grey-4: #e2e4e9; + --iqser-grey-5: #d3d5da; + --iqser-grey-6: #f0f1f4; + --iqser-helpmode-primary: green; +} + +@mixin configure($args...) { + :root { + @each $name, $value in meta.keywords($args) { + --#{$name}: #{$value}; + } + } +} diff --git a/src/assets/styles/_variables.scss b/src/assets/styles/_variables.scss deleted file mode 100644 index cf11624..0000000 --- a/src/assets/styles/_variables.scss +++ /dev/null @@ -1,10 +0,0 @@ -// This rebel line is crying (in WebStorm) but it actually works -@import '../../../../../apps/red-ui/src/assets/styles/variables'; - -$btn-bg-hover: #e2e4e9 !default; -$btn-bg: #f0f1f4 !default; -$warn: #fdbd00 !default; -$white: white !default; -$separator: rgba(226, 228, 233, 0.9) !default; -$quick-filter-border: #d3d5da !default; -$filter-bg: #f4f5f7 !default; diff --git a/src/assets/styles/_buttons.scss b/src/assets/styles/common-buttons.scss similarity index 71% rename from src/assets/styles/_buttons.scss rename to src/assets/styles/common-buttons.scss index 4c81a84..2624669 100644 --- a/src/assets/styles/_buttons.scss +++ b/src/assets/styles/common-buttons.scss @@ -1,5 +1,3 @@ -@import 'variables'; - .mat-button, .mat-flat-button { border-radius: 17px !important; @@ -36,19 +34,19 @@ padding: 0 14px; transition: background-color 0.2s, color 0.2s; - background-color: $primary; + background-color: var(--iqser-primary); &.mat-button-disabled { - background-color: $primary; + background-color: var(--iqser-primary); .mat-button-wrapper { - color: $white; + color: var(--iqser-white); opacity: 0.5; } } &:not(.mat-button-disabled):hover { - background-color: $primary-2; + background-color: var(--iqser-primary-2); } } @@ -64,30 +62,30 @@ iqser-circle-button { transition: background-color 0.2s; &.overlay { - background: rgba($primary, 0.1); + background: rgba(var(--iqser-primary-rgb), 0.1); } &:not(.overlay):hover { - background-color: $btn-bg; + background-color: var(--iqser-btn-bg); } &.primary { font-weight: 500 !important; - background-color: $primary; - color: $white; + background-color: var(--iqser-primary); + color: var(--iqser-white); &:hover { - background-color: $primary-2; + background-color: var(--iqser-primary-2); } } &.dark-bg:hover { - background-color: $btn-bg-hover; + background-color: var(--iqser-btn-bg-hover); } } .dot { - background: $primary; + background: var(--iqser-primary); height: 10px; width: 10px; border-radius: 50%; @@ -102,10 +100,10 @@ iqser-circle-button, iqser-icon-button { &[aria-expanded='true'] { button { - background: rgba($primary, 0.1); + background: rgba(var(--iqser-primary-rgb), 0.1); &.primary { - background: $primary-2; + background: var(--iqser-primary-2); } } } diff --git a/src/assets/styles/_dialogs.scss b/src/assets/styles/common-dialogs.scss similarity index 83% rename from src/assets/styles/_dialogs.scss rename to src/assets/styles/common-dialogs.scss index 5a68806..40751e1 100644 --- a/src/assets/styles/_dialogs.scss +++ b/src/assets/styles/common-dialogs.scss @@ -1,7 +1,5 @@ -@import 'apps/red-ui/src/assets/styles/variables'; - .mat-dialog-container { - color: $accent; + color: var(--iqser-accent); padding: 0 !important; border-radius: 8px !important; } @@ -27,7 +25,7 @@ .dialog-actions { height: 81px; box-sizing: border-box; - border-top: 1px solid $separator; + border-top: 1px solid var(--iqser-separator); padding: 0 32px; align-items: center; diff --git a/src/assets/styles/_full-pages.scss b/src/assets/styles/common-full-pages.scss similarity index 75% rename from src/assets/styles/_full-pages.scss rename to src/assets/styles/common-full-pages.scss index 9c9c6a4..3b37d46 100644 --- a/src/assets/styles/_full-pages.scss +++ b/src/assets/styles/common-full-pages.scss @@ -1,17 +1,12 @@ -@import 'variables'; - .full-page-section, .full-page-content { position: fixed; - top: 0; - bottom: 0; - left: 0; - right: 0; + inset: 0; } .full-page-section { opacity: 0.7; - background: $white; + background: var(--iqser-white); z-index: 900; } diff --git a/src/assets/styles/_inputs.scss b/src/assets/styles/common-inputs.scss similarity index 82% rename from src/assets/styles/_inputs.scss rename to src/assets/styles/common-inputs.scss index 02ed6c8..9725141 100644 --- a/src/assets/styles/_inputs.scss +++ b/src/assets/styles/common-inputs.scss @@ -1,5 +1,4 @@ -@import 'variables'; -@import 'mixins'; +@use 'common-mixins' as mixins; form .iqser-input-group:not(first-of-type) { margin-top: 14px; @@ -22,10 +21,10 @@ form .iqser-input-group:not(first-of-type) { position: absolute; right: 1px; bottom: 1px; - background: $quick-filter-border; + background: var(--iqser-quick-filter-border); height: 34px; width: 34px; - border-left: 1px solid $quick-filter-border; + border-left: 1px solid var(--iqser-quick-filter-border); border-top-right-radius: 7px; border-bottom-right-radius: 7px; cursor: pointer; @@ -35,13 +34,13 @@ form .iqser-input-group:not(first-of-type) { justify-content: center; &:hover { - background: $btn-bg; + background: var(--iqser-btn-bg); } mat-icon { width: 14px; height: 14px; - color: $accent; + color: var(--iqser-accent); } &.disabled { @@ -60,7 +59,7 @@ form .iqser-input-group:not(first-of-type) { .mat-form-field-label { opacity: 0.7 !important; - color: $accent !important; + color: var(--iqser-accent) !important; transform: translateY(-1.34em) !important; } @@ -83,9 +82,9 @@ form .iqser-input-group:not(first-of-type) { } .mat-button-toggle-checked { - background: $primary; + background: var(--iqser-primary); transition: background-color 0.25s ease; - color: $white; + color: var(--iqser-white); } input, @@ -94,7 +93,7 @@ form .iqser-input-group:not(first-of-type) { box-sizing: border-box; padding-left: 11px; padding-right: 11px; - border: 1px solid $quick-filter-border; + border: 1px solid var(--iqser-quick-filter-border); font-family: Inter, sans-serif; font-size: 13px; background-color: #ffffff; @@ -108,26 +107,26 @@ form .iqser-input-group:not(first-of-type) { } &:focus:not(:disabled):not(.mat-select-disabled) { - border-color: $accent; + border-color: var(--iqser-accent); } &::placeholder { - color: $accent; + color: var(--iqser-accent); opacity: 0.7; } &.ng-invalid.ng-touched { - border-color: rgba($primary, 0.3); + border-color: rgba(var(--iqser-primary-rgb), 0.3); &:focus { - border-color: $primary; + border-color: var(--iqser-primary); } } &:disabled, &.mat-select-disabled { - background-color: $filter-bg; - color: rgba($accent, 0.3); + background-color: var(--iqser-grey-2); + color: rgba(var(--iqser-accent-rgb), 0.3); } } @@ -154,7 +153,7 @@ form .iqser-input-group:not(first-of-type) { resize: vertical; padding-top: 7px; padding-bottom: 7px; - @include scroll-bar; + @include mixins.scroll-bar; &.has-scrollbar { border-top-right-radius: 0; @@ -168,7 +167,7 @@ form .iqser-input-group:not(first-of-type) { letter-spacing: 0; line-height: 14px; margin-bottom: 2px; - color: $accent; + color: var(--iqser-accent); &.mat-checkbox-layout { opacity: 1; @@ -178,7 +177,7 @@ form .iqser-input-group:not(first-of-type) { &.required label:after { content: ' *'; - color: $primary; + color: var(--iqser-primary); } &.datepicker-wrapper { @@ -196,10 +195,10 @@ form .iqser-input-group:not(first-of-type) { position: absolute; right: 0; top: 1px; - color: $accent; + color: var(--iqser-accent); &.mat-datepicker-toggle-active { - color: $primary; + color: var(--iqser-primary); } .mat-icon-button { diff --git a/src/assets/styles/_layout.scss b/src/assets/styles/common-layout.scss similarity index 100% rename from src/assets/styles/_layout.scss rename to src/assets/styles/common-layout.scss diff --git a/src/assets/styles/common-styles.scss b/src/assets/styles/common-styles.scss new file mode 100644 index 0000000..43d57f2 --- /dev/null +++ b/src/assets/styles/common-styles.scss @@ -0,0 +1,7 @@ +@use 'common-inputs'; +@use 'common-buttons'; +@use 'common-texts'; +@use 'common-tables'; +@use 'common-full-pages'; +@use 'common-layout'; +@use 'common-dialogs'; diff --git a/src/assets/styles/_tables.scss b/src/assets/styles/common-tables.scss similarity index 100% rename from src/assets/styles/_tables.scss rename to src/assets/styles/common-tables.scss diff --git a/src/assets/styles/_texts.scss b/src/assets/styles/common-texts.scss similarity index 82% rename from src/assets/styles/_texts.scss rename to src/assets/styles/common-texts.scss index 61e0cbf..bee024f 100644 --- a/src/assets/styles/_texts.scss +++ b/src/assets/styles/common-texts.scss @@ -1,5 +1,4 @@ -@import 'variables'; -@import 'mixins'; +@use 'common-mixins' as mixins; .all-caps-label { text-transform: uppercase; @@ -24,17 +23,18 @@ } &.primary { - color: $primary; + color: var(--iqser-primary); opacity: 1; } } a { - color: $primary; + color: var(--iqser-primary); transition: color 0.1s; &:hover { - color: lighten($primary, 10%); + color: var(--iqser-primary); + filter: brightness(140%); } &.with-underline { @@ -48,7 +48,7 @@ a { pre { font-family: Inter, sans-serif; - color: $accent; + color: var(--iqser-accent); } .heading-xl { @@ -102,17 +102,17 @@ pre { } .large-label { - color: $accent; + color: var(--iqser-accent); font-size: 13px; line-height: 16px; } .clamp-1 { - @include line-clamp(1); + @include mixins.line-clamp(1); } .clamp-2 { - @include line-clamp(2); + @include mixins.line-clamp(2); } .no-wrap { diff --git a/src/assets/styles/common.scss b/src/assets/styles/common.scss deleted file mode 100644 index 4e56f5a..0000000 --- a/src/assets/styles/common.scss +++ /dev/null @@ -1,7 +0,0 @@ -@import 'inputs'; -@import 'buttons'; -@import 'texts'; -@import 'tables'; -@import 'full-pages'; -@import 'layout'; -@import 'dialogs'; diff --git a/src/lib/buttons/circle-button/circle-button.component.scss b/src/lib/buttons/circle-button/circle-button.component.scss index 0e44ca4..94bf11c 100644 --- a/src/lib/buttons/circle-button/circle-button.component.scss +++ b/src/lib/buttons/circle-button/circle-button.component.scss @@ -1,5 +1,3 @@ -@import '../../../assets/styles/common'; - :host { height: var(--size); width: var(--size); @@ -26,15 +24,15 @@ } &.primary.mat-button-disabled { - background-color: $btn-bg; - color: $white !important; + background-color: var(--iqser-btn-bg); + color: var(--iqser-white) !important; } &.warn:not([disabled]) { - background-color: $warn; + background-color: var(--iqser-warn); &:hover { - background-color: $warn; + background-color: var(--iqser-warn); } } } diff --git a/src/lib/buttons/icon-button/icon-button.component.scss b/src/lib/buttons/icon-button/icon-button.component.scss index 787b413..056d65d 100644 --- a/src/lib/buttons/icon-button/icon-button.component.scss +++ b/src/lib/buttons/icon-button/icon-button.component.scss @@ -1,5 +1,3 @@ -@import '../../../assets/styles/common'; - button { padding: 0 14px; width: 100%; @@ -9,10 +7,10 @@ button { } &.show-bg { - background-color: $btn-bg; + background-color: var(--iqser-btn-bg); &:not(.mat-button-disabled):hover { - background-color: $btn-bg-hover; + background-color: var(--iqser-btn-bg-hover); } } diff --git a/src/lib/common-ui.module.ts b/src/lib/common-ui.module.ts index d255dc3..6222977 100644 --- a/src/lib/common-ui.module.ts +++ b/src/lib/common-ui.module.ts @@ -16,6 +16,7 @@ import { IqserIconsModule } from './icons'; import { IqserButtonsModule } from './buttons'; import { IqserScrollbarModule } from './scrollbar'; import { IqserEmptyStatesModule } from './empty-states'; +import { LogPipe } from './utils/pipes/log.pipe'; const matModules = [MatIconModule, MatProgressSpinnerModule]; const modules = [ @@ -33,8 +34,8 @@ const components = [StatusBarComponent, FullPageLoadingIndicatorComponent, FullP const pipes = [SortByPipe, HumanizePipe]; @NgModule({ - declarations: [...components, ...pipes], + declarations: [...components, ...pipes, LogPipe], imports: [CommonModule, ...matModules, ...modules], - exports: [...components, ...pipes, ...modules] + exports: [...components, ...pipes, ...modules, LogPipe] }) export class CommonUiModule {} diff --git a/src/lib/empty-states/empty-state/empty-state.component.scss b/src/lib/empty-states/empty-state/empty-state.component.scss index 042e4c1..09d4f96 100644 --- a/src/lib/empty-states/empty-state/empty-state.component.scss +++ b/src/lib/empty-states/empty-state/empty-state.component.scss @@ -1,5 +1,3 @@ -@import '../../../assets/styles/common'; - .empty-state { display: flex; flex-direction: column; @@ -13,7 +11,7 @@ } .heading-l { - color: $grey-7; + color: var(--iqser-disabled); } > .heading-l, diff --git a/src/lib/error/full-page-error/full-page-error.component.html b/src/lib/error/full-page-error/full-page-error.component.html index e55ee4d..1ffdcf7 100644 --- a/src/lib/error/full-page-error/full-page-error.component.html +++ b/src/lib/error/full-page-error/full-page-error.component.html @@ -1,15 +1,36 @@ -
-
- -
-
{{ error.message }}
- -
+ +
+ + +
+ + +
+
+ + +
+ +
+ + +
+ +
{{ error.message }}
+ + +
+
diff --git a/src/lib/error/full-page-error/full-page-error.component.scss b/src/lib/error/full-page-error/full-page-error.component.scss index 8791f01..d1f3986 100644 --- a/src/lib/error/full-page-error/full-page-error.component.scss +++ b/src/lib/error/full-page-error/full-page-error.component.scss @@ -1,13 +1,30 @@ -@import '../../../assets/styles/variables'; +.offline-box { + position: fixed; + bottom: 20px; + right: 20px; + height: 40px; + width: 300px; + background: var(--iqser-white); + border: 1px solid var(--iqser-separator); + border-radius: 10px; + padding: 14px; + + > mat-icon { + opacity: 0.3; + } + + > iqser-circle-button { + flex-grow: 1; + justify-content: flex-end; + } +} .full-page-section { opacity: 0.95; } .full-page-content { - display: flex; flex-direction: column; - align-items: center; text-align: center; > mat-icon { @@ -16,12 +33,12 @@ opacity: 0.1; } - .heading-l { - color: $grey-7; - font-weight: initial; - } - .error { - color: $red-1; + color: var(--iqser-primary); } } + +:is(.offline-box, .full-page-content) .heading-l { + color: #9398a0; + font-weight: initial; +} diff --git a/src/lib/error/full-page-error/full-page-error.component.ts b/src/lib/error/full-page-error/full-page-error.component.ts index dcc4fce..43862b9 100644 --- a/src/lib/error/full-page-error/full-page-error.component.ts +++ b/src/lib/error/full-page-error/full-page-error.component.ts @@ -1,11 +1,19 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; import { IconButtonTypes } from '../../buttons'; import { ErrorService } from '../error.service'; +import { animate, state, style, transition, trigger } from '@angular/animations'; @Component({ selector: 'iqser-full-page-error', templateUrl: './full-page-error.component.html', styleUrls: ['./full-page-error.component.scss'], + animations: [ + trigger('animateOpenClose', [ + state('open', style({ bottom: '20px' })), + state('void', style({ bottom: '-70px' })), + transition('* => open, open => void', animate('1s ease-in-out')) + ]) + ], changeDetection: ChangeDetectionStrategy.OnPush }) export class FullPageErrorComponent { diff --git a/src/lib/error/index.ts b/src/lib/error/index.ts index 9a01d8a..42fbcd1 100644 --- a/src/lib/error/index.ts +++ b/src/lib/error/index.ts @@ -1,2 +1,4 @@ export * from './error.service'; +export * from './max-retries.token'; +export * from './server-error-interceptor'; export * from './full-page-error/full-page-error.component'; diff --git a/src/lib/error/max-retries.token.ts b/src/lib/error/max-retries.token.ts new file mode 100644 index 0000000..b2ad58a --- /dev/null +++ b/src/lib/error/max-retries.token.ts @@ -0,0 +1,3 @@ +import { InjectionToken } from '@angular/core'; + +export const MAX_RETRIES_ON_SERVER_ERROR = new InjectionToken('Number of retries before giving up'); diff --git a/src/lib/error/server-error-interceptor.ts b/src/lib/error/server-error-interceptor.ts new file mode 100644 index 0000000..2124e5f --- /dev/null +++ b/src/lib/error/server-error-interceptor.ts @@ -0,0 +1,52 @@ +import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; +import { Inject, Injectable, Optional } from '@angular/core'; +import { MonoTypeOperatorFunction, Observable, throwError, timer } from 'rxjs'; +import { catchError, mergeMap, retryWhen, tap } from 'rxjs/operators'; +import { MAX_RETRIES_ON_SERVER_ERROR } from './max-retries.token'; +import { ErrorService } from './error.service'; + +function updateSeconds(seconds: number) { + if (seconds === 0 || seconds === 1) { + return seconds + 1; + } else { + return seconds * seconds; + } +} + +function backoffOnServerError(maxRetries = 3): MonoTypeOperatorFunction> { + let seconds = 0; + return retryWhen(attempts => + attempts.pipe( + tap(() => (seconds = updateSeconds(seconds))), + mergeMap((error: HttpErrorResponse, index) => { + if ((error.status < 500 && error.status !== 0) || index === maxRetries) { + return throwError(error); + } else { + console.error('An error occurred: ', error); + console.error(`Retrying in ${seconds} seconds...`); + return timer(seconds * 1000); + } + }) + ) + ); +} + +@Injectable() +export class ServerErrorInterceptor implements HttpInterceptor { + constructor( + private readonly _errorService: ErrorService, + @Optional() @Inject(MAX_RETRIES_ON_SERVER_ERROR) private readonly _maxRetries: number + ) {} + + intercept(req: HttpRequest, next: HttpHandler): Observable> { + return next.handle(req).pipe( + catchError((error: HttpErrorResponse) => { + if (error.status >= 500 || error.status === 0) { + this._errorService.set(error); + } + return throwError(error); + }), + backoffOnServerError(this._maxRetries || 3) + ); + } +} diff --git a/src/lib/filtering/popup-filter/popup-filter.component.scss b/src/lib/filtering/popup-filter/popup-filter.component.scss index d569c5d..01a8e56 100644 --- a/src/lib/filtering/popup-filter/popup-filter.component.scss +++ b/src/lib/filtering/popup-filter/popup-filter.component.scss @@ -1,5 +1,3 @@ -@import '../../../assets/styles/variables'; - .filter-menu-options, .filter-menu-header { display: flex; @@ -27,7 +25,7 @@ } .filter-options { - background-color: $filter-bg; + background-color: var(--iqser-grey-2); padding-bottom: 8px; } diff --git a/src/lib/filtering/quick-filters/quick-filters.component.scss b/src/lib/filtering/quick-filters/quick-filters.component.scss index 13ef48d..0974792 100644 --- a/src/lib/filtering/quick-filters/quick-filters.component.scss +++ b/src/lib/filtering/quick-filters/quick-filters.component.scss @@ -1,5 +1,3 @@ -@import '../../../assets/styles/variables'; - :host { display: flex; flex: 1; @@ -8,9 +6,9 @@ .quick-filter { box-sizing: border-box; - border: 1px solid $quick-filter-border; + border: 1px solid var(--iqser-quick-filter-border); border-radius: 17px; - background-color: $btn-bg; + background-color: var(--iqser-btn-bg); padding: 0 14px; height: 34px; display: flex; @@ -19,11 +17,11 @@ transition: background-color 0.2s; &:hover { - background-color: $white; + background-color: var(--iqser-white); } &.active { - background-color: $white; + background-color: var(--iqser-white); font-weight: 600; border: none; } diff --git a/src/lib/help-mode/help-mode/help-mode.component.scss b/src/lib/help-mode/help-mode/help-mode.component.scss index fb99479..e848a0c 100644 --- a/src/lib/help-mode/help-mode/help-mode.component.scss +++ b/src/lib/help-mode/help-mode/help-mode.component.scss @@ -1,5 +1,3 @@ -@import '../../../../../../apps/red-ui/src/assets/styles/variables'; - .help-button { width: 44px; height: 40px; @@ -7,7 +5,7 @@ bottom: 20px; right: 0; z-index: 1; - background: $green-2; + background: var(--iqser-helpmode-primary); border-top-left-radius: 8px; border-bottom-left-radius: 8px; box-shadow: -1px 1px 5px 0 rgba(40, 50, 65, 0.25); @@ -40,10 +38,10 @@ 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; + border-left: 8px solid var(--iqser-helpmode-primary); + border-right: 8px solid var(--iqser-helpmode-primary); + border-top: 8px solid var(--iqser-helpmode-primary); + border-bottom: 60px solid var(--iqser-helpmode-primary); z-index: 10; position: absolute; display: flex; diff --git a/src/lib/icons/icons.module.ts b/src/lib/icons/icons.module.ts index b5e40e8..1ad00b7 100644 --- a/src/lib/icons/icons.module.ts +++ b/src/lib/icons/icons.module.ts @@ -20,6 +20,7 @@ export class IqserIconsModule { 'help-outline', 'lanes', 'list', + 'offline', 'refresh', 'search', 'sort-asc', diff --git a/src/lib/inputs/round-checkbox/round-checkbox.component.scss b/src/lib/inputs/round-checkbox/round-checkbox.component.scss index 99c3e9b..97f77f2 100644 --- a/src/lib/inputs/round-checkbox/round-checkbox.component.scss +++ b/src/lib/inputs/round-checkbox/round-checkbox.component.scss @@ -1,5 +1,3 @@ -@import '../../../assets/styles/common'; - :host { cursor: pointer; } @@ -12,22 +10,22 @@ &.inactive { border: 1px solid #d3d5da; - background-color: $white; + background-color: var(--iqser-white); } .mat-icon { - color: $primary; + color: var(--iqser-primary); width: var(--size); height: var(--size); } &.with-bg { .mat-icon { - color: $white; + color: var(--iqser-white); } &.inactive { - border: 1px solid $btn-bg; + border: 1px solid var(--iqser-btn-bg); background-color: transparent; } } diff --git a/src/lib/listing/scroll-button/scroll-button.component.scss b/src/lib/listing/scroll-button/scroll-button.component.scss index 093aaa9..ee4a294 100644 --- a/src/lib/listing/scroll-button/scroll-button.component.scss +++ b/src/lib/listing/scroll-button/scroll-button.component.scss @@ -1,7 +1,5 @@ -@import '../../../assets/styles/common'; - .scroll-button { - background-color: $white; + background-color: var(--iqser-white); position: absolute; right: 0; height: 40px; @@ -26,5 +24,5 @@ mat-icon { width: 22px; height: 22px; - color: $grey-7; + color: var(--iqser-disabled); } diff --git a/src/lib/listing/scroll-button/scroll-button.component.ts b/src/lib/listing/scroll-button/scroll-button.component.ts index 09ad966..c6da626 100644 --- a/src/lib/listing/scroll-button/scroll-button.component.ts +++ b/src/lib/listing/scroll-button/scroll-button.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, Component, HostListener, Input, OnInit } from '@angular/core'; import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; -import { concatMap, delay, distinctUntilChanged, map, startWith } from 'rxjs/operators'; -import { Observable, of } from 'rxjs'; +import { delay, distinctUntilChanged, map, startWith } from 'rxjs/operators'; +import { combineLatest, fromEvent, Observable } from 'rxjs'; import { Required } from '../../utils'; const ButtonTypes = { @@ -34,11 +34,14 @@ export class ScrollButtonComponent implements OnInit { const showScrollUp = () => scrollIsNeeded() && !reachedEnd(ButtonTypes.top); const showScrollDown = () => scrollIsNeeded() && !reachedEnd(ButtonTypes.bottom); - const scroll$ = this.scrollViewport.elementScrolled().pipe( - startWith(''), - /** Delay first value so that we can wait for items to be rendered in viewport and get correct values */ - concatMap((value, index) => (index === 0 ? of(value).pipe(delay(0)) : of(value))) - ); + /** Force an initial emit, so combineLatest works */ + const scrolled$ = this.scrollViewport.elementScrolled().pipe(startWith(null)); + const resized$ = fromEvent(window, 'resize').pipe(startWith(null)); + const rangeChange$ = this.scrollViewport.renderedRangeStream.pipe(startWith(null)); + + /** Delay so that we can wait for items to be rendered in viewport and get correct values */ + const scroll$ = combineLatest([scrolled$, resized$, rangeChange$]).pipe(delay(0)); + this.showScrollUp$ = scroll$.pipe(map(showScrollUp), distinctUntilChanged()); this.showScrollDown$ = scroll$.pipe(map(showScrollDown), distinctUntilChanged()); } diff --git a/src/lib/listing/sync-width.directive.ts b/src/lib/listing/sync-width.directive.ts index c7cde8c..d2f4693 100644 --- a/src/lib/listing/sync-width.directive.ts +++ b/src/lib/listing/sync-width.directive.ts @@ -8,7 +8,7 @@ export class SyncWidthDirective implements OnDestroy { private readonly _interval: number; constructor(private readonly _elementRef: ElementRef) { - this._interval = setInterval(() => { + this._interval = window.setInterval(() => { this._matchWidth(); }, 1000); } diff --git a/src/lib/listing/table-column-name/table-column-name.component.scss b/src/lib/listing/table-column-name/table-column-name.component.scss index 731b910..7fb6e0d 100644 --- a/src/lib/listing/table-column-name/table-column-name.component.scss +++ b/src/lib/listing/table-column-name/table-column-name.component.scss @@ -1,5 +1,3 @@ -@import '../../../assets/styles/common'; - :host { display: flex; height: 30px; @@ -38,7 +36,7 @@ .sort-arrows-container { display: none; - color: $primary; + color: var(--iqser-primary); margin-left: 8px; mat-icon { diff --git a/src/lib/listing/table-header/table-header.component.scss b/src/lib/listing/table-header/table-header.component.scss index 106f35c..1a0c691 100644 --- a/src/lib/listing/table-header/table-header.component.scss +++ b/src/lib/listing/table-header/table-header.component.scss @@ -1,8 +1,6 @@ -@import '../../../assets/styles/common'; - .table-header { display: flex; - border-bottom: 1px solid $separator; + border-bottom: 1px solid var(--iqser-separator); &.no-data.selection-enabled:not([synced='true']) { padding-left: 30px; @@ -10,12 +8,12 @@ } .header-item { - background-color: $btn-bg; + background-color: var(--iqser-btn-bg); height: 50px; display: flex; align-items: center; z-index: 1; - border-bottom: 1px solid $separator; + border-bottom: 1px solid var(--iqser-separator); box-sizing: border-box; padding: 0 24px; diff --git a/src/lib/listing/table/table.component.scss b/src/lib/listing/table/table.component.scss index 810b74d..6486db2 100644 --- a/src/lib/listing/table/table.component.scss +++ b/src/lib/listing/table/table.component.scss @@ -1,4 +1,4 @@ -@import '../../../assets/styles/common'; +@use '../../../assets/styles/common-mixins' as mixins; :host cdk-virtual-scroll-viewport { height: calc(100vh - 50px - 31px - 111px); @@ -25,7 +25,7 @@ justify-content: center; position: relative; box-sizing: border-box; - border-bottom: 1px solid $separator; + border-bottom: 1px solid var(--iqser-separator); height: var(--itemSize); padding: 0 10px; @@ -61,8 +61,8 @@ } &.disabled > div { - background-color: $grey-2; - color: $grey-7; + background-color: var(--iqser-grey-2); + color: var(--iqser-disabled); .action-buttons { color: initial; @@ -71,7 +71,7 @@ .table-item-title { font-weight: 600; - @include line-clamp(1); + @include mixins.line-clamp(1); } .action-buttons { @@ -86,7 +86,7 @@ padding-left: 100px; padding-right: 21px; z-index: 1; - background: linear-gradient(to right, rgba(244, 245, 247, 0) 0%, $grey-2 35%); + background: linear-gradient(to right, rgba(244, 245, 247, 0) 0%, var(--iqser-grey-2) 35%); mat-icon { width: 14px; @@ -113,14 +113,14 @@ } &:hover:not(.disabled) > div { - background-color: $grey-8; + background-color: var(--iqser-not-disabled-table-item); } } } &:hover { overflow-y: auto !important; - @include scroll-bar; + @include mixins.scroll-bar; &.has-scrollbar { .table-item { diff --git a/src/lib/listing/workflow/workflow.component.scss b/src/lib/listing/workflow/workflow.component.scss index dd25803..c363708 100644 --- a/src/lib/listing/workflow/workflow.component.scss +++ b/src/lib/listing/workflow/workflow.component.scss @@ -1,4 +1,5 @@ -@import '../../../assets/styles/common'; +@import '../../../assets/styles/common-variables'; +@import '../../../assets/styles/common-mixins'; :host { display: flex; @@ -15,7 +16,7 @@ display: flex; flex-direction: column; flex: 1; - background-color: $grey-2; + background-color: var(--iqser-grey-2); border-radius: 6px; padding-top: 18px; position: relative; @@ -53,10 +54,10 @@ &:not(.list-can-receive):not(.list-dragging) { background: repeating-linear-gradient( -45deg, - $separator, - $separator 1px, - $white 1px, - $white 8px + var(--iqser-separator), + var(--iqser-separator) 1px, + var(--iqser-white) 1px, + var(--iqser-white) 8px ); > .heading, ::ng-deep.cdk-drag > * { @@ -76,7 +77,7 @@ } .cdk-drag { - background-color: $white; + background-color: var(--iqser-white); transition: background-color 0.2s, box-shadow 0.2s; border-radius: 8px; margin: 0 8px 4px 8px; @@ -86,13 +87,13 @@ } &:hover { - background-color: $grey-6; + background-color: var(--iqser-grey-6); box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.15); } } .cdk-drag-placeholder { - border: 1px dashed $grey-5; + border: 1px dashed var(--iqser-grey-5); border-radius: 8px; min-height: var(--height); margin: 0 8px 4px 8px; diff --git a/src/lib/utils/decorators/debounce.decorator.ts b/src/lib/utils/decorators/debounce.decorator.ts index 0da4c4d..8be812b 100644 --- a/src/lib/utils/decorators/debounce.decorator.ts +++ b/src/lib/utils/decorators/debounce.decorator.ts @@ -6,7 +6,7 @@ export function Debounce(delay = 300): MethodDecorator { descriptorCopy.value = function _new(...args: unknown[]) { clearTimeout(timeout); - timeout = setTimeout(() => (descriptor.value as (...params: unknown[]) => unknown).apply(this, args), delay); + timeout = window.setTimeout(() => (descriptor.value as (...params: unknown[]) => unknown).apply(this, args), delay); }; return descriptorCopy; diff --git a/src/lib/utils/decorators/on-change.decorator.ts b/src/lib/utils/decorators/on-change.decorator.ts new file mode 100644 index 0000000..b083e4d --- /dev/null +++ b/src/lib/utils/decorators/on-change.decorator.ts @@ -0,0 +1,60 @@ +import { FunctionKeys } from '../types/utility-types'; + +export interface SimpleChange { + readonly previousValue: T; + readonly currentValue: T; + readonly isFirstChange: boolean; +} + +export type CallBackFunction = (value: T, change: SimpleChange) => void; + +type TypedPropertyDecorator = (target: C, key: PropertyKey) => void; + +const CACHED_VALUE_KEY = Symbol(); +const IS_FIRST_CHANGE_KEY = Symbol(); + +interface Instance { + [CACHED_VALUE_KEY]: T; + [IS_FIRST_CHANGE_KEY]: boolean; + + [key: string]: unknown; +} + +export function OnChange(callback: CallBackFunction | string): PropertyDecorator; +// eslint-disable-next-line @typescript-eslint/ban-types +export function OnChange(callback: CallBackFunction | FunctionKeys): TypedPropertyDecorator; +// eslint-disable-next-line @typescript-eslint/ban-types +export function OnChange(callback: CallBackFunction | FunctionKeys): TypedPropertyDecorator { + return function _onChange(target: C, key: PropertyKey) { + Object.defineProperty(target, key, { + set(value: T) { + const instance = this as Instance; + const callBackFn = >(typeof callback === 'string' ? instance[callback] : callback); + if (!callBackFn) { + throw new Error(`Cannot find method ${String(callback)} in class ${target.constructor.name}`); + } + + instance[IS_FIRST_CHANGE_KEY] = instance[IS_FIRST_CHANGE_KEY] === undefined; + + // No operation if new value is same as old value + if (!instance[IS_FIRST_CHANGE_KEY] && instance[CACHED_VALUE_KEY] === value) { + return; + } + + const oldValue = instance[CACHED_VALUE_KEY]; + instance[CACHED_VALUE_KEY] = value; + + const simpleChange: SimpleChange = { + previousValue: oldValue, + currentValue: instance[CACHED_VALUE_KEY], + isFirstChange: instance[IS_FIRST_CHANGE_KEY] + }; + + callBackFn.call(instance, instance[CACHED_VALUE_KEY], simpleChange); + }, + get(): T { + return (this as Instance)[CACHED_VALUE_KEY]; + } + }); + }; +} diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index e601a91..f721eff 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -8,3 +8,4 @@ export * from './types/tooltip-positions.type'; export * from './decorators/bind.decorator'; export * from './decorators/required.decorator'; export * from './decorators/debounce.decorator'; +export * from './decorators/on-change.decorator'; diff --git a/src/lib/utils/pipes/log.pipe.ts b/src/lib/utils/pipes/log.pipe.ts new file mode 100644 index 0000000..d8dc377 --- /dev/null +++ b/src/lib/utils/pipes/log.pipe.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'log' +}) +export class LogPipe implements PipeTransform { + transform(value: T, message = ''): T { + console.log(message, value); + return value; + } +}