mirror of
https://github.com/valitydev/dashboard.git
synced 2024-11-06 02:25:23 +00:00
FR-710: Mobile sidebar menu refactoring (#549)
This commit is contained in:
parent
73d060cda9
commit
6ace562c9b
@ -9,7 +9,7 @@
|
||||
"build": "ng build --extra-webpack-config webpack.extra.js",
|
||||
"test": "ng test",
|
||||
"coverage": "npx http-server -c-1 -o -p 9875 ./coverage",
|
||||
"lint-cmd": "eslint \"src/**/*.{ts,js,html}\" --max-warnings 1765",
|
||||
"lint-cmd": "eslint \"src/**/*.{ts,js,html}\" --max-warnings 1729",
|
||||
"lint-cache-cmd": "npm run lint-cmd -- --cache",
|
||||
"lint": "npm run lint-cache-cmd",
|
||||
"lint-fix": "npm run lint-cache-cmd -- --fix",
|
||||
|
@ -6,7 +6,6 @@ import { Observable } from 'rxjs';
|
||||
import { filter, map, pluck, take } from 'rxjs/operators';
|
||||
|
||||
import { ThemeManager } from '../theme-manager';
|
||||
import { ROOT_ROUTE_PATH } from './navigation/consts';
|
||||
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
@ -26,7 +25,7 @@ export class HomeComponent implements OnInit {
|
||||
) {}
|
||||
|
||||
get hasBackground(): boolean {
|
||||
return this.router.url === ROOT_ROUTE_PATH && this.themeManager.isMainBackgroundImages;
|
||||
return this.router.url === '/' && this.themeManager.isMainBackgroundImages;
|
||||
}
|
||||
|
||||
get logoName(): string {
|
||||
|
@ -8,7 +8,6 @@ import { ConfigModule } from '../config';
|
||||
import { HomeComponent } from './home.component';
|
||||
import { LaptopGridModule } from './laptop-grid/laptop-grid.module';
|
||||
import { MobileGridModule } from './mobile-grid/mobile-grid.module';
|
||||
import { NavigationModule } from './navigation';
|
||||
import { ToolbarModule } from './toolbar';
|
||||
import { WelcomeImageModule } from './welcome-image';
|
||||
|
||||
@ -22,7 +21,6 @@ import { WelcomeImageModule } from './welcome-image';
|
||||
WelcomeImageModule,
|
||||
MobileGridModule,
|
||||
LaptopGridModule,
|
||||
NavigationModule,
|
||||
ConfigModule,
|
||||
],
|
||||
declarations: [HomeComponent],
|
||||
|
17
src/app/home/mobile-grid/_mobile-grid-theme.scss
Normal file
17
src/app/home/mobile-grid/_mobile-grid-theme.scss
Normal file
@ -0,0 +1,17 @@
|
||||
@import '@angular/material/theming';
|
||||
|
||||
@import './mobile-menu/mobile-menu-theme';
|
||||
|
||||
@mixin dsh-mobile-grid-theme($theme) {
|
||||
@include dsh-mobile-menu-theme($theme);
|
||||
|
||||
$background: map-get($theme, background);
|
||||
|
||||
.dsh-mobile-grid-drawer {
|
||||
background-color: map-get($background, card);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin dsh-mobile-grid-typography($config) {
|
||||
@include dsh-mobile-menu-typography($config);
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
@import 'node_modules/@angular/material/theming';
|
||||
@import './menu/navigation/mobile_menu_navigation_theme';
|
||||
|
||||
@mixin dsh-mobile-menu-theme($theme) {
|
||||
@include dsh-mobile-menu-navigation-theme($theme);
|
||||
|
||||
$background: map-get($theme, background);
|
||||
|
||||
.dsh-mobile-grid-drawer {
|
||||
background-color: map-get($background, card);
|
||||
}
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
import { InjectionToken } from '@angular/core';
|
||||
|
||||
import { NavigationSections } from '../navigation';
|
||||
import { PartialNavigationNode } from './types/partial-navigation-node';
|
||||
|
||||
export const ROOT_NODE_LEVEL = 0;
|
||||
|
||||
export const MOBILE_MENU_TOKEN = new InjectionToken<PartialNavigationNode[]>('mobile-menu-token');
|
||||
|
||||
export const MOBILE_MENU: PartialNavigationNode[] = [
|
||||
{
|
||||
id: NavigationSections.Main,
|
||||
},
|
||||
{
|
||||
id: 'payments_folder',
|
||||
children: [
|
||||
{
|
||||
id: NavigationSections.PaymentsAnalytics,
|
||||
icon: 'pie_chart',
|
||||
},
|
||||
{
|
||||
id: NavigationSections.PaymentsOperations,
|
||||
icon: 'table_chart',
|
||||
},
|
||||
{
|
||||
id: NavigationSections.PaymentsPayouts,
|
||||
icon: 'output',
|
||||
},
|
||||
{
|
||||
id: NavigationSections.PaymentsReports,
|
||||
icon: 'description',
|
||||
},
|
||||
{
|
||||
id: NavigationSections.PaymentsIntegrations,
|
||||
icon: 'build',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'wallets_folder',
|
||||
children: [
|
||||
{
|
||||
id: NavigationSections.WalletsWallet,
|
||||
icon: 'wallet_menu',
|
||||
},
|
||||
{
|
||||
id: NavigationSections.Wallets,
|
||||
icon: 'wallet_menu',
|
||||
},
|
||||
{
|
||||
id: NavigationSections.WalletsDeposits,
|
||||
icon: 'input',
|
||||
},
|
||||
{
|
||||
id: NavigationSections.WalletsWithdrawals,
|
||||
icon: 'output',
|
||||
},
|
||||
{
|
||||
id: NavigationSections.WalletsIntegrations,
|
||||
icon: 'build',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: NavigationSections.Claims,
|
||||
},
|
||||
];
|
2
src/app/home/mobile-grid/index.ts
Normal file
2
src/app/home/mobile-grid/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './mobile-grid.module';
|
||||
export * from './mobile-grid.component';
|
@ -1,7 +0,0 @@
|
||||
<div
|
||||
class="dsh-title dsh-menu-feedback"
|
||||
*transloco="let t; scope: 'toolbar'; read: 'toolbar.nav'"
|
||||
(click)="openFeedbackDialog()"
|
||||
>
|
||||
{{ t('feedback') }}
|
||||
</div>
|
@ -1,3 +0,0 @@
|
||||
.dsh-menu-feedback {
|
||||
cursor: pointer;
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FlexModule } from '@angular/flex-layout';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { TranslocoModule } from '@ngneat/transloco';
|
||||
|
||||
import { FeedbackDialogModule } from '@dsh/app/shared/components/dialog';
|
||||
|
||||
import { FeedbackComponent } from './feedback.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, MatIconModule, FlexModule, TranslocoModule, FeedbackDialogModule],
|
||||
declarations: [FeedbackComponent],
|
||||
exports: [FeedbackComponent],
|
||||
})
|
||||
export class FeedbackModule {}
|
@ -1,12 +0,0 @@
|
||||
<div class="mobile-menu" fxLayout="column">
|
||||
<div fxFlex fxLayout="column">
|
||||
<dsh-mobile-navigation
|
||||
[menu]="menu"
|
||||
[activeId]="activeId"
|
||||
(navigationChanged)="navigated()"
|
||||
></dsh-mobile-navigation>
|
||||
<dsh-feedback></dsh-feedback>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
<dsh-mobile-user-bar></dsh-mobile-user-bar>
|
||||
</div>
|
@ -1,13 +0,0 @@
|
||||
.mobile-menu {
|
||||
height: 100%;
|
||||
|
||||
dsh-mobile-navigation,
|
||||
dsh-mobile-user-bar {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
dsh-feedback {
|
||||
margin-top: -16px;
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
|
||||
import { NavigationFlatNode } from '../types/navigation-flat-node';
|
||||
import { MobileMenuComponent } from './mobile-menu.component';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-mobile-navigation',
|
||||
template: '',
|
||||
})
|
||||
class MockNavComponent {
|
||||
@Input() menu: NavigationFlatNode[];
|
||||
@Input() activeId: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-mobile-user-bar',
|
||||
template: '',
|
||||
})
|
||||
class MockUserBarComponent {}
|
||||
|
||||
describe('MobileMenuComponent', () => {
|
||||
let component: MobileMenuComponent;
|
||||
let fixture: ComponentFixture<MobileMenuComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [MatDividerModule],
|
||||
declarations: [MobileMenuComponent, MockNavComponent, MockUserBarComponent],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MobileMenuComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('navigated', () => {
|
||||
it('should emit navigationChanged event', () => {
|
||||
const spyOnNavigationChanged = spyOn(component.navigationChanged, 'emit');
|
||||
|
||||
component.navigated();
|
||||
|
||||
expect(spyOnNavigationChanged).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,19 +0,0 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
|
||||
import { NavigationFlatNode } from '../types/navigation-flat-node';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-mobile-menu',
|
||||
templateUrl: './mobile-menu.component.html',
|
||||
styleUrls: ['./mobile-menu.component.scss'],
|
||||
})
|
||||
export class MobileMenuComponent {
|
||||
@Input() menu: NavigationFlatNode[];
|
||||
@Input() activeId: string;
|
||||
|
||||
@Output() navigationChanged = new EventEmitter<void>();
|
||||
|
||||
navigated(): void {
|
||||
this.navigationChanged.emit();
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FlexModule } from '@angular/flex-layout';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
|
||||
import { FeedbackModule } from './feedback/feedback.module';
|
||||
import { MobileMenuComponent } from './mobile-menu.component';
|
||||
import { MobileNavigationModule } from './navigation/mobile-navigation.module';
|
||||
import { MobileUserBarModule } from './user-bar/mobile-user-bar.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, MobileNavigationModule, MatDividerModule, MobileUserBarModule, FlexModule, FeedbackModule],
|
||||
declarations: [MobileMenuComponent],
|
||||
exports: [MobileMenuComponent],
|
||||
})
|
||||
export class MobileMenuModule {}
|
@ -1,15 +0,0 @@
|
||||
@use '~@angular/material' as mat;
|
||||
@import 'node_modules/@angular/material/theming';
|
||||
|
||||
@mixin dsh-mobile-menu-navigation-theme($theme) {
|
||||
$primary: map-get($theme, primary);
|
||||
$foreground: map-get($theme, foreground);
|
||||
|
||||
.mobile-navigation-node-title {
|
||||
color: map-get($foreground, text);
|
||||
}
|
||||
|
||||
.mobile-navigation-node-wrap .mobile-navigation-node-title-active {
|
||||
color: mat.get-color-from-palette($primary, 500);
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export const CSS_UNIT_PATTERN = /([A-Za-z%]+)$/;
|
||||
export const DEFAULT_INDENT_UNITS = 'px';
|
@ -1,289 +0,0 @@
|
||||
import { CdkTreeModule, FlatTreeControl } from '@angular/cdk/tree';
|
||||
import { Component, DebugElement, Input, Type, ViewChild } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
import { isParentFlatNode } from '../../../../types/is-parent-flat-node';
|
||||
import { NavigationFlatNode } from '../../../../types/navigation-flat-node';
|
||||
import { TreeNavChildPaddingDirective } from './tree-nav-child-padding.directive';
|
||||
|
||||
abstract class BaseMockComponent {
|
||||
directive: TreeNavChildPaddingDirective;
|
||||
treeData: NavigationFlatNode[];
|
||||
|
||||
treeControl = new FlatTreeControl<NavigationFlatNode>(
|
||||
(node: NavigationFlatNode) => node.level,
|
||||
(node: NavigationFlatNode) => isParentFlatNode(node)
|
||||
);
|
||||
}
|
||||
|
||||
function configureModule(mockComponent: Type<BaseMockComponent>) {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CdkTreeModule],
|
||||
declarations: [mockComponent, TreeNavChildPaddingDirective],
|
||||
});
|
||||
}
|
||||
|
||||
describe('ChildNavPaddingDirective', () => {
|
||||
let fixture: ComponentFixture<BaseMockComponent>;
|
||||
let component: BaseMockComponent;
|
||||
|
||||
describe('creation', () => {
|
||||
@Component({
|
||||
template: `
|
||||
<cdk-tree [dataSource]="treeData" [treeControl]="treeControl">
|
||||
<cdk-tree-node
|
||||
*cdkTreeNodeDef="let node"
|
||||
dshTreeNavChildPadding="10"
|
||||
dshTreeNavChildPaddingLevelSwitch="20"
|
||||
></cdk-tree-node>
|
||||
</cdk-tree>
|
||||
`,
|
||||
})
|
||||
class MockComponent extends BaseMockComponent {
|
||||
@ViewChild(TreeNavChildPaddingDirective) directive: TreeNavChildPaddingDirective;
|
||||
@Input() treeData: NavigationFlatNode[];
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
configureModule(MockComponent);
|
||||
|
||||
fixture = TestBed.createComponent(MockComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
component.treeData = [
|
||||
{
|
||||
id: 'any',
|
||||
level: 0,
|
||||
},
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create an instance', () => {
|
||||
expect(component.directive).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ngOnInit', () => {
|
||||
@Component({
|
||||
template: `
|
||||
<cdk-tree [dataSource]="treeData" [treeControl]="treeControl">
|
||||
<cdk-tree-node
|
||||
*cdkTreeNodeDef="let node"
|
||||
dshTreeNavChildPadding="10"
|
||||
dshTreeNavChildPaddingLevelSwitch="20"
|
||||
></cdk-tree-node>
|
||||
</cdk-tree>
|
||||
`,
|
||||
})
|
||||
class MockComponent extends BaseMockComponent {
|
||||
@Input() treeData: NavigationFlatNode[];
|
||||
}
|
||||
|
||||
let nodes: DebugElement[];
|
||||
|
||||
beforeEach(() => {
|
||||
configureModule(MockComponent);
|
||||
fixture = TestBed.createComponent(MockComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.treeData = [
|
||||
{ id: 'one', level: 0 },
|
||||
{ id: 'two', level: 0 },
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should update padding values on treeData changes', () => {
|
||||
nodes = fixture.debugElement.queryAll(By.css('cdk-tree-node'));
|
||||
|
||||
expect(nodes.length).toBe(2);
|
||||
|
||||
component.treeData = [
|
||||
{ id: 'one', level: 0 },
|
||||
{ id: 'two', level: 0, isExpanded: true },
|
||||
{ id: 'two one', level: 1 },
|
||||
{ id: 'two two', level: 1 },
|
||||
{ id: 'two three', level: 1 },
|
||||
{ id: 'two four', level: 1 },
|
||||
{ id: 'three', level: 0 },
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
nodes = fixture.debugElement.queryAll(By.css('cdk-tree-node'));
|
||||
|
||||
expect(nodes.length).toBe(7);
|
||||
});
|
||||
});
|
||||
|
||||
describe('inputs', () => {
|
||||
let nodes: DebugElement[];
|
||||
|
||||
function createComponent(mockComponent: Type<BaseMockComponent>, treeData: NavigationFlatNode[]) {
|
||||
configureModule(mockComponent);
|
||||
fixture = TestBed.createComponent(mockComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.treeData = treeData;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
nodes = fixture.debugElement.queryAll(By.css('cdk-tree-node'));
|
||||
}
|
||||
|
||||
describe('dshTreeNavChildPadding', () => {
|
||||
it('should use px for default padding metric', () => {
|
||||
@Component({
|
||||
template: `
|
||||
<cdk-tree [dataSource]="treeData" [treeControl]="treeControl">
|
||||
<cdk-tree-node *cdkTreeNodeDef="let node" dshTreeNavChildPadding="10"></cdk-tree-node>
|
||||
</cdk-tree>
|
||||
`,
|
||||
})
|
||||
class MockComponent extends BaseMockComponent {
|
||||
@Input() treeData: NavigationFlatNode[];
|
||||
}
|
||||
|
||||
createComponent(MockComponent, [
|
||||
{ id: 'one', level: 0, isExpanded: true },
|
||||
{ id: 'one one', level: 1 },
|
||||
{ id: 'one four', level: 1 },
|
||||
]);
|
||||
|
||||
expect(nodes[2].nativeElement.style.paddingTop).toBe('10px');
|
||||
expect(nodes[2].nativeElement.style.paddingBottom).toBe('10px');
|
||||
});
|
||||
|
||||
it('should work with custom units for padding metric', () => {
|
||||
@Component({
|
||||
template: `
|
||||
<cdk-tree [dataSource]="treeData" [treeControl]="treeControl">
|
||||
<cdk-tree-node *cdkTreeNodeDef="let node" dshTreeNavChildPadding="1.2em"></cdk-tree-node>
|
||||
</cdk-tree>
|
||||
`,
|
||||
})
|
||||
class MockComponent extends BaseMockComponent {
|
||||
@Input() treeData: NavigationFlatNode[];
|
||||
}
|
||||
|
||||
createComponent(MockComponent, [
|
||||
{ id: 'one', level: 0, isExpanded: true },
|
||||
{ id: 'one one', level: 1 },
|
||||
{ id: 'one four', level: 1 },
|
||||
]);
|
||||
|
||||
expect(nodes[2].nativeElement.style.paddingTop).toBe('1.2em');
|
||||
expect(nodes[2].nativeElement.style.paddingBottom).toBe('1.2em');
|
||||
});
|
||||
});
|
||||
|
||||
describe('dshTreeNavChildPaddingLevelSwitch', () => {
|
||||
it('should change paddingTop for first child item using provided value', () => {
|
||||
@Component({
|
||||
template: `
|
||||
<cdk-tree [dataSource]="treeData" [treeControl]="treeControl">
|
||||
<cdk-tree-node
|
||||
*cdkTreeNodeDef="let node"
|
||||
dshTreeNavChildPadding="10"
|
||||
dshTreeNavChildPaddingLevelSwitch="20"
|
||||
></cdk-tree-node>
|
||||
</cdk-tree>
|
||||
`,
|
||||
})
|
||||
class MockComponent extends BaseMockComponent {
|
||||
@Input() treeData: NavigationFlatNode[];
|
||||
}
|
||||
|
||||
createComponent(MockComponent, [
|
||||
{ id: 'one', level: 0, isExpanded: true },
|
||||
{ id: 'one one', level: 1 },
|
||||
{ id: 'one four', level: 1 },
|
||||
]);
|
||||
|
||||
expect(nodes[1].nativeElement.style.paddingTop).toBe('20px');
|
||||
expect(nodes[1].nativeElement.style.paddingBottom).toBe('10px');
|
||||
});
|
||||
|
||||
it('should change paddingBottom for last child in the row item using provided value', () => {
|
||||
@Component({
|
||||
template: `
|
||||
<cdk-tree [dataSource]="treeData" [treeControl]="treeControl">
|
||||
<cdk-tree-node
|
||||
*cdkTreeNodeDef="let node"
|
||||
dshTreeNavChildPadding="10"
|
||||
dshTreeNavChildPaddingLevelSwitch="20"
|
||||
></cdk-tree-node>
|
||||
</cdk-tree>
|
||||
`,
|
||||
})
|
||||
class MockComponent extends BaseMockComponent {
|
||||
@Input() treeData: NavigationFlatNode[];
|
||||
}
|
||||
|
||||
createComponent(MockComponent, [
|
||||
{ id: 'one', level: 0, isExpanded: true },
|
||||
{ id: 'one one', level: 1 },
|
||||
{ id: 'one four', level: 1 },
|
||||
{ id: 'two', level: 0 },
|
||||
]);
|
||||
|
||||
expect(nodes[2].nativeElement.style.paddingTop).toBe('10px');
|
||||
expect(nodes[2].nativeElement.style.paddingBottom).toBe('20px');
|
||||
});
|
||||
|
||||
it('should not change paddingBottom for last child in the row if there is no parent after it', () => {
|
||||
@Component({
|
||||
template: `
|
||||
<cdk-tree [dataSource]="treeData" [treeControl]="treeControl">
|
||||
<cdk-tree-node
|
||||
*cdkTreeNodeDef="let node"
|
||||
dshTreeNavChildPadding="10"
|
||||
dshTreeNavChildPaddingLevelSwitch="20"
|
||||
></cdk-tree-node>
|
||||
</cdk-tree>
|
||||
`,
|
||||
})
|
||||
class MockComponent extends BaseMockComponent {
|
||||
@Input() treeData: NavigationFlatNode[];
|
||||
}
|
||||
|
||||
createComponent(MockComponent, [
|
||||
{ id: 'one', level: 0, isExpanded: true },
|
||||
{ id: 'one one', level: 1 },
|
||||
{ id: 'one four', level: 1 },
|
||||
]);
|
||||
|
||||
expect(nodes[2].nativeElement.style.paddingTop).toBe('10px');
|
||||
expect(nodes[2].nativeElement.style.paddingBottom).toBe('10px');
|
||||
});
|
||||
|
||||
it('should support non px units', () => {
|
||||
@Component({
|
||||
template: `
|
||||
<cdk-tree [dataSource]="treeData" [treeControl]="treeControl">
|
||||
<cdk-tree-node
|
||||
*cdkTreeNodeDef="let node"
|
||||
dshTreeNavChildPadding="10"
|
||||
dshTreeNavChildPaddingLevelSwitch="2em"
|
||||
></cdk-tree-node>
|
||||
</cdk-tree>
|
||||
`,
|
||||
})
|
||||
class MockComponent extends BaseMockComponent {
|
||||
@Input() treeData: NavigationFlatNode[];
|
||||
}
|
||||
|
||||
createComponent(MockComponent, [
|
||||
{ id: 'one', level: 0, isExpanded: true },
|
||||
{ id: 'one one', level: 1 },
|
||||
{ id: 'one four', level: 1 },
|
||||
]);
|
||||
|
||||
expect(nodes[1].nativeElement.style.paddingTop).toBe('2em');
|
||||
expect(nodes[1].nativeElement.style.paddingBottom).toBe('10px');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -1,94 +0,0 @@
|
||||
import { CdkTree, CdkTreeNode } from '@angular/cdk/tree';
|
||||
import { Directive, ElementRef, Input, OnInit } from '@angular/core';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import isObject from 'lodash-es/isObject';
|
||||
|
||||
import { ROOT_NODE_LEVEL } from '../../../../consts';
|
||||
import { NavigationFlatNode } from '../../../../types/navigation-flat-node';
|
||||
import { parseIndentValue } from '../../utils/parse-indent-value';
|
||||
|
||||
@UntilDestroy()
|
||||
@Directive({
|
||||
selector: '[dshTreeNavChildPadding]',
|
||||
})
|
||||
export class TreeNavChildPaddingDirective<T extends NavigationFlatNode = NavigationFlatNode, K = T> implements OnInit {
|
||||
@Input('dshTreeNavChildPadding')
|
||||
get indent(): number | string {
|
||||
return this.elPadding;
|
||||
}
|
||||
set indent(value: number | string) {
|
||||
this.setPadding(value);
|
||||
}
|
||||
|
||||
@Input('dshTreeNavChildPaddingLevelSwitch')
|
||||
get levelIndent(): number | string {
|
||||
return this.elLevelPadding;
|
||||
}
|
||||
set levelIndent(value: number | string) {
|
||||
this.setLevelPadding(value);
|
||||
}
|
||||
|
||||
private elPadding: number;
|
||||
private elPaddingUnit: string;
|
||||
private elLevelPadding: number;
|
||||
private elLevelPaddingUnit: string;
|
||||
|
||||
constructor(
|
||||
private treeNode: CdkTreeNode<T, K>,
|
||||
private tree: CdkTree<T, K>,
|
||||
private element: ElementRef<HTMLElement>
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.tree.viewChange.pipe(untilDestroyed(this)).subscribe(() => {
|
||||
this.updateElementIndents();
|
||||
});
|
||||
}
|
||||
|
||||
private updateElementIndents(): void {
|
||||
const nodeData = this.treeNode.data;
|
||||
const hasParents = nodeData.level > ROOT_NODE_LEVEL;
|
||||
|
||||
if (!hasParents) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dataSource = this.tree.dataSource as T[];
|
||||
const positionIndex = dataSource.map(({ id }: T) => id).indexOf(nodeData.id);
|
||||
|
||||
const elBefore = dataSource[positionIndex - 1];
|
||||
const elAfter = dataSource[positionIndex + 1];
|
||||
const hasParentBefore = isObject(elBefore) && elBefore.level === nodeData.level - 1;
|
||||
const hasParentAfter = isObject(elAfter) && elAfter.level === nodeData.level - 1;
|
||||
|
||||
const elementDomRef = this.element.nativeElement;
|
||||
|
||||
const childPadding = `${this.elPadding}${this.elPaddingUnit}`;
|
||||
const levelPadding = `${this.elLevelPadding * nodeData.level}${this.elLevelPaddingUnit}`;
|
||||
|
||||
elementDomRef.style.paddingTop = childPadding;
|
||||
elementDomRef.style.paddingBottom = childPadding;
|
||||
|
||||
if (hasParentBefore) {
|
||||
elementDomRef.style.paddingTop = levelPadding;
|
||||
}
|
||||
|
||||
if (hasParentAfter) {
|
||||
elementDomRef.style.paddingBottom = levelPadding;
|
||||
}
|
||||
}
|
||||
|
||||
private setPadding(value: number | string): void {
|
||||
const [indent, units] = parseIndentValue(value);
|
||||
|
||||
this.elPadding = indent;
|
||||
this.elPaddingUnit = units;
|
||||
}
|
||||
|
||||
private setLevelPadding(value: number | string): void {
|
||||
const [indent, units] = parseIndentValue(value);
|
||||
|
||||
this.elLevelPadding = indent;
|
||||
this.elLevelPaddingUnit = units;
|
||||
}
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
<cdk-tree [dataSource]="menu" [treeControl]="treeControl">
|
||||
<!-- leaf nodes -->
|
||||
<cdk-tree-node
|
||||
*cdkTreeNodeDef="let node"
|
||||
class="mobile-navigation-node"
|
||||
cdkTreeNodePadding
|
||||
cdkTreeNodePaddingIndent="4px"
|
||||
dshTreeNavChildPadding="4px"
|
||||
dshTreeNavChildPaddingLevelSwitch="12px"
|
||||
[class.mobile-navigation-node--hidden]="!shouldDisplayNode(node)"
|
||||
>
|
||||
<div class="mobile-navigation-node-wrap">
|
||||
<ng-container
|
||||
*ngTemplateOutlet="nodeName; context: { $implicit: node, isLink: hasNodeLink(node) }"
|
||||
></ng-container>
|
||||
</div>
|
||||
</cdk-tree-node>
|
||||
<!-- expandable nodes -->
|
||||
<cdk-tree-node
|
||||
*cdkTreeNodeDef="let node; when: isExpandableNode"
|
||||
class="mobile-navigation-node mobile-navigation-node-parent"
|
||||
cdkTreeNodePadding
|
||||
cdkTreeNodePaddingIndent="4px"
|
||||
[class.mobile-navigation-node--hidden]="!shouldDisplayNode(node)"
|
||||
>
|
||||
<div
|
||||
class="mobile-navigation-node-wrap"
|
||||
(click)="toggleNode(node)"
|
||||
fxLayout="row"
|
||||
fxLayoutAlign="space-between center"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="nodeName; context: { $implicit: node }"></ng-container>
|
||||
<mat-icon>
|
||||
{{ node.isExpanded ? 'expand_more' : 'chevron_right' }}
|
||||
</mat-icon>
|
||||
</div>
|
||||
</cdk-tree-node>
|
||||
</cdk-tree>
|
||||
|
||||
<ng-template #nodeName let-node let-isLink="isLink">
|
||||
<div fxLayout="row" fxLayoutGap="16px" fxLayoutAlign="center">
|
||||
<dsh-colored-icon
|
||||
fxLayout
|
||||
fxLayoutAlign="center center"
|
||||
*ngIf="hasNodeIcon(node)"
|
||||
[color]="isNodeActive(node) ? 'primary' : 'default'"
|
||||
[icon]="node.meta.icon"
|
||||
>
|
||||
</dsh-colored-icon>
|
||||
<ng-container *ngTemplateOutlet="isLink ? nodeLink : nodeTitle; context: { $implicit: node }"></ng-container>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #nodeTitle let-node>
|
||||
<ng-container *transloco="let t; scope: 'toolbar'; read: 'toolbar.nav'">
|
||||
<div
|
||||
fxFlex
|
||||
class="mobile-navigation-node-title"
|
||||
[class.mobile-navigation-node-title-active]="isNodeActive(node)"
|
||||
[class.mobile-navigation-node-title-bold]="node.level === 0"
|
||||
>
|
||||
{{ t(node.id) }}
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #nodeLink let-node>
|
||||
<ng-container *transloco="let t; scope: 'toolbar'; read: 'toolbar.nav'">
|
||||
<a
|
||||
fxFlex
|
||||
class="mobile-navigation-node-title"
|
||||
[class.mobile-navigation-node-title-active]="isNodeActive(node)"
|
||||
[class.mobile-navigation-node-title-bold]="node.level === 0"
|
||||
[routerLink]="node.meta.path"
|
||||
(click)="navigated()"
|
||||
>{{ t(node.id) }}</a
|
||||
>
|
||||
</ng-container>
|
||||
</ng-template>
|
@ -1,41 +0,0 @@
|
||||
.mobile-navigation {
|
||||
&-node {
|
||||
display: block;
|
||||
|
||||
a {
|
||||
font-family: 'Roboto', 'Helvetica Neue', sans-serif;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 28px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&-parent {
|
||||
cursor: pointer;
|
||||
|
||||
& > .mobile-navigation-node-wrap > .mat-icon {
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
&-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
|
||||
.mobile-navigation-node-title-bold {
|
||||
font-family: 'Roboto', 'Helvetica Neue', sans-serif;
|
||||
font-size: 20px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
&--hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,221 +0,0 @@
|
||||
import { CdkTreeModule } from '@angular/cdk/tree';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { FlexModule } from '@angular/flex-layout';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatIconTestingModule } from '@angular/material/icon/testing';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { getTranslocoModule } from '@dsh/app/shared/tests/get-transloco-module';
|
||||
import { ColoredIconModule } from '@dsh/components/indicators';
|
||||
|
||||
import { NavigationFlatNodeParent } from '../../types/navigation-flat-node-parent';
|
||||
import { TreeNavChildPaddingDirective } from './directives/tree-nav-child-padding/tree-nav-child-padding.directive';
|
||||
import { MobileNavigationComponent } from './mobile-navigation.component';
|
||||
|
||||
describe('MobileNavigationComponent', () => {
|
||||
let component: MobileNavigationComponent;
|
||||
let fixture: ComponentFixture<MobileNavigationComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
CdkTreeModule,
|
||||
MatIconTestingModule,
|
||||
RouterModule,
|
||||
getTranslocoModule(),
|
||||
FlexModule,
|
||||
ColoredIconModule,
|
||||
],
|
||||
declarations: [MobileNavigationComponent, TreeNavChildPaddingDirective, MatIcon],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MobileNavigationComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.menu = [];
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('ngOnChanges', () => {
|
||||
it('should open level nodes that contains active node id', () => {
|
||||
component.menu = [
|
||||
{ id: 'one', level: 0, isExpanded: false },
|
||||
{ id: 'one one', level: 1, isExpanded: false },
|
||||
{ id: 'one one one', level: 2, isExpanded: false },
|
||||
{ id: 'one one one one', level: 3 },
|
||||
{ id: 'two', level: 0, isExpanded: false },
|
||||
{ id: 'two one', level: 1, isExpanded: false },
|
||||
{ id: 'two one one', level: 2 },
|
||||
];
|
||||
component.activeId = 'one one one one';
|
||||
|
||||
fixture.detectChanges();
|
||||
component.ngOnChanges({
|
||||
menu: {
|
||||
previousValue: [],
|
||||
currentValue: component.menu,
|
||||
isFirstChange(): boolean {
|
||||
return false;
|
||||
},
|
||||
firstChange: false,
|
||||
},
|
||||
activeId: {
|
||||
previousValue: undefined,
|
||||
currentValue: component.activeId,
|
||||
isFirstChange(): boolean {
|
||||
return true;
|
||||
},
|
||||
firstChange: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(component.menu).toEqual([
|
||||
{ id: 'one', level: 0, isExpanded: true },
|
||||
{ id: 'one one', level: 1, isExpanded: true },
|
||||
{ id: 'one one one', level: 2, isExpanded: true },
|
||||
{ id: 'one one one one', level: 3 },
|
||||
{ id: 'two', level: 0, isExpanded: false },
|
||||
{ id: 'two one', level: 1, isExpanded: false },
|
||||
{ id: 'two one one', level: 2 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isExpandableNode', () => {
|
||||
it('should return true if node its own isExpanded property', () => {
|
||||
expect(component.isExpandableNode(0, { id: 'one one one', level: 2, isExpanded: true })).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if node has no isExpanded property', () => {
|
||||
expect(component.isExpandableNode(0, { id: 'one one one', level: 2 })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasNodeIcon', () => {
|
||||
it('should return true if node has icon meta info', () => {
|
||||
expect(component.hasNodeIcon({ id: 'one one one', level: 2, meta: { path: '/', icon: 'pie_chart' } })).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false if node is expandable', () => {
|
||||
expect(component.hasNodeIcon({ id: 'one one one', level: 2, isExpanded: true })).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if node has no meta information', () => {
|
||||
expect(component.hasNodeIcon({ id: 'one one one', level: 2 })).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if node has not provided icon info', () => {
|
||||
expect(component.hasNodeIcon({ id: 'one one one', level: 2, meta: { path: '/' } })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasNodeLink', () => {
|
||||
it('should return true if node has path', () => {
|
||||
expect(component.hasNodeLink({ id: 'one one one', level: 2, meta: { path: '/' } })).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if node is expandable', () => {
|
||||
expect(component.hasNodeLink({ id: 'one one one', level: 2, isExpanded: true })).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if node has no meta information', () => {
|
||||
expect(component.hasNodeLink({ id: 'one one one', level: 2 })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldDisplayNode', () => {
|
||||
beforeEach(() => {
|
||||
component.menu = [
|
||||
{ id: 'one', level: 0, isExpanded: true },
|
||||
{ id: 'one one', level: 1, isExpanded: true },
|
||||
{ id: 'one one one', level: 2, isExpanded: true },
|
||||
{ id: 'one one one one', level: 3 },
|
||||
{ id: 'two', level: 0, isExpanded: false },
|
||||
{ id: 'two one', level: 1, isExpanded: false },
|
||||
{ id: 'two one one', level: 2 },
|
||||
];
|
||||
});
|
||||
|
||||
it('should return true if parent node isExpanded', () => {
|
||||
expect(component.shouldDisplayNode(component.menu[3])).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if there is no parent of node', () => {
|
||||
expect(component.shouldDisplayNode(component.menu[4])).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if parent node is not expanded', () => {
|
||||
expect(component.shouldDisplayNode(component.menu[5])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleNode', () => {
|
||||
beforeEach(() => {
|
||||
component.menu = [
|
||||
{ id: 'one', level: 0, isExpanded: true },
|
||||
{ id: 'one one', level: 1, isExpanded: true },
|
||||
{ id: 'one one one', level: 2, isExpanded: true },
|
||||
{ id: 'one one one one', level: 3 },
|
||||
{ id: 'two', level: 0, isExpanded: false },
|
||||
{ id: 'two one', level: 1, isExpanded: false },
|
||||
{ id: 'two one one', level: 2 },
|
||||
];
|
||||
});
|
||||
|
||||
it('should switch true to false', () => {
|
||||
component.toggleNode(component.menu[2]);
|
||||
expect((component.menu[2] as NavigationFlatNodeParent).isExpanded).toBe(false);
|
||||
});
|
||||
|
||||
it('should switch false to true', () => {
|
||||
component.toggleNode(component.menu[4]);
|
||||
expect((component.menu[4] as NavigationFlatNodeParent).isExpanded).toBe(true);
|
||||
});
|
||||
|
||||
it('should not toggle node that is not a parent', () => {
|
||||
component.toggleNode(component.menu[3]);
|
||||
expect(component.menu[3]).toEqual({ id: 'one one one one', level: 3 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('isNodeActive', () => {
|
||||
beforeEach(() => {
|
||||
component.menu = [
|
||||
{ id: 'one', level: 0, isExpanded: true },
|
||||
{ id: 'one one', level: 1, isExpanded: true },
|
||||
{ id: 'one one one', level: 2, isExpanded: true },
|
||||
{ id: 'one one one one', level: 3 },
|
||||
{ id: 'two', level: 0, isExpanded: false },
|
||||
{ id: 'two one', level: 1, isExpanded: false },
|
||||
{ id: 'two one one', level: 2 },
|
||||
];
|
||||
component.activeId = 'two one one';
|
||||
});
|
||||
|
||||
it('should return true if active id equals to node id', () => {
|
||||
expect(component.isNodeActive(component.menu[6])).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if active id does not match node id', () => {
|
||||
expect(component.isNodeActive(component.menu[3])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('navigated', () => {
|
||||
it('should emit navigationChanged event', () => {
|
||||
const spyOnNavigationChanged = spyOn(component.navigationChanged, 'emit');
|
||||
|
||||
component.navigated();
|
||||
|
||||
expect(spyOnNavigationChanged).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,112 +0,0 @@
|
||||
import { FlatTreeControl } from '@angular/cdk/tree';
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
|
||||
import isNil from 'lodash-es/isNil';
|
||||
import isObject from 'lodash-es/isObject';
|
||||
|
||||
import { ComponentChanges } from '@dsh/type-utils';
|
||||
|
||||
import { isParentFlatNode } from '../../types/is-parent-flat-node';
|
||||
import { NavigationFlatNode } from '../../types/navigation-flat-node';
|
||||
import { NavigationFlatNodeParent } from '../../types/navigation-flat-node-parent';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-mobile-navigation',
|
||||
templateUrl: './mobile-navigation.component.html',
|
||||
styleUrls: ['./mobile-navigation.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MobileNavigationComponent implements OnChanges {
|
||||
@Input() menu: NavigationFlatNode[];
|
||||
@Input() activeId: string;
|
||||
|
||||
@Output() navigationChanged = new EventEmitter<void>();
|
||||
|
||||
treeControl = new FlatTreeControl<NavigationFlatNode>(
|
||||
(node: NavigationFlatNode) => node.level,
|
||||
(node: NavigationFlatNode) => isParentFlatNode(node)
|
||||
);
|
||||
|
||||
ngOnChanges(changes: ComponentChanges<MobileNavigationComponent>): void {
|
||||
if (isObject(changes.activeId) || isObject(changes.menu)) {
|
||||
this.openActiveNodeParents();
|
||||
}
|
||||
}
|
||||
|
||||
isExpandableNode(_: number, node: NavigationFlatNode): boolean {
|
||||
return isParentFlatNode(node);
|
||||
}
|
||||
|
||||
hasNodeIcon(node: NavigationFlatNode): boolean {
|
||||
if (isParentFlatNode(node)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Boolean(node?.meta?.icon);
|
||||
}
|
||||
|
||||
hasNodeLink(node: NavigationFlatNode): boolean {
|
||||
return isParentFlatNode(node) ? false : Boolean(node?.meta?.path);
|
||||
}
|
||||
|
||||
shouldDisplayNode(node: NavigationFlatNode): boolean {
|
||||
let parent = this.getParentNode(node);
|
||||
while (parent) {
|
||||
if (!parent.isExpanded) {
|
||||
return false;
|
||||
}
|
||||
parent = this.getParentNode(parent);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
toggleNode(node: NavigationFlatNode): void {
|
||||
if (isParentFlatNode(node)) {
|
||||
node.isExpanded = !node.isExpanded;
|
||||
}
|
||||
}
|
||||
|
||||
navigated(): void {
|
||||
this.navigationChanged.emit();
|
||||
}
|
||||
|
||||
isNodeActive(node: NavigationFlatNode): boolean {
|
||||
return node.id === this.activeId;
|
||||
}
|
||||
|
||||
private getParentNode(node: NavigationFlatNode): NavigationFlatNodeParent | null {
|
||||
const nodeIndex = this.menu.indexOf(node);
|
||||
|
||||
for (let i = nodeIndex - 1; i >= 0; i -= 1) {
|
||||
if (this.menu[i].level === node.level - 1) {
|
||||
return this.menu[i] as NavigationFlatNodeParent;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private openActiveNodeParents(): void {
|
||||
if (isNil(this.menu) || isNil(this.activeId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeNode = this.menu.find(({ id }: NavigationFlatNode) => id === this.activeId);
|
||||
const parents = [];
|
||||
let parentNode = this.getParentNode(activeNode);
|
||||
|
||||
while (!isNil(parentNode)) {
|
||||
parents.push(parentNode);
|
||||
parentNode = this.getParentNode(parentNode);
|
||||
}
|
||||
|
||||
this.menu.forEach((menuNode: NavigationFlatNode) => {
|
||||
if (isParentFlatNode(menuNode)) {
|
||||
menuNode.isExpanded = false;
|
||||
}
|
||||
});
|
||||
|
||||
parents.forEach((node: NavigationFlatNodeParent) => {
|
||||
node.isExpanded = true;
|
||||
});
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import { CdkTreeModule } from '@angular/cdk/tree';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FlexModule } from '@angular/flex-layout';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { TranslocoModule } from '@ngneat/transloco';
|
||||
|
||||
import { ColoredIconModule } from '@dsh/components/indicators';
|
||||
|
||||
import { TreeNavChildPaddingDirective } from './directives/tree-nav-child-padding/tree-nav-child-padding.directive';
|
||||
import { MobileNavigationComponent } from './mobile-navigation.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, CdkTreeModule, MatIconModule, RouterModule, TranslocoModule, FlexModule, ColoredIconModule],
|
||||
declarations: [MobileNavigationComponent, TreeNavChildPaddingDirective],
|
||||
exports: [MobileNavigationComponent],
|
||||
})
|
||||
export class MobileNavigationModule {}
|
@ -1,29 +0,0 @@
|
||||
import { parseIndentValue } from './parse-indent-value';
|
||||
|
||||
const ERROR_MESSAGE = `Сan't parse non numeric indent`;
|
||||
|
||||
describe('parseIndentValue', () => {
|
||||
it('should return value as is and px units if value is number', () => {
|
||||
expect(parseIndentValue(1)).toEqual([1, 'px']);
|
||||
});
|
||||
|
||||
it('should return value as a number and px units if value is numeric string', () => {
|
||||
expect(parseIndentValue('1')).toEqual([1, 'px']);
|
||||
});
|
||||
|
||||
it('should parse custom units from string value', () => {
|
||||
expect(parseIndentValue('1em')).toEqual([1, 'em']);
|
||||
});
|
||||
|
||||
it('should throw an error if number part of string is not parsable', () => {
|
||||
expect(() => {
|
||||
parseIndentValue('-)1em');
|
||||
}).toThrowError(ERROR_MESSAGE);
|
||||
});
|
||||
|
||||
it('should throw an error string is not parsable in valid number', () => {
|
||||
expect(() => {
|
||||
parseIndentValue('-)1');
|
||||
}).toThrowError(ERROR_MESSAGE);
|
||||
});
|
||||
});
|
@ -1,18 +0,0 @@
|
||||
import { CSS_UNIT_PATTERN, DEFAULT_INDENT_UNITS } from '../consts';
|
||||
|
||||
export const parseIndentValue = (value: number | string): [number, string] => {
|
||||
let indent: number = Number(value);
|
||||
let units = DEFAULT_INDENT_UNITS;
|
||||
|
||||
if (typeof value === 'string') {
|
||||
const parts = value.split(CSS_UNIT_PATTERN);
|
||||
indent = Number(parts[0]);
|
||||
units = parts[1] || units;
|
||||
}
|
||||
|
||||
if (isNaN(indent)) {
|
||||
throw new Error(`Сan't parse non numeric indent`);
|
||||
}
|
||||
|
||||
return [indent, units];
|
||||
};
|
@ -1,4 +0,0 @@
|
||||
<div class="mobile-user-bar" fxLayout="row" fxLayoutAlign="space-between center">
|
||||
<div class="mobile-user-bar-username">{{ userName }}</div>
|
||||
<mat-icon class="mobile-user-bar-exit" svgIcon="output" (click)="logout()"></mat-icon>
|
||||
</div>
|
@ -1,17 +0,0 @@
|
||||
.mobile-user-bar {
|
||||
&-username {
|
||||
font-family: 'Roboto', 'Helvetica Neue', sans-serif;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
&-exit {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatIconTestingModule } from '@angular/material/icon/testing';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
import { instance, mock, verify, when } from 'ts-mockito';
|
||||
|
||||
import { MobileUserBarComponent } from './mobile-user-bar.component';
|
||||
|
||||
const MOCK_USERNAME = 'TestUserName@user.user';
|
||||
|
||||
describe('MobileUserBarComponent', () => {
|
||||
let component: MobileUserBarComponent;
|
||||
let fixture: ComponentFixture<MobileUserBarComponent>;
|
||||
let mockKeycloakService: KeycloakService;
|
||||
|
||||
beforeEach(() => {
|
||||
mockKeycloakService = mock(KeycloakService);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
when(mockKeycloakService.getUsername()).thenReturn(MOCK_USERNAME);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [MatIconTestingModule],
|
||||
declarations: [MobileUserBarComponent, MatIcon],
|
||||
providers: [
|
||||
{
|
||||
provide: KeycloakService,
|
||||
useFactory: () => instance(mockKeycloakService),
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MobileUserBarComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('creation', () => {
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should update set userName property using keycloak data', () => {
|
||||
expect(component.userName).toBe(MOCK_USERNAME);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logout', () => {
|
||||
it('should call keycloak logout method', () => {
|
||||
component.logout();
|
||||
|
||||
verify(mockKeycloakService.logout()).once();
|
||||
expect().nothing();
|
||||
});
|
||||
});
|
||||
});
|
@ -1,13 +0,0 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FlexModule } from '@angular/flex-layout';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
|
||||
import { MobileUserBarComponent } from './mobile-user-bar.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, MatIconModule, FlexModule],
|
||||
declarations: [MobileUserBarComponent],
|
||||
exports: [MobileUserBarComponent],
|
||||
})
|
||||
export class MobileUserBarModule {}
|
@ -4,12 +4,7 @@
|
||||
<div class="dsh-mobile-grid-drawer-actions" fxLayout="row" fxLayoutAlign="end center">
|
||||
<mat-icon class="dsh-mobile-grid-toggle-button" (click)="closeSideNav()" svgIcon="cross"></mat-icon>
|
||||
</div>
|
||||
<dsh-mobile-menu
|
||||
fxFlex
|
||||
[menu]="menu$ | async"
|
||||
[activeId]="activeMenuItemId$ | async"
|
||||
(navigationChanged)="closeSideNav()"
|
||||
></dsh-mobile-menu>
|
||||
<dsh-mobile-menu fxFlex (menuItemSelected)="closeSideNav()"></dsh-mobile-menu>
|
||||
</div>
|
||||
</mat-drawer>
|
||||
<mat-drawer-content>
|
||||
|
@ -1,299 +0,0 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatIconTestingModule } from '@angular/material/icon/testing';
|
||||
import { MatDrawer, MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { cold } from 'jasmine-marbles';
|
||||
import { of } from 'rxjs';
|
||||
import { instance, mock, verify, when } from 'ts-mockito';
|
||||
|
||||
import { NavigationService } from '../navigation';
|
||||
import { MOBILE_MENU_TOKEN } from './consts';
|
||||
import { MobileGridComponent } from './mobile-grid.component';
|
||||
import { NavigationFlatNode } from './types/navigation-flat-node';
|
||||
import { PartialNavigationNode } from './types/partial-navigation-node';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-mobile-menu',
|
||||
template: '',
|
||||
})
|
||||
class MockMobileMenuComponent {
|
||||
@Input() menu: NavigationFlatNode[];
|
||||
@Input() activeId: string;
|
||||
|
||||
@Output() navigationChanged = new EventEmitter<void>();
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-brand',
|
||||
template: '',
|
||||
})
|
||||
class MockBrandComponent {
|
||||
@Input() inverted: boolean;
|
||||
@Input() navigationLink = '/';
|
||||
}
|
||||
|
||||
describe('MobileGridComponent', () => {
|
||||
let component: MobileGridComponent;
|
||||
let fixture: ComponentFixture<MobileGridComponent>;
|
||||
let mockNavigationService: NavigationService;
|
||||
let mockMatDrawer: MatDrawer;
|
||||
|
||||
beforeEach(() => {
|
||||
mockNavigationService = mock(NavigationService);
|
||||
mockMatDrawer = mock(MatDrawer);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
when(mockNavigationService.availableLinks$).thenReturn(of([]));
|
||||
when(mockNavigationService.activeLink$).thenReturn(of());
|
||||
});
|
||||
|
||||
async function createComponent(menu: PartialNavigationNode[] = []) {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [NoopAnimationsModule, MatSidenavModule, FlexLayoutModule, MatIconTestingModule],
|
||||
declarations: [MobileGridComponent, MockMobileMenuComponent, MatIcon, MockBrandComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: NavigationService,
|
||||
useFactory: () => instance(mockNavigationService),
|
||||
},
|
||||
{
|
||||
provide: MOBILE_MENU_TOKEN,
|
||||
useValue: menu,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(MobileGridComponent);
|
||||
component = fixture.componentInstance;
|
||||
}
|
||||
|
||||
it('should create', async () => {
|
||||
await createComponent();
|
||||
fixture.detectChanges();
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('menuIcon', () => {
|
||||
beforeEach(async () => {
|
||||
await createComponent();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should return "menu_inverted" if logo was inverted', () => {
|
||||
component.inverted = true;
|
||||
expect(component.menuIcon).toBe('menu_inverted');
|
||||
});
|
||||
|
||||
it('should return "menu" if logo was not inverted', () => {
|
||||
component.inverted = false;
|
||||
expect(component.menuIcon).toBe('menu');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ngOnInit', () => {
|
||||
it('should flat provided mobile menu and map it on availableLinks', async () => {
|
||||
when(mockNavigationService.availableLinks$).thenReturn(
|
||||
of([
|
||||
{
|
||||
id: 'one',
|
||||
path: '/one',
|
||||
navPlace: {
|
||||
page: 'one',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'two one',
|
||||
path: '/two_one',
|
||||
navPlace: {
|
||||
page: 'two_one',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'two two',
|
||||
path: '/two_two',
|
||||
navPlace: {
|
||||
page: 'two_two',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'three',
|
||||
path: '/three',
|
||||
navPlace: {
|
||||
page: 'three',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'four',
|
||||
path: '/four',
|
||||
navPlace: {
|
||||
page: 'four',
|
||||
},
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
await createComponent([
|
||||
{ id: 'one' },
|
||||
{
|
||||
id: 'two',
|
||||
children: [{ id: 'two one' }, { id: 'two two' }],
|
||||
},
|
||||
{ id: 'three', icon: 'description' },
|
||||
{ id: 'four' },
|
||||
]);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.menu$).toBeObservable(
|
||||
cold('(a|)', {
|
||||
a: [
|
||||
{
|
||||
id: 'one',
|
||||
level: 0,
|
||||
meta: {
|
||||
path: '/one',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'two',
|
||||
level: 0,
|
||||
isExpanded: false,
|
||||
},
|
||||
{
|
||||
id: 'two one',
|
||||
level: 1,
|
||||
meta: {
|
||||
path: '/two_one',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'two two',
|
||||
level: 1,
|
||||
meta: {
|
||||
path: '/two_two',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'three',
|
||||
level: 0,
|
||||
meta: {
|
||||
path: '/three',
|
||||
icon: 'description',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'four',
|
||||
level: 0,
|
||||
meta: {
|
||||
path: '/four',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove parent element from list if children is not in available links list', async () => {
|
||||
when(mockNavigationService.availableLinks$).thenReturn(
|
||||
of([
|
||||
{
|
||||
id: 'one',
|
||||
path: '/one',
|
||||
navPlace: {
|
||||
page: 'one',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'three',
|
||||
path: '/three',
|
||||
navPlace: {
|
||||
page: 'three',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'four',
|
||||
path: '/four',
|
||||
navPlace: {
|
||||
page: 'four',
|
||||
},
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
await createComponent([
|
||||
{ id: 'one' },
|
||||
{
|
||||
id: 'two',
|
||||
children: [{ id: 'two one' }, { id: 'two two' }],
|
||||
},
|
||||
{ id: 'three', icon: 'description' },
|
||||
{ id: 'four' },
|
||||
]);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.menu$).toBeObservable(
|
||||
cold('(a|)', {
|
||||
a: [
|
||||
{
|
||||
id: 'one',
|
||||
level: 0,
|
||||
meta: {
|
||||
path: '/one',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'three',
|
||||
level: 0,
|
||||
meta: {
|
||||
path: '/three',
|
||||
icon: 'description',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'four',
|
||||
level: 0,
|
||||
meta: {
|
||||
path: '/four',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('openSideNav', () => {
|
||||
beforeEach(async () => {
|
||||
await createComponent();
|
||||
fixture.detectChanges();
|
||||
|
||||
component.drawer = instance(mockMatDrawer);
|
||||
});
|
||||
|
||||
it('should call drawer open method', () => {
|
||||
component.openSideNav();
|
||||
|
||||
verify(mockMatDrawer.open('program')).once();
|
||||
expect().nothing();
|
||||
});
|
||||
});
|
||||
|
||||
describe('closeSideNav', () => {
|
||||
beforeEach(async () => {
|
||||
await createComponent();
|
||||
fixture.detectChanges();
|
||||
|
||||
component.drawer = instance(mockMatDrawer);
|
||||
});
|
||||
|
||||
it('should call drawer close method', () => {
|
||||
component.closeSideNav();
|
||||
|
||||
verify(mockMatDrawer.close()).once();
|
||||
expect().nothing();
|
||||
});
|
||||
});
|
||||
});
|
@ -1,25 +1,14 @@
|
||||
import { Component, Inject, Input, OnInit, ViewChild } from '@angular/core';
|
||||
import { MatDrawer } from '@angular/material/sidenav';
|
||||
import isNil from 'lodash-es/isNil';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { Component, Input, ViewChild } from '@angular/core';
|
||||
import { MatDrawer, MatDrawerToggleResult } from '@angular/material/sidenav';
|
||||
|
||||
import { coerceBoolean } from '@dsh/utils';
|
||||
|
||||
import { NavigationLink, NavigationService } from '../navigation';
|
||||
import { MOBILE_MENU_TOKEN } from './consts';
|
||||
import { isParentFlatNode } from './types/is-parent-flat-node';
|
||||
import { NavigationFlatNode } from './types/navigation-flat-node';
|
||||
import { PartialNavigationFlatNode } from './types/partial-navigation-flat-node';
|
||||
import { PartialNavigationNode } from './types/partial-navigation-node';
|
||||
import { getFlattenMobileMenu } from './utils/get-flatten-mobile-menu';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-mobile-grid',
|
||||
templateUrl: './mobile-grid.component.html',
|
||||
styleUrls: ['./mobile-grid.component.scss'],
|
||||
})
|
||||
export class MobileGridComponent implements OnInit {
|
||||
export class MobileGridComponent {
|
||||
@Input()
|
||||
@coerceBoolean
|
||||
inverted: boolean;
|
||||
@ -28,67 +17,15 @@ export class MobileGridComponent implements OnInit {
|
||||
|
||||
@ViewChild(MatDrawer) drawer: MatDrawer;
|
||||
|
||||
menu$: Observable<NavigationFlatNode[]>;
|
||||
activeMenuItemId$: Observable<NavigationFlatNode['id']>;
|
||||
|
||||
private mobileMenuFormat: PartialNavigationFlatNode[];
|
||||
|
||||
constructor(
|
||||
private navigationService: NavigationService,
|
||||
@Inject(MOBILE_MENU_TOKEN)
|
||||
private mobileMenu: PartialNavigationNode[]
|
||||
) {}
|
||||
|
||||
get menuIcon(): string {
|
||||
return this.inverted ? 'menu_inverted' : 'menu';
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.mobileMenuFormat = getFlattenMobileMenu(this.mobileMenu);
|
||||
this.activeMenuItemId$ = this.navigationService.activeLink$.pipe(map((link: NavigationLink) => link.id));
|
||||
this.menu$ = this.navigationService.availableLinks$.pipe(
|
||||
map((links: NavigationLink[]) => {
|
||||
return links.reduce((linksMap: Map<string, NavigationLink>, link: NavigationLink) => {
|
||||
linksMap.set(link.id, link);
|
||||
return linksMap;
|
||||
}, new Map());
|
||||
}),
|
||||
map((linksMap: Map<string, NavigationLink>) => {
|
||||
const flatMenuNodes = this.mobileMenuFormat
|
||||
.filter(
|
||||
(menuNode: PartialNavigationFlatNode) => linksMap.has(menuNode.id) || isParentFlatNode(menuNode)
|
||||
)
|
||||
.map((menuNode: PartialNavigationFlatNode) => {
|
||||
if (isParentFlatNode(menuNode)) {
|
||||
return menuNode;
|
||||
}
|
||||
|
||||
const { path } = linksMap.get(menuNode.id);
|
||||
const { meta = {} } = menuNode;
|
||||
return {
|
||||
...menuNode,
|
||||
meta: {
|
||||
...meta,
|
||||
path,
|
||||
},
|
||||
};
|
||||
});
|
||||
return flatMenuNodes.filter((menuNode: NavigationFlatNode, index: number) => {
|
||||
if (isParentFlatNode(menuNode)) {
|
||||
const nextNode = flatMenuNodes[index + 1];
|
||||
return !(isNil(nextNode) || nextNode.level <= menuNode.level);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
})
|
||||
);
|
||||
openSideNav(): Promise<MatDrawerToggleResult> {
|
||||
return this.drawer.open('program');
|
||||
}
|
||||
|
||||
openSideNav(): void {
|
||||
this.drawer.open('program');
|
||||
}
|
||||
|
||||
closeSideNav(): void {
|
||||
this.drawer.close();
|
||||
closeSideNav(): Promise<MatDrawerToggleResult> {
|
||||
return this.drawer.close();
|
||||
}
|
||||
}
|
||||
|
@ -3,22 +3,14 @@ import { NgModule } from '@angular/core';
|
||||
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import cloneDeep from 'lodash-es/cloneDeep';
|
||||
|
||||
import { BrandModule } from '../brand';
|
||||
import { MOBILE_MENU, MOBILE_MENU_TOKEN } from './consts';
|
||||
import { MobileMenuModule } from './menu/mobile-menu.module';
|
||||
import { MobileGridComponent } from './mobile-grid.component';
|
||||
import { MobileMenuModule } from './mobile-menu/mobile-menu.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, MatSidenavModule, MatIconModule, BrandModule, FlexLayoutModule, MobileMenuModule],
|
||||
declarations: [MobileGridComponent],
|
||||
exports: [MobileGridComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: MOBILE_MENU_TOKEN,
|
||||
useValue: cloneDeep(MOBILE_MENU),
|
||||
},
|
||||
],
|
||||
})
|
||||
export class MobileGridModule {}
|
||||
|
13
src/app/home/mobile-grid/mobile-menu/_mobile-menu-theme.scss
Normal file
13
src/app/home/mobile-grid/mobile-menu/_mobile-menu-theme.scss
Normal file
@ -0,0 +1,13 @@
|
||||
@import '@angular/material/theming';
|
||||
|
||||
@import './components/mobile-menu-nav-item/mobile-menu-nav-item-theme';
|
||||
@import './components/mobile-user-bar/mobile-user-bar-theme';
|
||||
|
||||
@mixin dsh-mobile-menu-theme($theme) {
|
||||
@include dsh-mobile-nav-item-theme($theme);
|
||||
}
|
||||
|
||||
@mixin dsh-mobile-menu-typography($config) {
|
||||
@include dsh-mobile-menu-nav-item-typography($config);
|
||||
@include dsh-mobile-user-bar-typography($config);
|
||||
}
|
3
src/app/home/mobile-grid/mobile-menu/components/index.ts
Normal file
3
src/app/home/mobile-grid/mobile-menu/components/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './mobile-menu-nav-item';
|
||||
export * from './mobile-menu-feedback-item';
|
||||
export * from './mobile-user-bar';
|
@ -0,0 +1 @@
|
||||
export * from './mobile-menu-feedback-item.component';
|
@ -0,0 +1,3 @@
|
||||
<dsh-mobile-menu-nav-item *transloco="let t; scope: 'toolbar'; read: 'toolbar.nav'" (click)="openFeedbackDialog()">{{
|
||||
t('feedback')
|
||||
}}</dsh-mobile-menu-nav-item>
|
@ -1,14 +1,14 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
|
||||
|
||||
import { FeedbackDialogComponent } from '@dsh/app/shared/components/dialog';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-feedback',
|
||||
templateUrl: './feedback.component.html',
|
||||
styleUrls: ['./feedback.component.scss'],
|
||||
selector: 'dsh-mobile-menu-feedback-item',
|
||||
templateUrl: 'mobile-menu-feedback-item.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FeedbackComponent {
|
||||
export class MobileMenuFeedbackItemComponent {
|
||||
constructor(private dialog: MatDialog) {}
|
||||
|
||||
openFeedbackDialog(): MatDialogRef<FeedbackDialogComponent> {
|
@ -0,0 +1,15 @@
|
||||
@use '~@angular/material' as mat;
|
||||
|
||||
@mixin dsh-mobile-nav-item-theme($theme) {
|
||||
$primary: map-get($theme, primary);
|
||||
|
||||
.dsh-mobile-nav-item-active {
|
||||
color: mat.get-color-from-palette($primary, default);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin dsh-mobile-menu-nav-item-typography($config) {
|
||||
.dsh-mobile-menu-nav-item {
|
||||
@include mat.typography-level($config, title);
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export * from './mobile-menu-nav-item.component';
|
@ -0,0 +1,3 @@
|
||||
<div class="dsh-mobile-menu-nav-item" [ngClass]="{ 'dsh-mobile-nav-item-active': active }">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
@ -0,0 +1,7 @@
|
||||
:host {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.dsh-mobile-menu-nav-item {
|
||||
cursor: pointer;
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-mobile-menu-nav-item',
|
||||
templateUrl: 'mobile-menu-nav-item.component.html',
|
||||
styleUrls: ['mobile-menu-nav-item.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class NavItemComponent {
|
||||
@Input() active = false;
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
@use '~@angular/material' as mat;
|
||||
|
||||
@mixin dsh-mobile-user-bar-typography($config) {
|
||||
.dsh-mobile-user-bar-username {
|
||||
@include mat.typography-level($config, body-2);
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export * from './mobile-user-bar.component';
|
@ -0,0 +1,4 @@
|
||||
<div class="dsh-mobile-user-bar" fxLayout="row" fxLayoutAlign="space-between center">
|
||||
<div class="dsh-mobile-user-bar-username">{{ userName }}</div>
|
||||
<mat-icon class="dsh-mobile-user-bar-exit" svgIcon="output" (click)="logout()"></mat-icon>
|
||||
</div>
|
@ -0,0 +1,13 @@
|
||||
$dsh-mobile-user-bar-padding: 24px 16px;
|
||||
|
||||
.dsh-mobile-user-bar {
|
||||
padding: $dsh-mobile-user-bar-padding;
|
||||
|
||||
&-exit {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@ export class MobileUserBarComponent {
|
||||
|
||||
constructor(private keycloakService: KeycloakService) {}
|
||||
|
||||
logout(): void {
|
||||
this.keycloakService.logout();
|
||||
logout(): Promise<void> {
|
||||
return this.keycloakService.logout();
|
||||
}
|
||||
}
|
2
src/app/home/mobile-grid/mobile-menu/index.ts
Normal file
2
src/app/home/mobile-grid/mobile-menu/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './mobile-menu.module';
|
||||
export * from './mobile-menu.component';
|
@ -0,0 +1,17 @@
|
||||
<div class="dsh-mobile-menu" fxLayout="column">
|
||||
<div class="dsh-enu-nav-items" fxFlex fxLayout="column" fxLayoutGap="16px">
|
||||
<dsh-mobile-menu-nav-item
|
||||
*ngFor="let link of sectionLinks$ | async"
|
||||
[routerLink]="link.path"
|
||||
routerLinkActive
|
||||
[routerLinkActiveOptions]="{ exact: link?.exact }"
|
||||
#rla="routerLinkActive"
|
||||
[active]="rla.isActive"
|
||||
(click)="menuItemSelected.emit()"
|
||||
>{{ link.label }}</dsh-mobile-menu-nav-item
|
||||
>
|
||||
<dsh-mobile-menu-feedback-item></dsh-mobile-menu-feedback-item>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
<dsh-mobile-user-bar></dsh-mobile-user-bar>
|
||||
</div>
|
@ -0,0 +1,9 @@
|
||||
$dsh-enu-nav-items-padding: 0 16px;
|
||||
|
||||
.dsh-mobile-menu {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dsh-enu-nav-items {
|
||||
padding: $dsh-enu-nav-items-padding;
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
import { Component, EventEmitter, Output } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { SectionsLinksService, SectionLink } from '@dsh/app/shared/services/sections-links';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-mobile-menu',
|
||||
templateUrl: './mobile-menu.component.html',
|
||||
styleUrls: ['./mobile-menu.component.scss'],
|
||||
})
|
||||
export class MobileMenuComponent {
|
||||
@Output() menuItemSelected: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
sectionLinks$: Observable<SectionLink[]> = this.sectionsLinksService.sectionLinks$;
|
||||
|
||||
constructor(private sectionsLinksService: SectionsLinksService) {}
|
||||
}
|
27
src/app/home/mobile-grid/mobile-menu/mobile-menu.module.ts
Normal file
27
src/app/home/mobile-grid/mobile-menu/mobile-menu.module.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FlexModule } from '@angular/flex-layout';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { TranslocoModule } from '@ngneat/transloco';
|
||||
|
||||
import { SectionsLinksModule } from '@dsh/app/shared/services/sections-links';
|
||||
|
||||
import { MobileMenuFeedbackItemComponent, MobileUserBarComponent, NavItemComponent } from './components';
|
||||
import { MobileMenuComponent } from './mobile-menu.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatDividerModule,
|
||||
FlexModule,
|
||||
SectionsLinksModule,
|
||||
RouterModule,
|
||||
TranslocoModule,
|
||||
MatIconModule,
|
||||
],
|
||||
declarations: [MobileMenuComponent, NavItemComponent, MobileMenuFeedbackItemComponent, MobileUserBarComponent],
|
||||
exports: [MobileMenuComponent],
|
||||
})
|
||||
export class MobileMenuModule {}
|
@ -1,4 +0,0 @@
|
||||
export interface BaseNavigationFlatNode {
|
||||
id: string;
|
||||
level: number;
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
import { NavigationFlatNode } from './navigation-flat-node';
|
||||
import { NavigationFlatNodeParent } from './navigation-flat-node-parent';
|
||||
import { PartialNavigationFlatNode } from './partial-navigation-flat-node';
|
||||
|
||||
export function isParentFlatNode(
|
||||
node: NavigationFlatNode | PartialNavigationFlatNode
|
||||
): node is NavigationFlatNodeParent {
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
return node.hasOwnProperty('isExpanded');
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
import { BaseNavigationFlatNode } from './base-navigation-flat-node';
|
||||
import { NavigationLinkNodeMeta } from './navigation-link-node-meta';
|
||||
|
||||
export interface NavigationFlatNodeLeaf extends BaseNavigationFlatNode {
|
||||
meta?: NavigationLinkNodeMeta;
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import { BaseNavigationFlatNode } from './base-navigation-flat-node';
|
||||
|
||||
export interface NavigationFlatNodeParent extends BaseNavigationFlatNode {
|
||||
isExpanded: boolean;
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
import { NavigationFlatNodeLeaf } from './navigation-flat-node-leaf';
|
||||
import { NavigationFlatNodeParent } from './navigation-flat-node-parent';
|
||||
|
||||
export type NavigationFlatNode = NavigationFlatNodeLeaf | NavigationFlatNodeParent;
|
@ -1,8 +0,0 @@
|
||||
import { IconName } from '@dsh/components/indicators/colored-icon/icon-name';
|
||||
|
||||
import { NavigationLink } from '../../navigation';
|
||||
|
||||
// export interface NavigationLinkNodeMeta extends Pick<NavigationLink, 'path' | 'navPlace'> {
|
||||
export interface NavigationLinkNodeMeta extends Pick<NavigationLink, 'path'> {
|
||||
icon?: IconName;
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
import { NavigationFlatNodeLeaf } from './navigation-flat-node-leaf';
|
||||
import { NavigationLinkNodeMeta } from './navigation-link-node-meta';
|
||||
|
||||
export type PartialNavigationFlatNodeLeaf = Omit<NavigationFlatNodeLeaf, 'meta'> & {
|
||||
meta?: { icon?: NavigationLinkNodeMeta['icon'] };
|
||||
};
|
@ -1,4 +0,0 @@
|
||||
import { NavigationFlatNodeParent } from './navigation-flat-node-parent';
|
||||
import { PartialNavigationFlatNodeLeaf } from './partial-navigation-flat-node-leaf';
|
||||
|
||||
export type PartialNavigationFlatNode = NavigationFlatNodeParent | PartialNavigationFlatNodeLeaf;
|
@ -1,6 +0,0 @@
|
||||
import { PartialNavigationNode } from './partial-navigation-node';
|
||||
|
||||
export interface PartialNavigationGroup {
|
||||
id: string;
|
||||
children: PartialNavigationNode[];
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
import { IconName } from '@dsh/components/indicators/colored-icon/icon-name';
|
||||
|
||||
export interface PartialNavigationLeaf {
|
||||
id: string;
|
||||
icon?: string | IconName;
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
import { PartialNavigationGroup } from './partial-navigation-group';
|
||||
import { PartialNavigationLeaf } from './partial-navigation-leaf';
|
||||
|
||||
export type PartialNavigationNode = PartialNavigationLeaf | PartialNavigationGroup;
|
@ -1,81 +0,0 @@
|
||||
import { getFlattenMobileMenu } from './get-flatten-mobile-menu';
|
||||
|
||||
describe('getFlattenMobileMenu', () => {
|
||||
it('should flat menu with no children', () => {
|
||||
expect(getFlattenMobileMenu([{ id: 'one' }, { id: 'two' }])).toEqual([
|
||||
{ id: 'one', level: 0, meta: {} },
|
||||
{ id: 'two', level: 0, meta: {} },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should flat menu with one child level', () => {
|
||||
expect(
|
||||
getFlattenMobileMenu([
|
||||
{ id: 'one' },
|
||||
{
|
||||
id: 'two',
|
||||
children: [{ id: 'two one' }, { id: 'two two' }],
|
||||
},
|
||||
{ id: 'three' },
|
||||
{ id: 'four' },
|
||||
])
|
||||
).toEqual([
|
||||
{ id: 'one', level: 0, meta: {} },
|
||||
{ id: 'two', level: 0, isExpanded: false },
|
||||
{ id: 'two one', level: 1, meta: {} },
|
||||
{ id: 'two two', level: 1, meta: {} },
|
||||
{ id: 'three', level: 0, meta: {} },
|
||||
{ id: 'four', level: 0, meta: {} },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should flat menu with two child levels', () => {
|
||||
expect(
|
||||
getFlattenMobileMenu([
|
||||
{ id: 'one' },
|
||||
{
|
||||
id: 'two',
|
||||
children: [
|
||||
{ id: 'two one' },
|
||||
{
|
||||
id: 'two two',
|
||||
children: [{ id: 'two two one' }, { id: 'two two two' }, { id: 'two two three' }],
|
||||
},
|
||||
{ id: 'two three' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'three',
|
||||
children: [{ id: 'three one' }, { id: 'three two' }, { id: 'three three' }],
|
||||
},
|
||||
{ id: 'four' },
|
||||
])
|
||||
).toEqual([
|
||||
{ id: 'one', level: 0, meta: {} },
|
||||
{ id: 'two', level: 0, isExpanded: false },
|
||||
{ id: 'two one', level: 1, meta: {} },
|
||||
{ id: 'two two', level: 1, isExpanded: false },
|
||||
{ id: 'two two one', level: 2, meta: {} },
|
||||
{ id: 'two two two', level: 2, meta: {} },
|
||||
{ id: 'two two three', level: 2, meta: {} },
|
||||
{ id: 'two three', level: 1, meta: {} },
|
||||
{ id: 'three', level: 0, isExpanded: false },
|
||||
{ id: 'three one', level: 1, meta: {} },
|
||||
{ id: 'three two', level: 1, meta: {} },
|
||||
{ id: 'three three', level: 1, meta: {} },
|
||||
{ id: 'four', level: 0, meta: {} },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should add icon data in meta object', () => {
|
||||
expect(
|
||||
getFlattenMobileMenu([
|
||||
{ id: 'one', icon: 'build' },
|
||||
{ id: 'two', icon: 'description' },
|
||||
])
|
||||
).toEqual([
|
||||
{ id: 'one', level: 0, meta: { icon: 'build' } },
|
||||
{ id: 'two', level: 0, meta: { icon: 'description' } },
|
||||
]);
|
||||
});
|
||||
});
|
@ -1,62 +0,0 @@
|
||||
import isNil from 'lodash-es/isNil';
|
||||
|
||||
import { ROOT_NODE_LEVEL } from '../consts';
|
||||
import { NavigationFlatNodeParent } from '../types/navigation-flat-node-parent';
|
||||
import { NavigationLinkNodeMeta } from '../types/navigation-link-node-meta';
|
||||
import { PartialNavigationFlatNode } from '../types/partial-navigation-flat-node';
|
||||
import { PartialNavigationGroup } from '../types/partial-navigation-group';
|
||||
import { PartialNavigationLeaf } from '../types/partial-navigation-leaf';
|
||||
import { PartialNavigationNode } from '../types/partial-navigation-node';
|
||||
|
||||
export function getFlattenMobileMenu(menu: PartialNavigationNode[]): PartialNavigationFlatNode[] {
|
||||
const flatNodes = menu.map((el: PartialNavigationNode) => {
|
||||
return {
|
||||
...el,
|
||||
level: ROOT_NODE_LEVEL,
|
||||
};
|
||||
});
|
||||
let hasUnhandledChildren = true;
|
||||
|
||||
while (hasUnhandledChildren) {
|
||||
const processingNodes = flatNodes.slice();
|
||||
hasUnhandledChildren = false;
|
||||
|
||||
for (let index = 0; index < processingNodes.length; index += 1) {
|
||||
const node: PartialNavigationNode & { level: number; isExpanded?: boolean } = processingNodes[index];
|
||||
const children = (node as PartialNavigationGroup).children;
|
||||
|
||||
if (Array.isArray(children)) {
|
||||
hasUnhandledChildren = true;
|
||||
const childNodes = children.map((child: PartialNavigationNode) => {
|
||||
return {
|
||||
...child,
|
||||
level: node.level + 1,
|
||||
};
|
||||
});
|
||||
flatNodes.splice(index + 1, 0, ...childNodes);
|
||||
|
||||
(node as NavigationFlatNodeParent).isExpanded = false;
|
||||
delete (node as PartialNavigationGroup).children;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return flatNodes.map(
|
||||
({ id, level, isExpanded, icon }: PartialNavigationLeaf & { level: number; isExpanded?: boolean }) => {
|
||||
const meta = isNil(icon) ? {} : ({ icon } as Partial<NavigationLinkNodeMeta>);
|
||||
|
||||
return isNil(isExpanded)
|
||||
? {
|
||||
id,
|
||||
level,
|
||||
meta,
|
||||
}
|
||||
: {
|
||||
id,
|
||||
level,
|
||||
isExpanded,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
@ -1,124 +0,0 @@
|
||||
import { InjectionToken } from '@angular/core';
|
||||
|
||||
import { NavigationLink } from './types/navigation-link';
|
||||
import { NavigationSections } from './types/navigation-sections';
|
||||
|
||||
export const ROOT_ROUTE_PATH = '/';
|
||||
|
||||
export const REALM_SEGMENT = 'realm';
|
||||
export const REALM_TYPE = 'REALM_TYPE';
|
||||
|
||||
export const PAGE_POSITION_INDEX = 0;
|
||||
export const SECTION_POSITION_INDEX = 1;
|
||||
export const SUBSECTION_POSITION_INDEX = 2;
|
||||
export const REALM_POSITION_INDEX = 1;
|
||||
export const REALM_TYPE_POSITION_INDEX = 2;
|
||||
export const REALM_SECTION_POSITION_INDEX = 3;
|
||||
export const REALM_SUBSECTION_POSITION_INDEX = 4;
|
||||
|
||||
export const MENU_LINKS_TOKEN = new InjectionToken<NavigationLink[]>('menu-links-token');
|
||||
|
||||
export const WALLETS_LINKS: string[] = [
|
||||
NavigationSections.Wallets,
|
||||
NavigationSections.WalletsWallet,
|
||||
NavigationSections.WalletsDeposits,
|
||||
NavigationSections.WalletsWithdrawals,
|
||||
NavigationSections.WalletsReports,
|
||||
NavigationSections.WalletsIntegrations,
|
||||
];
|
||||
|
||||
export const MENU_LINKS: NavigationLink[] = [
|
||||
{
|
||||
id: NavigationSections.Main,
|
||||
path: ROOT_ROUTE_PATH,
|
||||
navPlace: {
|
||||
page: ROOT_ROUTE_PATH,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: NavigationSections.Payments,
|
||||
path: `/payment-section/realm/${REALM_TYPE}/operations/payments`,
|
||||
navPlace: {
|
||||
page: 'payment-section',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: NavigationSections.Wallets,
|
||||
path: '/wallet-section/wallets',
|
||||
navPlace: {
|
||||
page: ['wallet-section', 'wallet'],
|
||||
section: 'wallets',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: NavigationSections.WalletsDeposits,
|
||||
path: '/wallet-section/deposits',
|
||||
navPlace: {
|
||||
page: 'wallet-section',
|
||||
section: 'deposits',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: NavigationSections.WalletsWithdrawals,
|
||||
path: '/wallet-section/withdrawals',
|
||||
navPlace: {
|
||||
page: 'wallet-section',
|
||||
section: 'withdrawals',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: NavigationSections.WalletsIntegrations,
|
||||
path: '/wallet-section/integrations',
|
||||
navPlace: {
|
||||
page: 'wallet-section',
|
||||
section: 'integrations',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: NavigationSections.Claims,
|
||||
path: '/claims',
|
||||
navPlace: {
|
||||
page: ['claims', 'claim', 'onboarding'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: NavigationSections.PaymentsAnalytics,
|
||||
path: `/payment-section/realm/${REALM_TYPE}/analytics`,
|
||||
navPlace: {
|
||||
page: 'payment-section',
|
||||
section: 'analytics',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: NavigationSections.PaymentsOperations,
|
||||
path: `/payment-section/realm/${REALM_TYPE}/operations`,
|
||||
navPlace: {
|
||||
page: 'payment-section',
|
||||
section: 'operations',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: NavigationSections.PaymentsPayouts,
|
||||
path: `/payment-section/realm/${REALM_TYPE}/payouts`,
|
||||
navPlace: {
|
||||
page: 'payment-section',
|
||||
section: 'payouts',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: NavigationSections.PaymentsReports,
|
||||
path: `/payment-section/realm/${REALM_TYPE}/reports`,
|
||||
navPlace: {
|
||||
page: 'payment-section',
|
||||
section: 'reports',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: NavigationSections.PaymentsIntegrations,
|
||||
path: `/payment-section/realm/${REALM_TYPE}/integrations`,
|
||||
navPlace: {
|
||||
page: 'payment-section',
|
||||
section: 'integrations',
|
||||
},
|
||||
},
|
||||
];
|
@ -1,5 +0,0 @@
|
||||
export * from './navigation.module';
|
||||
export * from './navigation.service';
|
||||
|
||||
export * from './types/navigation-link';
|
||||
export * from './types/navigation-sections';
|
@ -1,18 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import cloneDeep from 'lodash-es/cloneDeep';
|
||||
|
||||
import { MENU_LINKS, MENU_LINKS_TOKEN } from './consts';
|
||||
import { NavigationService } from './navigation.service';
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule],
|
||||
providers: [
|
||||
NavigationService,
|
||||
{
|
||||
provide: MENU_LINKS_TOKEN,
|
||||
useValue: cloneDeep(MENU_LINKS),
|
||||
},
|
||||
],
|
||||
})
|
||||
export class NavigationModule {}
|
@ -1,339 +0,0 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { Router } from '@angular/router';
|
||||
import { cold } from 'jasmine-marbles';
|
||||
import { of } from 'rxjs';
|
||||
import { anything, instance, mock, when } from 'ts-mockito';
|
||||
|
||||
import { PaymentInstitutionRealm, WalletService } from '@dsh/api';
|
||||
|
||||
import { MENU_LINKS_TOKEN, REALM_TYPE, ROOT_ROUTE_PATH } from './consts';
|
||||
import { NavigationService } from './navigation.service';
|
||||
import { NavigationLink } from './types/navigation-link';
|
||||
import { NavigationSections } from './types/navigation-sections';
|
||||
|
||||
describe('NavigationService', () => {
|
||||
let service: NavigationService;
|
||||
let mockWalletService: WalletService;
|
||||
let mockRouter: Router;
|
||||
|
||||
function createService(links: NavigationLink[]) {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
NavigationService,
|
||||
{
|
||||
provide: WalletService,
|
||||
useFactory: () => instance(mockWalletService),
|
||||
},
|
||||
{
|
||||
provide: Router,
|
||||
useFactory: () => instance(mockRouter),
|
||||
},
|
||||
{
|
||||
provide: MENU_LINKS_TOKEN,
|
||||
useValue: links,
|
||||
},
|
||||
],
|
||||
});
|
||||
service = TestBed.inject(NavigationService);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockWalletService = mock(WalletService);
|
||||
mockRouter = mock(Router);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
when(mockRouter.url).thenReturn('test_url');
|
||||
when(mockRouter.events).thenReturn(of());
|
||||
when(mockRouter.parseUrl(anything())).thenReturn({
|
||||
root: {
|
||||
children: {
|
||||
primary: null,
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
when(mockWalletService.hasWallets$).thenReturn(of());
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
createService([]);
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('availableLinks$', () => {
|
||||
beforeEach(() => {
|
||||
when(mockWalletService.hasWallets$).thenReturn(of(true));
|
||||
});
|
||||
|
||||
it('should filter wallets links if wallets are not available', () => {
|
||||
when(mockWalletService.hasWallets$).thenReturn(of(false));
|
||||
createService([
|
||||
{
|
||||
id: NavigationSections.Wallets,
|
||||
path: 'wallets',
|
||||
navPlace: {
|
||||
page: 'wallets',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: NavigationSections.WalletsWithdrawals,
|
||||
path: 'walletsWithdrawals',
|
||||
navPlace: {
|
||||
page: 'walletsWithdrawals',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'mine',
|
||||
path: 'mine',
|
||||
navPlace: {
|
||||
page: 'mine',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(service.availableLinks$).toBeObservable(
|
||||
cold('(a|)', {
|
||||
a: [
|
||||
{
|
||||
id: 'mine',
|
||||
path: 'mine',
|
||||
navPlace: {
|
||||
page: 'mine',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should return links as is if wallets is available', () => {
|
||||
when(mockWalletService.hasWallets$).thenReturn(of(true));
|
||||
|
||||
createService([
|
||||
{
|
||||
id: NavigationSections.Wallets,
|
||||
path: 'wallets',
|
||||
navPlace: {
|
||||
page: 'wallets',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'mine',
|
||||
path: 'mine',
|
||||
navPlace: {
|
||||
page: 'mine',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(service.availableLinks$).toBeObservable(
|
||||
cold('(a|)', {
|
||||
a: [
|
||||
{
|
||||
id: NavigationSections.Wallets,
|
||||
path: 'wallets',
|
||||
navPlace: {
|
||||
page: 'wallets',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'mine',
|
||||
path: 'mine',
|
||||
navPlace: {
|
||||
page: 'mine',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should replace realm type using existing realm from url', () => {
|
||||
when(mockRouter.url).thenReturn('/home/realm/live/section/subsection');
|
||||
|
||||
createService([
|
||||
{
|
||||
id: 'mine_realm',
|
||||
path: `/mine/realm/${REALM_TYPE}/section`,
|
||||
navPlace: {
|
||||
page: 'mine',
|
||||
section: 'section',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'mine',
|
||||
path: 'mine',
|
||||
navPlace: {
|
||||
page: 'mine',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(service.availableLinks$).toBeObservable(
|
||||
cold('(a|)', {
|
||||
a: [
|
||||
{
|
||||
id: 'mine_realm',
|
||||
path: `/mine/realm/live/section`,
|
||||
navPlace: {
|
||||
page: 'mine',
|
||||
section: 'section',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'mine',
|
||||
path: 'mine',
|
||||
navPlace: {
|
||||
page: 'mine',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should replace realm type using live if realm does not set in url', () => {
|
||||
when(mockRouter.url).thenReturn('/');
|
||||
|
||||
createService([
|
||||
{
|
||||
id: 'mine_realm',
|
||||
path: `/mine/realm/${REALM_TYPE}/section`,
|
||||
navPlace: {
|
||||
page: 'mine',
|
||||
section: 'section',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'mine',
|
||||
path: 'mine',
|
||||
navPlace: {
|
||||
page: 'mine',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(service.availableLinks$).toBeObservable(
|
||||
cold('(a|)', {
|
||||
a: [
|
||||
{
|
||||
id: 'mine_realm',
|
||||
path: `/mine/realm/${PaymentInstitutionRealm.Live}/section`,
|
||||
navPlace: {
|
||||
page: 'mine',
|
||||
section: 'section',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'mine',
|
||||
path: 'mine',
|
||||
navPlace: {
|
||||
page: 'mine',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('activeLink$', () => {
|
||||
beforeEach(() => {
|
||||
when(mockWalletService.hasWallets$).thenReturn(of(true));
|
||||
});
|
||||
|
||||
it('should return link that match current url', () => {
|
||||
when(mockRouter.url).thenReturn('/mine');
|
||||
when(mockRouter.parseUrl('/mine')).thenReturn({
|
||||
root: {
|
||||
children: {
|
||||
primary: {
|
||||
segments: [{ path: 'mine' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
|
||||
createService([
|
||||
{
|
||||
id: 'root',
|
||||
path: ROOT_ROUTE_PATH,
|
||||
navPlace: { page: ROOT_ROUTE_PATH },
|
||||
},
|
||||
{
|
||||
id: 'mine_realm',
|
||||
path: `/mine/realm/${REALM_TYPE}/section`,
|
||||
navPlace: {
|
||||
page: 'mine',
|
||||
section: 'section',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'mine',
|
||||
path: 'mine',
|
||||
navPlace: {
|
||||
page: 'mine',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(service.activeLink$).toBeObservable(
|
||||
cold('(a|)', {
|
||||
a: {
|
||||
id: 'mine',
|
||||
path: 'mine',
|
||||
navPlace: {
|
||||
page: 'mine',
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should return link with root path if there is no valid link for current url', () => {
|
||||
when(mockRouter.url).thenReturn('/idk');
|
||||
when(mockRouter.parseUrl('/idk')).thenReturn({
|
||||
root: {
|
||||
children: {
|
||||
primary: {
|
||||
segments: [{ path: 'idk' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
|
||||
createService([
|
||||
{
|
||||
id: 'root',
|
||||
path: ROOT_ROUTE_PATH,
|
||||
navPlace: { page: ROOT_ROUTE_PATH },
|
||||
},
|
||||
{
|
||||
id: 'mine_realm',
|
||||
path: `/mine/realm/${REALM_TYPE}/section`,
|
||||
navPlace: {
|
||||
page: 'mine',
|
||||
section: 'section',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'mine',
|
||||
path: 'mine',
|
||||
navPlace: {
|
||||
page: 'mine',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(service.activeLink$).toBeObservable(
|
||||
cold('(a|)', {
|
||||
a: {
|
||||
id: 'root',
|
||||
path: ROOT_ROUTE_PATH,
|
||||
navPlace: { page: ROOT_ROUTE_PATH },
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,85 +0,0 @@
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { Event, Router, UrlSegment } from '@angular/router';
|
||||
import { combineLatest, Observable } from 'rxjs';
|
||||
import { distinctUntilChanged, map, shareReplay, startWith } from 'rxjs/operators';
|
||||
|
||||
import { PaymentInstitutionRealm, WalletService } from '@dsh/api';
|
||||
|
||||
import { MENU_LINKS_TOKEN, REALM_TYPE, REALM_TYPE_POSITION_INDEX, ROOT_ROUTE_PATH, WALLETS_LINKS } from './consts';
|
||||
import { NavigationLink } from './types/navigation-link';
|
||||
import { findActiveNavLink } from './utils/find-active-nav-link';
|
||||
|
||||
@Injectable()
|
||||
export class NavigationService {
|
||||
availableLinks$: Observable<NavigationLink[]>;
|
||||
activeLink$: Observable<NavigationLink>;
|
||||
|
||||
private currentUrl$: Observable<string> = this.router.events.pipe(
|
||||
startWith<Event, null>(null),
|
||||
map(() => this.router.url),
|
||||
distinctUntilChanged(),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
private currentUrlSegments$: Observable<string[]> = this.currentUrl$.pipe(
|
||||
map((url: string) => {
|
||||
const urlRootChildren = this.router.parseUrl(url).root.children;
|
||||
return urlRootChildren.primary?.segments ?? [];
|
||||
}),
|
||||
map((segments: UrlSegment[]) => segments.map((segment: UrlSegment) => segment.path))
|
||||
);
|
||||
|
||||
private realm$: Observable<PaymentInstitutionRealm> = this.currentUrlSegments$.pipe(
|
||||
map(
|
||||
(segments: string[]) =>
|
||||
(segments[REALM_TYPE_POSITION_INDEX] as PaymentInstitutionRealm) ?? PaymentInstitutionRealm.Live
|
||||
)
|
||||
);
|
||||
|
||||
constructor(
|
||||
private walletsService: WalletService,
|
||||
private router: Router,
|
||||
@Inject(MENU_LINKS_TOKEN)
|
||||
private menuLinks: NavigationLink[]
|
||||
) {
|
||||
this.initLinks();
|
||||
this.initActiveLink();
|
||||
this.activeLink$.subscribe();
|
||||
}
|
||||
|
||||
private initLinks(): void {
|
||||
this.availableLinks$ = combineLatest([this.walletsService.hasWallets$, this.realm$]).pipe(
|
||||
map(([hasWallets, realm]: [boolean, PaymentInstitutionRealm]) => {
|
||||
return [this.filterLinks(hasWallets), realm];
|
||||
}),
|
||||
map(([links, realm]: [NavigationLink[], PaymentInstitutionRealm]) => {
|
||||
return links.map((link: NavigationLink) => {
|
||||
return {
|
||||
...link,
|
||||
path: link.path.replace(REALM_TYPE, realm),
|
||||
};
|
||||
});
|
||||
}),
|
||||
shareReplay(1)
|
||||
);
|
||||
}
|
||||
|
||||
private initActiveLink(): void {
|
||||
this.activeLink$ = combineLatest([this.currentUrlSegments$, this.availableLinks$]).pipe(
|
||||
map(([segments, links]: [string[], NavigationLink[]]) => {
|
||||
// route link is active by default
|
||||
return (
|
||||
findActiveNavLink(segments, links) ??
|
||||
links.find((link: NavigationLink) => link.path === ROOT_ROUTE_PATH)
|
||||
);
|
||||
}),
|
||||
shareReplay(1)
|
||||
);
|
||||
}
|
||||
|
||||
private filterLinks(hasWallets: boolean): NavigationLink[] {
|
||||
return this.menuLinks.filter(({ id }: NavigationLink) => {
|
||||
return WALLETS_LINKS.indexOf(id) >= 0 ? hasWallets : true;
|
||||
});
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
export interface NavigationLinkSections {
|
||||
page: string | string[];
|
||||
section?: string;
|
||||
subsection?: string;
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import { NavigationLinkSections } from './navigation-link-sections';
|
||||
|
||||
export interface NavigationLink {
|
||||
id: string;
|
||||
path: string;
|
||||
navPlace: NavigationLinkSections;
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
export enum NavigationSections {
|
||||
Main = 'main',
|
||||
Payments = 'payments',
|
||||
PaymentsAnalytics = 'payments_analytics',
|
||||
PaymentsOperations = 'payments_operations',
|
||||
PaymentsPayouts = 'payments_payouts',
|
||||
PaymentsReports = 'payments_reports',
|
||||
PaymentsIntegrations = 'payments_integrations',
|
||||
Wallets = 'wallets',
|
||||
WalletsWallet = 'wallets_wallet',
|
||||
WalletsDeposits = 'wallets_deposits',
|
||||
WalletsWithdrawals = 'wallets_withdrawals',
|
||||
WalletsReports = 'wallets_reports',
|
||||
WalletsIntegrations = 'wallets_integrations',
|
||||
Claims = 'claims',
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
import { findActiveNavLink } from './find-active-nav-link';
|
||||
|
||||
describe('findActiveNavLink', () => {
|
||||
it('should return the deepest matched link', () => {
|
||||
expect(
|
||||
findActiveNavLink(
|
||||
['page', 'realm', 'live', 'section', 'subsection'],
|
||||
[
|
||||
{
|
||||
id: 'home',
|
||||
path: '/',
|
||||
navPlace: { page: '/' },
|
||||
},
|
||||
{
|
||||
id: 'not_find_one',
|
||||
path: '/page/realm/any/section/another_section',
|
||||
navPlace: {
|
||||
page: 'page',
|
||||
section: 'section',
|
||||
subsection: 'another_section',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'find',
|
||||
path: '/page/realm/any/section/subsection',
|
||||
navPlace: {
|
||||
page: 'page',
|
||||
section: 'section',
|
||||
subsection: 'subsection',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'not_find_two',
|
||||
path: '/page/realm/any/section/another_section_two',
|
||||
navPlace: {
|
||||
page: 'page',
|
||||
section: 'section',
|
||||
subsection: 'another_section',
|
||||
},
|
||||
},
|
||||
]
|
||||
)
|
||||
).toEqual({
|
||||
id: 'find',
|
||||
path: '/page/realm/any/section/subsection',
|
||||
navPlace: {
|
||||
page: 'page',
|
||||
section: 'section',
|
||||
subsection: 'subsection',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null if there is no url segments was provided', () => {
|
||||
expect(
|
||||
findActiveNavLink(
|
||||
[],
|
||||
[
|
||||
{
|
||||
id: 'home',
|
||||
path: '/',
|
||||
navPlace: { page: '/' },
|
||||
},
|
||||
]
|
||||
)
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null if there is no valid active url was found', () => {
|
||||
expect(
|
||||
findActiveNavLink(
|
||||
['home'],
|
||||
[
|
||||
{
|
||||
id: 'root',
|
||||
path: '/',
|
||||
navPlace: { page: '/' },
|
||||
},
|
||||
]
|
||||
)
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
@ -1,84 +0,0 @@
|
||||
import isEmpty from 'lodash-es/isEmpty';
|
||||
import isNil from 'lodash-es/isNil';
|
||||
|
||||
import {
|
||||
PAGE_POSITION_INDEX,
|
||||
REALM_POSITION_INDEX,
|
||||
REALM_SECTION_POSITION_INDEX,
|
||||
REALM_SEGMENT,
|
||||
REALM_SUBSECTION_POSITION_INDEX,
|
||||
SECTION_POSITION_INDEX,
|
||||
SUBSECTION_POSITION_INDEX,
|
||||
} from '../consts';
|
||||
import { NavigationLink } from '../types/navigation-link';
|
||||
|
||||
const NO_MATCH_LEVEL = 0;
|
||||
const PAGE_MATCH_LEVEL = 1;
|
||||
const SECTION_MATCH_LEVEL = 2;
|
||||
const SUBSECTION_MATCH_LEVEL = 3;
|
||||
|
||||
const findSectionsSegments = (urlSegments: string[]): [string | undefined, string | undefined] => {
|
||||
const realmSegment = urlSegments[REALM_POSITION_INDEX];
|
||||
let sectionSegment: string | undefined;
|
||||
let subsectionSegment: string | undefined;
|
||||
|
||||
if (realmSegment === REALM_SEGMENT) {
|
||||
sectionSegment = urlSegments[REALM_SECTION_POSITION_INDEX];
|
||||
subsectionSegment = urlSegments[REALM_SUBSECTION_POSITION_INDEX];
|
||||
} else {
|
||||
sectionSegment = urlSegments[SECTION_POSITION_INDEX];
|
||||
subsectionSegment = urlSegments[SUBSECTION_POSITION_INDEX];
|
||||
}
|
||||
|
||||
return [sectionSegment, subsectionSegment];
|
||||
};
|
||||
|
||||
export const findActiveNavLink = (urlSegments: string[], links: NavigationLink[]): NavigationLink | null => {
|
||||
if (isEmpty(urlSegments)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pageSegment = urlSegments[PAGE_POSITION_INDEX];
|
||||
const [sectionSegment, subsectionSegment] = findSectionsSegments(urlSegments);
|
||||
|
||||
const activeLink = links
|
||||
.map((link: NavigationLink) => {
|
||||
const { page, section, subsection } = link.navPlace;
|
||||
|
||||
const isSamePage = Array.isArray(page)
|
||||
? page.some((path: string) => path === pageSegment)
|
||||
: page === pageSegment;
|
||||
const isSameSection = section === sectionSegment;
|
||||
const isSameSubsection = subsection === subsectionSegment;
|
||||
|
||||
if (isNil(section) || isNil(sectionSegment)) {
|
||||
return {
|
||||
matchLevel: isSamePage ? PAGE_MATCH_LEVEL : NO_MATCH_LEVEL,
|
||||
link,
|
||||
};
|
||||
}
|
||||
|
||||
if (isNil(subsection) || isNil(subsectionSegment)) {
|
||||
return {
|
||||
matchLevel: isSamePage && isSameSection ? SECTION_MATCH_LEVEL : NO_MATCH_LEVEL,
|
||||
link,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
matchLevel: isSamePage && isSameSection && isSameSubsection ? SUBSECTION_MATCH_LEVEL : NO_MATCH_LEVEL,
|
||||
link,
|
||||
};
|
||||
})
|
||||
.filter(({ matchLevel }: { matchLevel: number; link: NavigationLink }) => matchLevel > NO_MATCH_LEVEL)
|
||||
.sort((a: { matchLevel: number; link: NavigationLink }, b: { matchLevel: number; link: NavigationLink }) =>
|
||||
a.matchLevel >= b.matchLevel ? 1 : -1
|
||||
)
|
||||
.pop();
|
||||
|
||||
if (isNil(activeLink)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return activeLink.link;
|
||||
};
|
@ -1,2 +0,0 @@
|
||||
export * from './link-id';
|
||||
export * from './toolbar-link';
|
@ -1,6 +0,0 @@
|
||||
export enum LinkId {
|
||||
Main = 'main',
|
||||
Payments = 'payments',
|
||||
Wallets = 'wallets',
|
||||
Claims = 'claims',
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import { LinkId } from './link-id';
|
||||
|
||||
export interface ToolbarLink {
|
||||
id: LinkId;
|
||||
path: string;
|
||||
exact?: boolean;
|
||||
}
|
@ -1,12 +1,8 @@
|
||||
<div class="toolbar" fxLayout fxLayoutGap="24px" fxLayoutAlign="start center">
|
||||
<dsh-brand fxFlex="168px" [inverted]="inverted" [name]="logoName" class="dsh-side-section"></dsh-brand>
|
||||
<nav
|
||||
[class.dsh-inverted-nav-bar]="inverted"
|
||||
*transloco="let t; scope: 'toolbar'; read: 'toolbar.nav'"
|
||||
mat-tab-nav-bar
|
||||
>
|
||||
<nav [class.dsh-inverted-nav-bar]="inverted" mat-tab-nav-bar>
|
||||
<a
|
||||
*ngFor="let link of links$ | async"
|
||||
*ngFor="let link of sectionLinks$ | async"
|
||||
mat-tab-link
|
||||
[routerLink]="link.path"
|
||||
routerLinkActive
|
||||
@ -14,7 +10,7 @@
|
||||
#rla="routerLinkActive"
|
||||
[active]="rla.isActive"
|
||||
>
|
||||
{{ t(link.id) }}
|
||||
{{ link.label }}
|
||||
</a>
|
||||
</nav>
|
||||
<dsh-actionbar fxFlex="grow" fxLayoutAlign="end"></dsh-actionbar>
|
||||
|
@ -1,11 +1,8 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import { first, map } from 'rxjs/operators';
|
||||
|
||||
import { WalletService } from '@dsh/api/wallet';
|
||||
import { SectionsLinksService } from '@dsh/app/shared/services/sections-links';
|
||||
import { coerceBoolean } from '@dsh/utils';
|
||||
|
||||
import { createLinks } from './utils';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-toolbar',
|
||||
templateUrl: 'toolbar.component.html',
|
||||
@ -16,7 +13,7 @@ export class ToolbarComponent {
|
||||
@Input() @coerceBoolean inverted: boolean;
|
||||
@Input() logoName: string;
|
||||
|
||||
links$ = this.walletsService.hasWallets$.pipe(map(createLinks), first());
|
||||
sectionLinks$ = this.sectionsLinksService.sectionLinks$;
|
||||
|
||||
constructor(private walletsService: WalletService) {}
|
||||
constructor(private sectionsLinksService: SectionsLinksService) {}
|
||||
}
|
||||
|
@ -3,7 +3,8 @@ import { NgModule } from '@angular/core';
|
||||
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { TranslocoModule } from '@ngneat/transloco';
|
||||
|
||||
import { SectionsLinksModule } from '@dsh/app/shared/services/sections-links';
|
||||
|
||||
import { ActionbarModule } from '../actionbar';
|
||||
import { BrandModule } from '../brand';
|
||||
@ -16,8 +17,8 @@ import { ToolbarComponent } from './toolbar.component';
|
||||
BrandModule,
|
||||
ActionbarModule,
|
||||
RouterModule,
|
||||
TranslocoModule,
|
||||
MatTabsModule,
|
||||
SectionsLinksModule,
|
||||
],
|
||||
declarations: [ToolbarComponent],
|
||||
exports: [ToolbarComponent],
|
||||
|
@ -1,19 +0,0 @@
|
||||
import { LinkId, ToolbarLink } from '../model';
|
||||
|
||||
export const createLinks = (hasWallets: boolean): ToolbarLink[] =>
|
||||
[
|
||||
{
|
||||
id: LinkId.Main,
|
||||
path: '/',
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
id: LinkId.Payments,
|
||||
path: `/payment-section`,
|
||||
},
|
||||
hasWallets && {
|
||||
id: LinkId.Wallets,
|
||||
path: '/wallet-section',
|
||||
},
|
||||
{ id: LinkId.Claims, path: '/claim-section' },
|
||||
].filter(Boolean);
|
@ -6,3 +6,4 @@ export * from './fake-paginator.service';
|
||||
export * from './notification';
|
||||
export * from './error';
|
||||
export * from './keycloak-token-info';
|
||||
export * from './sections-links';
|
||||
|
3
src/app/shared/services/sections-links/index.ts
Normal file
3
src/app/shared/services/sections-links/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './section-links.module';
|
||||
export * from './section-links.service';
|
||||
export * from './model';
|
1
src/app/shared/services/sections-links/model/index.ts
Normal file
1
src/app/shared/services/sections-links/model/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './section-link';
|
@ -0,0 +1,5 @@
|
||||
export interface SectionLink {
|
||||
label: string;
|
||||
path: string;
|
||||
exact?: boolean;
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { TranslocoModule } from '@ngneat/transloco';
|
||||
|
||||
import { SectionsLinksService } from './section-links.service';
|
||||
|
||||
@NgModule({
|
||||
imports: [TranslocoModule],
|
||||
providers: [SectionsLinksService],
|
||||
})
|
||||
export class SectionsLinksModule {}
|
@ -0,0 +1,22 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { TranslocoService } from '@ngneat/transloco';
|
||||
import { combineLatest, Observable } from 'rxjs';
|
||||
import { first, map } from 'rxjs/operators';
|
||||
|
||||
import { WalletService } from '@dsh/api/wallet';
|
||||
|
||||
import { SectionLink } from './model';
|
||||
import { createLinks } from './utils';
|
||||
|
||||
@Injectable()
|
||||
export class SectionsLinksService {
|
||||
sectionLinks$: Observable<SectionLink[]> = combineLatest([
|
||||
this.transloco.selectTranslateObject<{ [k: string]: string }>('sectionLinks'),
|
||||
this.walletsService.hasWallets$,
|
||||
]).pipe(
|
||||
map((v) => createLinks(...v)),
|
||||
first()
|
||||
);
|
||||
|
||||
constructor(private walletsService: WalletService, private transloco: TranslocoService) {}
|
||||
}
|
22
src/app/shared/services/sections-links/utils/create-links.ts
Normal file
22
src/app/shared/services/sections-links/utils/create-links.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { SectionLink } from '../model';
|
||||
|
||||
export const createLinks = (
|
||||
{ claims, main, payments, wallets }: { [k: string]: string },
|
||||
hasWallets: boolean
|
||||
): SectionLink[] =>
|
||||
[
|
||||
{
|
||||
label: main,
|
||||
path: '/',
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
label: payments,
|
||||
path: `/payment-section`,
|
||||
},
|
||||
hasWallets && {
|
||||
label: wallets,
|
||||
path: '/wallet-section',
|
||||
},
|
||||
{ label: claims, path: '/claim-section' },
|
||||
].filter(Boolean);
|
@ -228,5 +228,11 @@
|
||||
"success": "Успешно",
|
||||
"httpError": "Произошла ошибка в процессе передачи / получения данных",
|
||||
"unknownError": "Неизвестная ошибка"
|
||||
},
|
||||
"sectionLinks": {
|
||||
"main": "Главная",
|
||||
"payments": "Платежи",
|
||||
"wallets": "Кошельки",
|
||||
"claims": "Заявки"
|
||||
}
|
||||
}
|
||||
|
@ -3,3 +3,7 @@
|
||||
@mixin dsh-navbar-theme($theme) {
|
||||
@include dsh-navbar-item-theme($theme);
|
||||
}
|
||||
|
||||
@mixin dsh-navbar-typography($config) {
|
||||
@include dsh-navbar-item-typography($config);
|
||||
}
|
||||
|
@ -35,7 +35,7 @@
|
||||
@import '../../app/home/home-theme';
|
||||
@import '../../app/home/actionbar/actionbar-theme';
|
||||
@import '../../app/home/welcome-image/welcome-image-theme';
|
||||
@import '../../app/home/mobile-grid/mobile_menu_theme';
|
||||
@import '../../app/home/mobile-grid/mobile-grid-theme';
|
||||
@import '../../app/dadata/dadata-theme';
|
||||
@import '../../app/sections/landing/landing-theme';
|
||||
@import '../../app/sections/payment-section/analytics/percent-difference/percent-difference-theme';
|
||||
@ -52,7 +52,7 @@
|
||||
@include dsh-card-theme($theme);
|
||||
@include dsh-state-nav-theme($theme);
|
||||
@include dsh-navbar-theme($theme);
|
||||
@include dsh-mobile-menu-theme($theme);
|
||||
@include dsh-mobile-grid-theme($theme);
|
||||
@include dsh-button-theme($theme);
|
||||
@include dsh-button-toggle-theme($theme);
|
||||
@include dsh-float-panel-theme($theme);
|
||||
|
@ -25,6 +25,7 @@
|
||||
@import '../../app/home/home-theme';
|
||||
@import '../../app/home/toolbar/toolbar-theme';
|
||||
@import '../../app/home/welcome-image/welcome-image-theme';
|
||||
@import '../../app/home/mobile-grid/mobile-grid-theme';
|
||||
@import '../../app/dadata/dadata-theme';
|
||||
@import '../../app/sections/payment-section/balances/balances-theme';
|
||||
|
||||
@ -41,7 +42,6 @@
|
||||
@include dsh-range-datepicker-typography($config);
|
||||
@include dsh-panel-typography($config);
|
||||
@include dsh-charts-typography($config);
|
||||
@include dsh-navbar-item-typography($config);
|
||||
@include dsh-global-banner-typography($config);
|
||||
@include dsh-row-typography($config);
|
||||
@include dsh-accordion-typography($config);
|
||||
@ -54,4 +54,6 @@
|
||||
@include dsh-breadcrumb-typography($config);
|
||||
@include dsh-link-typography($config);
|
||||
@include dsh-toolbar-typography($config);
|
||||
@include dsh-navbar-typography($config);
|
||||
@include dsh-mobile-grid-typography($config);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user