diff --git a/src/assets/icons/logout.svg b/src/assets/icons/logout.svg
new file mode 100755
index 0000000..b9ff468
--- /dev/null
+++ b/src/assets/icons/logout.svg
@@ -0,0 +1,14 @@
+
+
diff --git a/src/assets/icons/menu.svg b/src/assets/icons/menu.svg
new file mode 100644
index 0000000..fac3ef6
--- /dev/null
+++ b/src/assets/icons/menu.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/assets/styles/common-components.scss b/src/assets/styles/common-components.scss
new file mode 100644
index 0000000..fa36537
--- /dev/null
+++ b/src/assets/styles/common-components.scss
@@ -0,0 +1,90 @@
+.oval,
+.square {
+ font-weight: 600;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 24px;
+ width: 24px;
+ min-width: 24px;
+ font-size: 10px;
+ line-height: 12px;
+ text-align: center;
+ text-transform: uppercase;
+ border: none;
+ box-sizing: border-box;
+
+ &.large {
+ height: 32px;
+ width: 32px;
+ font-size: 13px;
+ }
+
+ &.gray-dark {
+ background-color: var(--iqser-grey-6);
+ }
+
+ &.gray-primary {
+ background-color: var(--iqser-grey-6);
+ color: var(--iqser-primary);
+ }
+
+ &.lightgray-dark {
+ background-color: var(--iqser-grey-4);
+ }
+
+ &.lightgray-primary {
+ background-color: var(--iqser-grey-4);
+ color: var(--iqser-primary);
+ }
+
+ &.darkgray-white {
+ background-color: var(--iqser-accent);
+ color: var(--iqser-white);
+ }
+
+ &.lightgray-white {
+ background-color: var(--iqser-grey-5);
+ color: var(--iqser-white);
+ }
+
+ &.primary--white {
+ background-color: var(--iqser-primary);
+ color: var(--iqser-white);
+ }
+
+ &.white-dark {
+ border: 1px solid var(--iqser-grey-4);
+ }
+
+ &.inactive {
+ background-color: var(--iqser-grey-6);
+ color: var(--iqser-grey-7);
+ }
+}
+
+.oval {
+ border-radius: 50%;
+}
+
+.stats-subtitle {
+ display: flex;
+
+ > div {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: fit-content;
+
+ mat-icon {
+ width: 10px;
+ height: 10px;
+ line-height: 13px;
+ margin-right: 6px;
+ }
+
+ &:not(:last-child) {
+ margin-right: 12px;
+ }
+ }
+}
diff --git a/src/assets/styles/common-styles.scss b/src/assets/styles/common-styles.scss
index 43d57f2..e599e86 100644
--- a/src/assets/styles/common-styles.scss
+++ b/src/assets/styles/common-styles.scss
@@ -5,3 +5,4 @@
@use 'common-full-pages';
@use 'common-layout';
@use 'common-dialogs';
+@use 'common-components';
diff --git a/src/lib/icons/icons.module.ts b/src/lib/icons/icons.module.ts
index 1ad00b7..58f472a 100644
--- a/src/lib/icons/icons.module.ts
+++ b/src/lib/icons/icons.module.ts
@@ -6,7 +6,7 @@ import { DomSanitizer } from '@angular/platform-browser';
@NgModule({
imports: [CommonModule, MatIconModule],
declarations: [],
- exports: [MatIconModule]
+ exports: [MatIconModule],
})
export class IqserIconsModule {
constructor(private readonly _iconRegistry: MatIconRegistry, private readonly _sanitizer: DomSanitizer) {
@@ -20,14 +20,20 @@ export class IqserIconsModule {
'help-outline',
'lanes',
'list',
+ 'logout',
+ 'menu',
'offline',
'refresh',
'search',
'sort-asc',
- 'sort-desc'
+ 'sort-desc',
]);
icons.forEach(icon => {
- _iconRegistry.addSvgIconInNamespace('iqser', icon, _sanitizer.bypassSecurityTrustResourceUrl(`/assets/icons/${icon}.svg`));
+ _iconRegistry.addSvgIconInNamespace(
+ 'iqser',
+ icon,
+ _sanitizer.bypassSecurityTrustResourceUrl(`/assets/icons/${icon}.svg`),
+ );
});
}
}
diff --git a/src/lib/services/composite-route.guard.ts b/src/lib/services/composite-route.guard.ts
new file mode 100644
index 0000000..57ab3c2
--- /dev/null
+++ b/src/lib/services/composite-route.guard.ts
@@ -0,0 +1,40 @@
+import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot, UrlTree } from '@angular/router';
+import { Injectable, InjectionToken, Injector } from '@angular/core';
+import { from, of } from 'rxjs';
+import { LoadingService } from '../loading';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class CompositeRouteGuard implements CanActivate {
+ constructor(protected readonly _injector: Injector, private readonly _loadingService: LoadingService) {}
+
+ async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise {
+ this._loadingService.start();
+
+ const routeGuards = []>route.data.routeGuards;
+
+ if (routeGuards) {
+ for (let i = 0; i < routeGuards.length; i++) {
+ const routeGuard = this._injector.get(routeGuards[i]);
+ let canActivateResult = routeGuard.canActivate(route, state);
+ if (canActivateResult instanceof Promise) {
+ canActivateResult = from(canActivateResult);
+ }
+ if (typeof canActivateResult === 'boolean' || canActivateResult instanceof UrlTree) {
+ canActivateResult = of(canActivateResult);
+ }
+
+ const result = await canActivateResult.toPromise();
+ if (!result) {
+ this._loadingService.stop();
+ return false;
+ }
+ }
+ }
+
+ this._loadingService.stop();
+
+ return true;
+ }
+}
diff --git a/src/lib/services/dialog.service.ts b/src/lib/services/dialog.service.ts
new file mode 100644
index 0000000..5b47fb4
--- /dev/null
+++ b/src/lib/services/dialog.service.ts
@@ -0,0 +1,56 @@
+import { Injectable } from '@angular/core';
+import { MatDialog, MatDialogConfig, MatDialogRef } from '@angular/material/dialog';
+import { ComponentType } from '@angular/cdk/portal';
+
+export const largeDialogConfig: MatDialogConfig = {
+ width: '90vw',
+ maxWidth: '90vw',
+ maxHeight: '90vh',
+ autoFocus: false,
+} as const;
+
+export const defaultDialogConfig: MatDialogConfig = {
+ width: '662px',
+ maxWidth: '90vw',
+ autoFocus: false,
+} as const;
+
+@Injectable()
+export abstract class DialogService {
+ protected readonly _config: {
+ [key in T]: {
+ component: ComponentType;
+ dialogConfig?: MatDialogConfig;
+ };
+ };
+
+ protected constructor(protected readonly _dialog: MatDialog) {}
+
+ openDialog(
+ type: T,
+ $event: MouseEvent,
+ data: unknown,
+ cb?: (...params) => unknown,
+ finallyCb?: (...params) => unknown | Promise,
+ ): MatDialogRef {
+ const config = this._config[type];
+
+ $event?.stopPropagation();
+ const ref = this._dialog.open(config.component, {
+ ...defaultDialogConfig,
+ ...(config.dialogConfig || {}),
+ data,
+ });
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
+ ref.afterClosed().subscribe(async result => {
+ if (result && cb) {
+ await cb(result);
+ }
+
+ if (finallyCb) {
+ await finallyCb(result);
+ }
+ });
+ return ref;
+ }
+}
diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts
index 6b2e67d..214c413 100644
--- a/src/lib/services/index.ts
+++ b/src/lib/services/index.ts
@@ -1,3 +1,5 @@
+export * from './dialog.service';
export * from './toaster.service';
export * from './error-message.service';
export * from './generic.service';
+export * from './composite-route.guard';
diff --git a/src/lib/utils/functions.ts b/src/lib/utils/functions.ts
index 9ed07df..ebb6d58 100644
--- a/src/lib/utils/functions.ts
+++ b/src/lib/utils/functions.ts
@@ -1,15 +1,15 @@
-import {tap} from 'rxjs/operators';
+import { tap } from 'rxjs/operators';
export function capitalize(value: string): string {
if (!value) {
- return "";
+ return '';
}
return value.charAt(0).toUpperCase() + value.slice(1);
}
export function humanize(value: string, lowercase = true): string {
if (!value) {
- return "";
+ return '';
}
const words = (lowercase ? value.toLowerCase() : value).split(/[ \-_]+/);
@@ -17,3 +17,11 @@ export function humanize(value: string, lowercase = true): string {
}
export const log = tap(console.log);
+
+export const toNumber = (str: string) => {
+ try {
+ return parseInt(`${str}`.replace(/\D/g, ''), 10);
+ } catch (e) {
+ return 0;
+ }
+};
diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts
index 9edaef4..1b27d07 100644
--- a/src/lib/utils/index.ts
+++ b/src/lib/utils/index.ts
@@ -12,3 +12,4 @@ export * from './decorators/debounce.decorator';
export * from './decorators/on-change.decorator';
export * from './http-encoder';
export * from './types/iqser-types';
+export * from './pruning-translation-loader';
diff --git a/src/lib/utils/pruning-translation-loader.ts b/src/lib/utils/pruning-translation-loader.ts
new file mode 100644
index 0000000..55a8b07
--- /dev/null
+++ b/src/lib/utils/pruning-translation-loader.ts
@@ -0,0 +1,29 @@
+import { HttpClient } from '@angular/common/http';
+import { TranslateLoader } from '@ngx-translate/core';
+import { map } from 'rxjs/operators';
+import { Observable } from 'rxjs';
+
+export class PruningTranslationLoader implements TranslateLoader {
+ constructor(private _http: HttpClient, private _prefix: string, private _suffix: string) {}
+
+ getTranslation(lang: string): Observable> {
+ return this._http
+ .get(`${this._prefix}${lang}${this._suffix}`)
+ .pipe(map((result: Record) => this._process(result)));
+ }
+
+ private _process(object: unknown): Record {
+ return (
+ Object.keys(object)
+ // eslint-disable-next-line no-prototype-builtins
+ .filter(key => object.hasOwnProperty(key) && object[key] !== '')
+ .reduce(
+ (result: Record, key) => (
+ (result[key] = typeof object[key] === 'object' ? this._process(object[key]) : object[key]),
+ result
+ ),
+ {},
+ )
+ );
+ }
+}