FR-699: Shop creation fixes (#538)

This commit is contained in:
Rinat Arsaev 2021-08-13 18:17:57 +03:00 committed by GitHub
parent 5f86ed6632
commit 407b46da20
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
156 changed files with 719 additions and 2394 deletions

1
.gitignore vendored
View File

@ -13,7 +13,6 @@ chrome-profiler-events.json
speed-measure-plugin.json
# IDEs and editors
/.idea
.project
.classpath
.c9/

5
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CamelCaseConfig">
<option name="cb5State" value="false" />
<option name="cb6State" value="false" />
</component>
</project>

View File

@ -0,0 +1,50 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<HTMLCodeStyleSettings>
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
<option name="HTML_QUOTE_STYLE" value="Single" />
<option name="HTML_ENFORCE_QUOTES" value="true" />
</HTMLCodeStyleSettings>
<JSCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="USE_DOUBLE_QUOTES" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</JSCodeStyleSettings>
<TypeScriptCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="USE_DOUBLE_QUOTES" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</TypeScriptCodeStyleSettings>
<VueCodeStyleSettings>
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
</VueCodeStyleSettings>
<codeStyleSettings language="HTML">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">
<option name="SOFT_MARGINS" value="120" />
</codeStyleSettings>
<codeStyleSettings language="TypeScript">
<option name="SOFT_MARGINS" value="120" />
</codeStyleSettings>
<codeStyleSettings language="Vue">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

13
.idea/dashboard.iml Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
<excludeFolder url="file://$MODULE_DIR$/dist" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EslintConfiguration">
<option name="fix-on-save" value="true" />
</component>
</project>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/dashboard.iml" filepath="$PROJECT_DIR$/.idea/dashboard.iml" />
</modules>
</component>
</project>

7
.idea/prettier.xml Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PrettierConfiguration">
<option name="myRunOnSave" value="true" />
<option name="myFilesPattern" value="{**/*,*}.{js,ts,jsx,tsx,md,json,html,svg}" />
</component>
</project>

18
.idea/vcs.xml Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/build_utils" vcs="Git" />
<mapping directory="$PROJECT_DIR$/schemes/claim-management/v0" vcs="Git" />
<mapping directory="$PROJECT_DIR$/schemes/dark-api/v0" vcs="Git" />
<mapping directory="$PROJECT_DIR$/schemes/messages/v0" vcs="Git" />
<mapping directory="$PROJECT_DIR$/schemes/organizations/v0" vcs="Git" />
<mapping directory="$PROJECT_DIR$/schemes/questionary-aggr-proxy/v0" vcs="Git" />
<mapping directory="$PROJECT_DIR$/schemes/questionary/v0" vcs="Git" />
<mapping directory="$PROJECT_DIR$/schemes/sender/v0" vcs="Git" />
<mapping directory="$PROJECT_DIR$/schemes/swag-analytics/v1" vcs="Git" />
<mapping directory="$PROJECT_DIR$/schemes/swag-wallets/v0" vcs="Git" />
<mapping directory="$PROJECT_DIR$/schemes/swag/v2" vcs="Git" />
<mapping directory="$PROJECT_DIR$/schemes/url-shortener/v0" vcs="Git" />
</component>
</project>

32
package-lock.json generated
View File

@ -47,6 +47,7 @@
"moment": "^2.24.0",
"ng-apexcharts": "^1.3.0",
"ng-yandex-metrika": "^4.0.0",
"ngx-mat-select-search": "^3.3.0",
"rxjs": "~6.6.7",
"shelljs": "^0.8.4",
"ts-keycode-enum": "^1.0.6",
@ -14200,6 +14201,22 @@
"rxjs": ">= 6.0.0"
}
},
"node_modules/ngx-mat-select-search": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/ngx-mat-select-search/-/ngx-mat-select-search-3.3.0.tgz",
"integrity": "sha512-OLLkNpWdFvb+AKtKEuq0G48gz9l5HA1WuVBuayOubekpq3UlMUHh+xr9TFh5DX4VY6NektTzG/XYfdAh5Sg9dg==",
"dependencies": {
"tslib": "^1.9.0"
},
"peerDependencies": {
"@angular/material": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0"
}
},
"node_modules/ngx-mat-select-search/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/nice-try": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
@ -33376,6 +33393,21 @@
"webpack-merge": "^5.0.0"
}
},
"ngx-mat-select-search": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/ngx-mat-select-search/-/ngx-mat-select-search-3.3.0.tgz",
"integrity": "sha512-OLLkNpWdFvb+AKtKEuq0G48gz9l5HA1WuVBuayOubekpq3UlMUHh+xr9TFh5DX4VY6NektTzG/XYfdAh5Sg9dg==",
"requires": {
"tslib": "^1.9.0"
},
"dependencies": {
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
}
},
"nice-try": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",

View File

@ -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 1801",
"lint-cmd": "eslint \"src/**/*.{ts,js,html}\" --max-warnings 1765",
"lint-cache-cmd": "npm run lint-cmd -- --cache",
"lint": "npm run lint-cache-cmd",
"lint-fix": "npm run lint-cache-cmd -- --fix",
@ -68,6 +68,7 @@
"moment": "^2.24.0",
"ng-apexcharts": "^1.3.0",
"ng-yandex-metrika": "^4.0.0",
"ngx-mat-select-search": "^3.3.0",
"rxjs": "~6.6.7",
"shelljs": "^0.8.4",
"ts-keycode-enum": "^1.0.6",

View File

@ -1,7 +1,5 @@
import { PartyModification, PayoutToolInfo, RussianBankAccount } from '@dsh/api-codegen/claim-management';
import { RussianShopCreateData } from '../../../../sections/payment-section/integrations/shops/shop-creation/create-russian-shop-entity/types/russian-shop-create-data';
import { createContractPayoutToolCreationModification } from './create-contract-payout-tool-creation-modification';
import { createContractPayoutToolCreationModification } from '@dsh/api/claims/claim-party-modification';
export function createRussianContractPayoutToolCreationModification(
id: string,
@ -18,16 +16,3 @@ export function createRussianContractPayoutToolCreationModification(
},
});
}
export function createTestRussianContractPayoutToolCreationModification(
id: string,
payoutToolID: string,
{ bankAccount: { account, bankName, bankPostAccount, bankBik } }: RussianShopCreateData
): PartyModification {
return createRussianContractPayoutToolCreationModification(id, payoutToolID, {
account,
bankName,
bankPostAccount,
bankBik,
});
}

View File

@ -1,18 +1,18 @@
import { Injectable } from '@angular/core';
import { IdGeneratorService } from '@rbkmoney/id-generator';
import { defer, Observable, Subject } from 'rxjs';
import { shareReplay, startWith, switchMapTo } from 'rxjs/operators';
import { startWith, switchMapTo } from 'rxjs/operators';
import { Shop } from '@dsh/api-codegen/capi';
import { ShopsService } from '@dsh/api-codegen/capi/shops.service';
import { SHARE_REPLAY_CONF } from '@dsh/operators';
import { shareReplayRefCount } from '@dsh/operators';
@Injectable()
export class ApiShopsService {
shops$: Observable<Shop[]> = defer(() => this.reloadShops$).pipe(
startWith<void, null>(null),
switchMapTo(this.getShops()),
shareReplay(SHARE_REPLAY_CONF)
shareReplayRefCount()
);
private reloadShops$ = new Subject<void>();

View File

@ -18,7 +18,7 @@ import * as Sentry from '@sentry/angular';
import { ErrorModule, KeycloakTokenInfoModule, LoggerModule } from '@dsh/app/shared/services';
import { QUERY_PARAMS_SERIALIZERS } from '@dsh/app/shared/services/query-params/utils/query-params-serializers';
import { createDateRangeWithPresetSerializer } from '@dsh/components/filters/date-range-filter';
import { AUTOCOMPLETE_FIELD_OPTIONS } from '@dsh/components/form-controls/autocomplete-field';
import { SELECT_SEARCH_FIELD_OPTIONS } from '@dsh/components/form-controls/select-search-field';
import { ENV, environment } from '../environments';
import { OrganizationsModule } from './api';
@ -122,7 +122,7 @@ import { YandexMetrikaConfigService, YandexMetrikaModule } from './yandex-metrik
deps: [Router],
},
{
provide: AUTOCOMPLETE_FIELD_OPTIONS,
provide: SELECT_SEARCH_FIELD_OPTIONS,
useValue: {
svgIcon: 'cross',
},

View File

@ -1,12 +1,21 @@
<div class="container" fxLayout="column" fxLayoutGap="32px">
<div fxLayout="column" fxLayoutGap="48px">
<div class="mat-display-1" *transloco="let t; scope: 'claims'; read: 'claims'">
<div *transloco="let t; scope: 'claims'; read: 'claims'" fxLayout="column" fxLayoutGap="48px">
<div class="mat-display-1">
{{ t('title') }}
</div>
<dsh-claims-search-filters
[initParams]="params$ | async"
(searchParamsChanges)="search($event)"
></dsh-claims-search-filters>
<div
fxLayout="column-reverse"
fxLayout.gt-sm="row"
fxLayoutGap="16px"
fxLayoutAlign="start start"
fxLayoutAlign.gt-sm="space-between"
>
<dsh-claims-search-filters
[initParams]="params$ | async"
(searchParamsChanges)="search($event)"
></dsh-claims-search-filters>
<button (click)="createShop()" color="accent" dsh-button>{{ t('createClaim') }}</button>
</div>
</div>
<dsh-claims-list
[lastUpdated]="lastUpdated$ | async"

View File

@ -5,9 +5,10 @@ import { FlexLayoutModule } from '@angular/flex-layout';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { Router } from '@angular/router';
import { of } from 'rxjs';
import { deepEqual, instance, mock, verify, when } from 'ts-mockito';
import { deepEqual, mock, verify, when } from 'ts-mockito';
import { Claim } from '@dsh/api-codegen/claim-management';
import { ShopCreationService } from '@dsh/app/shared/components/shop-creation';
import { QueryParamsService } from '@dsh/app/shared/services/query-params';
import { provideMockService } from '@dsh/app/shared/tests';
import { getTranslocoModule } from '@dsh/app/shared/tests/get-transloco-module';
@ -51,11 +52,13 @@ describe('ClaimsComponent', () => {
let mockFetchClaimsService: FetchClaimsService;
let mockRouter: Router;
let mockQueryParamsService: QueryParamsService<any>;
let mockShopCreationService: ShopCreationService;
beforeEach(() => {
mockRouter = mock(Router);
mockFetchClaimsService = mock(FetchClaimsService);
mockQueryParamsService = mock(QueryParamsService);
mockShopCreationService = mock(ShopCreationService);
});
beforeEach(() => {
@ -76,15 +79,10 @@ describe('ClaimsComponent', () => {
],
declarations: [ClaimsComponent, MockClaimsSearchFiltersComponent, MockClaimsListComponent],
providers: [
{
provide: FetchClaimsService,
useFactory: () => instance(mockFetchClaimsService),
},
{
provide: Router,
useFactory: () => instance(mockRouter),
},
provideMockService(FetchClaimsService, mockFetchClaimsService),
provideMockService(Router, mockRouter),
provideMockService(QueryParamsService, mockQueryParamsService),
provideMockService(ShopCreationService, mockShopCreationService),
],
})
.overrideComponent(ClaimsComponent, {

View File

@ -1,6 +1,7 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Router } from '@angular/router';
import { ShopCreationService } from '@dsh/app/shared/components/shop-creation';
import { SpinnerType } from '@dsh/components/indicators';
import { QueryParamsService } from '../../../shared/services/query-params';
@ -25,7 +26,8 @@ export class ClaimsComponent {
constructor(
private fetchClaimsService: FetchClaimsService,
private router: Router,
private qp: QueryParamsService<Filters>
private qp: QueryParamsService<Filters>,
private shopCreationService: ShopCreationService
) {}
search(filters: Filters): void {
@ -44,4 +46,8 @@ export class ClaimsComponent {
goToClaimDetails(id: number): void {
void this.router.navigate(['claim-section', 'claims', id]);
}
createShop(): void {
this.shopCreationService.createShop();
}
}

View File

@ -10,6 +10,7 @@ import { MatSelectModule } from '@angular/material/select';
import { TranslocoModule } from '@ngneat/transloco';
import { ClaimsService } from '@dsh/api/claims';
import { ShopCreationModule } from '@dsh/app/shared/components/shop-creation';
import { ButtonModule } from '@dsh/components/buttons';
import { IndicatorsModule } from '@dsh/components/indicators';
import { LayoutModule } from '@dsh/components/layout';
@ -38,6 +39,7 @@ import { ClaimsComponent } from './claims.component';
ButtonModule,
ClaimsListModule,
ClaimsSearchFiltersModule,
ShopCreationModule,
],
declarations: [ClaimsComponent],
exports: [ClaimsComponent],

View File

@ -3,10 +3,9 @@ import isNil from 'lodash-es/isNil';
import { BehaviorSubject, combineLatest, Observable, ReplaySubject } from 'rxjs';
import { map, mapTo, pluck, scan, shareReplay, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import { Shop as ApiShop } from '@dsh/api-codegen/capi/swagger-codegen';
import { PaymentInstitutionRealm } from '@dsh/api/model';
import { PaymentInstitution, Shop as ApiShop } from '@dsh/api-codegen/capi/swagger-codegen';
import { ApiShopsService } from '@dsh/api/shop';
import { mapToTimestamp, SHARE_REPLAY_CONF } from '@dsh/operators';
import { mapToTimestamp, shareReplayRefCount } from '@dsh/operators';
import { filterShopsByRealm } from '../../../../operations/operators';
import { ShopBalance } from '../../types/shop-balance';
@ -17,6 +16,8 @@ import { ShopsFiltersStoreService } from '../shops-filters-store/shops-filters-s
import { ShopsFiltersService } from '../shops-filters/shops-filters.service';
import { combineShopItem } from './combine-shop-item';
import RealmEnum = PaymentInstitution.RealmEnum;
const DEFAULT_LIST_PAGINATION_OFFSET = 20;
export const SHOPS_LIST_PAGINATION_OFFSET = new InjectionToken('shops-list-pagination-offset');
@ -32,7 +33,7 @@ export class FetchShopsService {
private selectedIndex$ = new ReplaySubject<number>(1);
private listOffset$: Observable<number>;
private realmData$ = new ReplaySubject<PaymentInstitutionRealm>(1);
private realmData$ = new ReplaySubject<RealmEnum>(1);
private showMore$ = new ReplaySubject<void>(1);
private loader$ = new BehaviorSubject<boolean>(true);
@ -57,7 +58,7 @@ export class FetchShopsService {
this.initFiltersStore();
}
initRealm(realm: PaymentInstitutionRealm): void {
initRealm(realm: RealmEnum): void {
this.realmData$.next(realm);
}
@ -96,10 +97,7 @@ export class FetchShopsService {
}
private initAllShopsFetching(): void {
this.allShops$ = this.realmData$.pipe(
filterShopsByRealm(this.apiShopsService.shops$),
shareReplay(SHARE_REPLAY_CONF)
);
this.allShops$ = this.realmData$.pipe(filterShopsByRealm(this.apiShopsService.shops$), shareReplayRefCount());
}
private initOffsetObservable(): void {
@ -108,7 +106,7 @@ export class FetchShopsService {
withLatestFrom(this.selectedIndex$),
map(([curOffset]: [number, number]) => curOffset),
scan((offset: number, limit: number) => offset + limit, 0),
shareReplay(SHARE_REPLAY_CONF)
shareReplayRefCount()
);
}
@ -132,7 +130,7 @@ export class FetchShopsService {
.pipe(map((balances: ShopBalance[]) => combineShopItem(shops, balances)));
}),
tap(() => this.stopLoading()),
shareReplay(SHARE_REPLAY_CONF)
shareReplayRefCount()
);
}
@ -145,7 +143,7 @@ export class FetchShopsService {
this.shownShops$.pipe(pluck('length')),
]).pipe(
map(([count, showedCount]: [number, number]) => count > showedCount),
shareReplay(SHARE_REPLAY_CONF)
shareReplayRefCount()
);
}

View File

@ -1,46 +0,0 @@
import { Component, Inject } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { ShopType } from '../../../types/shop-type';
import { CreateShopDialogResponse } from '../../create-russian-shop-entity/types/create-shop-dialog-response';
import { CreateShopDialogConfig } from '../../types/create-shop-dialog-config';
@Component({
selector: 'dsh-create-shop-dialog',
templateUrl: 'create-shop-dialog.component.html',
styleUrls: ['create-shop-dialog.component.scss'],
})
export class CreateShopDialogComponent {
selectedShopType: ShopType;
selectionConfirmed = false;
shopType = ShopType;
constructor(
public dialogRef: MatDialogRef<CreateShopDialogComponent, CreateShopDialogResponse>,
@Inject(MAT_DIALOG_DATA) public data: CreateShopDialogConfig,
private router: Router
) {}
onTypeChange(type: ShopType): void {
this.selectedShopType = type;
}
next(): void {
if (this.selectedShopType === ShopType.New) {
this.dialogRef.close();
this.router.navigate(['claim-section', 'onboarding']);
}
this.selectionConfirmed = true;
}
sendClaim(): void {
this.dialogRef.close('send');
}
cancelClaim(): void {
this.dialogRef.close('canceled');
}
}

View File

@ -1,119 +0,0 @@
import { ChangeDetectionStrategy } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { of } from 'rxjs';
import { instance, mock, verify, when } from 'ts-mockito';
import { Shop } from '@dsh/api-codegen/capi';
import { AutocompleteVirtualScrollModule } from '@dsh/app/shared/components/selects/autocomplete-virtual-scroll';
import { BaseOption } from '@dsh/app/shared/components/selects/autocomplete-virtual-scroll/types/base-option';
import { getTranslocoModule } from '@dsh/app/shared/tests/get-transloco-module';
import { generateMockShopsList } from '../../../../tests/generate-mock-shops-list';
import { BANK_SHOP_ID_FIELD } from '../../consts';
import { ShopOptionsSelectionService } from '../../services/shop-options-selection/shop-options-selection.service';
import { ExistingBankAccountComponent } from './existing-bank-account.component';
describe('ExistingBankAccountComponent', () => {
let component: ExistingBankAccountComponent;
let fixture: ComponentFixture<ExistingBankAccountComponent>;
let mockShopOptionsSelectionService: ShopOptionsSelectionService;
const mockShopSelectionControl = new FormControl();
async function makeTestingModule() {
TestBed.resetTestingModule();
await TestBed.configureTestingModule({
imports: [NoopAnimationsModule, ReactiveFormsModule, AutocompleteVirtualScrollModule, getTranslocoModule()],
providers: [
{
provide: ShopOptionsSelectionService,
useFactory: () => instance(mockShopOptionsSelectionService),
},
],
declarations: [ExistingBankAccountComponent],
})
.overrideComponent(ExistingBankAccountComponent, {
set: {
providers: [],
changeDetection: ChangeDetectionStrategy.Default,
},
})
.compileComponents();
fixture = TestBed.createComponent(ExistingBankAccountComponent);
component = fixture.componentInstance;
}
beforeEach(() => {
mockShopOptionsSelectionService = mock(ShopOptionsSelectionService);
when(mockShopOptionsSelectionService.options$).thenReturn(of([]));
when(mockShopOptionsSelectionService.selectedShop$).thenReturn(of());
when(mockShopOptionsSelectionService.control).thenReturn(mockShopSelectionControl);
});
describe('creation', () => {
beforeEach(async () => {
await makeTestingModule();
});
it('should create', () => {
when(mockShopOptionsSelectionService.selectedShop$).thenReturn(of());
component.form = new FormGroup({
[BANK_SHOP_ID_FIELD]: new FormControl(),
});
fixture.detectChanges();
expect(component).toBeTruthy();
});
});
describe('ngOnInit', () => {
let mockShopsList: Shop[];
let mockOptionsList: BaseOption<string>[];
beforeEach(() => {
mockShopsList = generateMockShopsList(15);
mockOptionsList = mockShopsList.map(({ id, details: { name: label } }) => {
return { id, label };
});
when(mockShopOptionsSelectionService.options$).thenReturn(of(mockOptionsList));
});
it('should init innerFormControl with form group value', async () => {
await makeTestingModule();
component.form = new FormGroup({
[BANK_SHOP_ID_FIELD]: new FormControl(mockOptionsList[5].id),
});
fixture.detectChanges();
expect(component.innerShopControl.value).toEqual(mockOptionsList[5]);
});
it('should update form control on every selected change', async () => {
const mockFormControl = mock(FormControl);
when(mockFormControl.value).thenReturn(undefined);
when(mockShopOptionsSelectionService.selectedShop$).thenReturn(of(mockShopsList[2], mockShopsList[5]));
when(mockFormControl.setValue(mockShopsList[2].id)).thenReturn();
when(mockFormControl.setValue(mockShopsList[5].id)).thenReturn();
await makeTestingModule();
component.form = new FormGroup({
[BANK_SHOP_ID_FIELD]: instance(mockFormControl),
});
fixture.detectChanges();
verify(mockFormControl.setValue(mockShopsList[2].id)).once();
verify(mockFormControl.setValue(mockShopsList[5].id)).once();
expect().nothing();
});
});
});

View File

@ -1,63 +0,0 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import isNil from 'lodash-es/isNil';
import { Observable } from 'rxjs';
import { filter, map, take } from 'rxjs/operators';
import { PayoutTool, Shop } from '@dsh/api-codegen/capi';
import { BaseOption } from '@dsh/app/shared/components/selects/autocomplete-virtual-scroll/types/base-option';
import { BANK_SHOP_ID_FIELD } from '../../consts';
import { ShopOptionsSelectionService } from '../../services/shop-options-selection/shop-options-selection.service';
@UntilDestroy()
@Component({
selector: 'dsh-existing-bank-account',
templateUrl: 'existing-bank-account.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [ShopOptionsSelectionService],
})
export class ExistingBankAccountComponent implements OnInit {
@Input() form: FormGroup;
@Input() payoutTool: PayoutTool;
@Input() isLoading: boolean;
@Input() hasError: boolean;
@Input() contentWindow: HTMLElement;
shopsList$: Observable<BaseOption<string>[]> = this.shopOptionsService.options$;
innerShopControl: FormControl = this.shopOptionsService.control;
private get shopControl(): FormControl {
if (isNil(this.form) || isNil(this.form.get(BANK_SHOP_ID_FIELD))) {
throw new Error(`Can't find shop control. FormGroup or FormControl doesn't exist`);
}
return this.form.get(BANK_SHOP_ID_FIELD) as FormControl;
}
constructor(private shopOptionsService: ShopOptionsSelectionService) {}
ngOnInit(): void {
const formShopId = this.shopControl.value as string | undefined;
this.shopsList$
.pipe(
map((shops: BaseOption<string>[]) => {
return shops.find(({ id }: BaseOption<string>) => id === formShopId);
}),
take(1),
filter(Boolean)
)
.subscribe((shop: BaseOption<string>) => {
this.innerShopControl.setValue(shop);
});
this.shopOptionsService.selectedShop$
.pipe(
map((shop: Shop | null) => (isNil(shop) ? '' : shop.id)),
untilDestroyed(this)
)
.subscribe((shopID: string) => {
this.shopControl.setValue(shopID);
});
}
}

View File

@ -1,160 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { instance, mock } from 'ts-mockito';
import { DaDataService as DaDataApiService } from '@dsh/api-codegen/aggr-proxy';
import { getTranslocoModule } from '@dsh/app/shared/tests/get-transloco-module';
import { DaDataModule } from '../../../../../../../../dadata';
import {
NEW_BANK_ACCOUNT_ACCOUNT_FIELD,
NEW_BANK_ACCOUNT_BANK_BIK_FIELD,
NEW_BANK_ACCOUNT_BANK_NAME_FIELD,
NEW_BANK_ACCOUNT_BANK_POST_ACCOUNT_FIELD,
NEW_BANK_ACCOUNT_FIELD,
NEW_BANK_ACCOUNT_SEARCH_FIELD,
} from '../../consts';
import { NewBankAccountComponent } from './new-bank-account.component';
describe('NewBankAccountComponent', () => {
let component: NewBankAccountComponent;
let fixture: ComponentFixture<NewBankAccountComponent>;
let mockDaDataApiService: DaDataApiService;
beforeEach(() => {
mockDaDataApiService = mock(DaDataApiService);
});
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
ReactiveFormsModule,
MatFormFieldModule,
DaDataModule,
NoopAnimationsModule,
getTranslocoModule(),
],
providers: [
{
provide: DaDataApiService,
useFactory: () => instance(mockDaDataApiService),
},
],
declarations: [NewBankAccountComponent],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(NewBankAccountComponent);
component = fixture.componentInstance;
component.form = new FormGroup({
[NEW_BANK_ACCOUNT_FIELD]: new FormGroup({
[NEW_BANK_ACCOUNT_SEARCH_FIELD]: new FormControl(''),
[NEW_BANK_ACCOUNT_BANK_NAME_FIELD]: new FormControl('', [Validators.required]),
[NEW_BANK_ACCOUNT_BANK_BIK_FIELD]: new FormControl('', [Validators.required]),
[NEW_BANK_ACCOUNT_BANK_POST_ACCOUNT_FIELD]: new FormControl('', [Validators.required]),
[NEW_BANK_ACCOUNT_ACCOUNT_FIELD]: new FormControl('', [Validators.required]),
}),
});
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('bankAccountForm', () => {
it('should return form group', () => {
const testFormGroup = new FormGroup({
[NEW_BANK_ACCOUNT_FIELD]: new FormGroup({
[NEW_BANK_ACCOUNT_SEARCH_FIELD]: new FormControl(''),
[NEW_BANK_ACCOUNT_BANK_NAME_FIELD]: new FormControl('', [Validators.required]),
[NEW_BANK_ACCOUNT_BANK_BIK_FIELD]: new FormControl('', [Validators.required]),
[NEW_BANK_ACCOUNT_BANK_POST_ACCOUNT_FIELD]: new FormControl('', [Validators.required]),
[NEW_BANK_ACCOUNT_ACCOUNT_FIELD]: new FormControl('', [Validators.required]),
}),
});
component.form = testFormGroup;
expect(component.bankAccountForm).toEqual(testFormGroup.get(NEW_BANK_ACCOUNT_FIELD) as FormGroup);
});
it(`should throw an error if form wasn't provided`, () => {
component.form = undefined;
expect(() => component.bankAccountForm).toThrowError(`Form wasn't provided`);
});
it(`should throw an error if group doesn't exist on form`, () => {
component.form = new FormGroup({});
expect(() => component.bankAccountForm).toThrowError(
`Form doesn't contains "${NEW_BANK_ACCOUNT_FIELD}" control`
);
});
});
describe('bankAccountNameControl', () => {
it('should return form group', () => {
const testFormControl = new FormControl('my value', [Validators.required]);
component.form = new FormGroup({
[NEW_BANK_ACCOUNT_FIELD]: new FormGroup({
[NEW_BANK_ACCOUNT_SEARCH_FIELD]: new FormControl(''),
[NEW_BANK_ACCOUNT_BANK_NAME_FIELD]: testFormControl,
[NEW_BANK_ACCOUNT_BANK_BIK_FIELD]: new FormControl('', [Validators.required]),
[NEW_BANK_ACCOUNT_BANK_POST_ACCOUNT_FIELD]: new FormControl('', [Validators.required]),
[NEW_BANK_ACCOUNT_ACCOUNT_FIELD]: new FormControl('', [Validators.required]),
}),
});
expect(component.bankAccountNameControl).toEqual(testFormControl);
});
it(`should throw an error if form wasn't provided`, () => {
component.form = undefined;
expect(() => component.bankAccountNameControl).toThrowError(`Form wasn't provided`);
});
it(`should throw an error if control doesn't exist in group`, () => {
component.form = new FormGroup({
[NEW_BANK_ACCOUNT_FIELD]: new FormGroup({
[NEW_BANK_ACCOUNT_SEARCH_FIELD]: new FormControl(''),
[NEW_BANK_ACCOUNT_BANK_BIK_FIELD]: new FormControl('', [Validators.required]),
[NEW_BANK_ACCOUNT_BANK_POST_ACCOUNT_FIELD]: new FormControl('', [Validators.required]),
[NEW_BANK_ACCOUNT_ACCOUNT_FIELD]: new FormControl('', [Validators.required]),
}),
});
expect(() => component.bankAccountNameControl).toThrowError(
`Form doesn't contains "${NEW_BANK_ACCOUNT_FIELD}.${NEW_BANK_ACCOUNT_BANK_NAME_FIELD}" control`
);
});
});
describe('bankSelected', () => {
it('should patch form value', () => {
const spyOnPatchValue = spyOn(component.form, 'patchValue').and.callThrough();
component.bankSelected({
correspondentAccount: 'account',
bic: '0000000000000',
value: 'my name',
});
expect(spyOnPatchValue).toHaveBeenCalledTimes(1);
expect(spyOnPatchValue).toHaveBeenCalledWith(
{
[NEW_BANK_ACCOUNT_FIELD]: {
[NEW_BANK_ACCOUNT_BANK_NAME_FIELD]: 'my name',
[NEW_BANK_ACCOUNT_BANK_BIK_FIELD]: '0000000000000',
[NEW_BANK_ACCOUNT_BANK_POST_ACCOUNT_FIELD]: 'account',
},
},
{ emitEvent: true }
);
});
});
});

View File

@ -1,62 +0,0 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import isNil from 'lodash-es/isNil';
import { BankContent } from '@dsh/api-codegen/aggr-proxy';
import {
NEW_BANK_ACCOUNT_BANK_BIK_FIELD,
NEW_BANK_ACCOUNT_BANK_NAME_FIELD,
NEW_BANK_ACCOUNT_BANK_POST_ACCOUNT_FIELD,
NEW_BANK_ACCOUNT_FIELD,
} from '../../consts';
@Component({
selector: 'dsh-new-bank-account',
templateUrl: './new-bank-account.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NewBankAccountComponent {
@Input() form: FormGroup;
get bankAccountForm(): FormGroup {
this.chekFormProvided();
if (isNil(this.form.get(NEW_BANK_ACCOUNT_FIELD))) {
throw new Error(`Form doesn't contains "${NEW_BANK_ACCOUNT_FIELD}" control`);
}
return this.form.get(NEW_BANK_ACCOUNT_FIELD) as FormGroup;
}
get bankAccountNameControl(): FormControl {
this.chekFormProvided();
if (isNil(this.form.get(`${NEW_BANK_ACCOUNT_FIELD}.${NEW_BANK_ACCOUNT_BANK_NAME_FIELD}`))) {
throw new Error(
`Form doesn't contains "${NEW_BANK_ACCOUNT_FIELD}.${NEW_BANK_ACCOUNT_BANK_NAME_FIELD}" control`
);
}
return this.form.get(`${NEW_BANK_ACCOUNT_FIELD}.${NEW_BANK_ACCOUNT_BANK_NAME_FIELD}`) as FormControl;
}
bankSelected(bank: BankContent): void {
if (bank)
this.form.patchValue(
{
[NEW_BANK_ACCOUNT_FIELD]: {
[NEW_BANK_ACCOUNT_BANK_NAME_FIELD]: bank.value,
[NEW_BANK_ACCOUNT_BANK_BIK_FIELD]: bank.bic,
[NEW_BANK_ACCOUNT_BANK_POST_ACCOUNT_FIELD]: bank.correspondentAccount,
},
},
{
emitEvent: true,
}
);
}
protected chekFormProvided(): void {
if (isNil(this.form)) {
throw new Error(`Form wasn't provided`);
}
}
}

View File

@ -1,75 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormControl } from '@angular/forms';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { of } from 'rxjs';
import { instance, mock, verify, when } from 'ts-mockito';
import { AutocompleteVirtualScrollModule } from '@dsh/app/shared/components/selects/autocomplete-virtual-scroll';
import { getTranslocoModule } from '@dsh/app/shared/tests/get-transloco-module';
import { DetailsItemModule } from '@dsh/components/layout';
import { ShopContractDetailsService } from '../../../../services/shop-contract-details/shop-contract-details.service';
import { generateMockShop } from '../../../../tests/generate-mock-shop';
import { ShopOptionsSelectionService } from '../../services/shop-options-selection/shop-options-selection.service';
import { ShopContractComponent } from './shop-contract.component';
describe('ShopContractComponent', () => {
let component: ShopContractComponent;
let fixture: ComponentFixture<ShopContractComponent>;
let mockShopOptionsSelectionService: ShopOptionsSelectionService;
let mockShopContractDetailsService: ShopContractDetailsService;
const mockShop = generateMockShop(15);
beforeEach(async () => {
mockShopOptionsSelectionService = mock(ShopOptionsSelectionService);
mockShopContractDetailsService = mock(ShopContractDetailsService);
when(mockShopContractDetailsService.shopContract$).thenReturn(of());
when(mockShopContractDetailsService.isLoading$).thenReturn(of());
when(mockShopContractDetailsService.errorOccurred$).thenReturn(of());
when(mockShopOptionsSelectionService.control).thenReturn(new FormControl());
when(mockShopOptionsSelectionService.options$).thenReturn(of([]));
when(mockShopOptionsSelectionService.selectedShop$).thenReturn(of(mockShop));
await TestBed.configureTestingModule({
imports: [getTranslocoModule(), NoopAnimationsModule, AutocompleteVirtualScrollModule, DetailsItemModule],
declarations: [ShopContractComponent],
providers: [
{
provide: ShopOptionsSelectionService,
useFactory: () => instance(mockShopOptionsSelectionService),
},
{
provide: ShopContractDetailsService,
useFactory: () => instance(mockShopContractDetailsService),
},
],
})
.overrideComponent(ShopContractComponent, {
set: {
providers: [],
},
})
.compileComponents();
fixture = TestBed.createComponent(ShopContractComponent);
component = fixture.componentInstance;
const componentControl = new FormControl();
component.control = componentControl;
fixture.detectChanges();
});
describe('creation', () => {
it('should create', () => {
expect(component).toBeTruthy();
});
});
describe('ngOnInit', () => {
it('should request new contract on every selected shop change', () => {
verify(mockShopContractDetailsService.requestContract(mockShop.contractID)).once();
expect().nothing();
});
});
});

View File

@ -1,48 +0,0 @@
import { Component, Input, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { Observable } from 'rxjs';
import { BaseOption } from '@dsh/app/shared/components/selects/autocomplete-virtual-scroll/types/base-option';
import { ShopContractDetailsService } from '../../../../services/shop-contract-details/shop-contract-details.service';
import { ShopOptionsSelectionService } from '../../services/shop-options-selection/shop-options-selection.service';
@UntilDestroy()
@Component({
selector: 'dsh-shop-contract',
templateUrl: 'shop-contract.component.html',
providers: [ShopOptionsSelectionService, ShopContractDetailsService],
})
export class ShopContractComponent implements OnInit {
@Input() control: FormControl;
@Input() contentWindow: HTMLElement;
shopsList$: Observable<BaseOption<string>[]> = this.shopOptionsService.options$;
shopControl: FormControl = this.shopOptionsService.control;
contract$ = this.contractService.shopContract$;
isLoading$ = this.contractService.isLoading$;
hasError$ = this.contractService.errorOccurred$;
constructor(
private shopOptionsService: ShopOptionsSelectionService,
private contractService: ShopContractDetailsService
) {}
ngOnInit(): void {
this.initContractRequests();
this.initContractUpdater();
}
private initContractRequests(): void {
this.shopOptionsService.selectedShop$.pipe(untilDestroyed(this)).subscribe((shop) => {
this.contractService.requestContract(shop?.contractID);
});
}
private initContractUpdater(): void {
this.contract$.pipe(untilDestroyed(this)).subscribe((contract) => {
this.control.setValue(contract);
});
}
}

View File

@ -1,234 +0,0 @@
import { Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { MatDividerModule } from '@angular/material/divider';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatRadioModule } from '@angular/material/radio';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { of } from 'rxjs';
import { instance, mock, when } from 'ts-mockito';
import { AutocompleteVirtualScrollModule } from '@dsh/app/shared/components/selects/autocomplete-virtual-scroll';
import { getTranslocoModule } from '@dsh/app/shared/tests/get-transloco-module';
import { DetailsItemModule } from '@dsh/components/layout';
import { FetchShopsService } from '../../../../services/fetch-shops/fetch-shops.service';
import { ShopContractDetailsService } from '../../../../services/shop-contract-details/shop-contract-details.service';
import {
BANK_ACCOUNT_TYPE_FIELD,
BANK_SHOP_ID_FIELD,
CONTRACT_FORM_FIELD,
NEW_BANK_ACCOUNT_ACCOUNT_FIELD,
NEW_BANK_ACCOUNT_BANK_BIK_FIELD,
NEW_BANK_ACCOUNT_BANK_NAME_FIELD,
NEW_BANK_ACCOUNT_BANK_POST_ACCOUNT_FIELD,
NEW_BANK_ACCOUNT_FIELD,
NEW_BANK_ACCOUNT_SEARCH_FIELD,
} from '../../consts';
import { ShopOptionsSelectionService } from '../../services/shop-options-selection/shop-options-selection.service';
import { BankAccountType } from '../../types/bank-account-type';
import { ShopFormComponent } from './shop-form.component';
@Component({
selector: 'dsh-shop-contract',
template: '<p>Mock Shop Contract</p>',
})
class MockShopContractComponent {}
describe('ShopFormComponent', () => {
let component: ShopFormComponent;
let fixture: ComponentFixture<ShopFormComponent>;
let mockFetchShopsService: FetchShopsService;
let mockShopOptionsSelectionService: ShopOptionsSelectionService;
let mockShopContractDetailsService: ShopContractDetailsService;
beforeEach(() => {
mockFetchShopsService = mock(FetchShopsService);
mockShopOptionsSelectionService = mock(ShopOptionsSelectionService);
mockShopContractDetailsService = mock(ShopContractDetailsService);
});
beforeEach(() => {
when(mockFetchShopsService.allShops$).thenReturn(of([]));
when(mockShopOptionsSelectionService.control).thenReturn(new FormControl());
when(mockShopOptionsSelectionService.options$).thenReturn(of([]));
when(mockShopOptionsSelectionService.selectedShop$).thenReturn(of());
when(mockShopContractDetailsService.shopContract$).thenReturn(of());
});
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
getTranslocoModule(),
ReactiveFormsModule,
MatFormFieldModule,
MatInputModule,
MatDividerModule,
NoopAnimationsModule,
AutocompleteVirtualScrollModule,
DetailsItemModule,
MatRadioModule,
],
declarations: [ShopFormComponent, MockShopContractComponent],
providers: [
{
provide: FetchShopsService,
useFactory: () => instance(mockFetchShopsService),
},
{
provide: ShopOptionsSelectionService,
useFactory: () => instance(mockShopOptionsSelectionService),
},
{
provide: ShopContractDetailsService,
useFactory: () => instance(mockShopContractDetailsService),
},
],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ShopFormComponent);
component = fixture.componentInstance;
component.form = new FormGroup({
url: new FormControl(''),
name: new FormControl(''),
[CONTRACT_FORM_FIELD]: new FormControl(''),
[BANK_ACCOUNT_TYPE_FIELD]: new FormControl(''),
[BANK_SHOP_ID_FIELD]: new FormControl(''),
[NEW_BANK_ACCOUNT_FIELD]: new FormGroup({
[NEW_BANK_ACCOUNT_SEARCH_FIELD]: new FormControl(''),
[NEW_BANK_ACCOUNT_BANK_NAME_FIELD]: new FormControl(''),
[NEW_BANK_ACCOUNT_BANK_BIK_FIELD]: new FormControl(''),
[NEW_BANK_ACCOUNT_BANK_POST_ACCOUNT_FIELD]: new FormControl(''),
[NEW_BANK_ACCOUNT_ACCOUNT_FIELD]: new FormControl(''),
}),
});
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('contractControl', () => {
it('should return contract control', () => {
const contractControl = new FormControl('');
component.form = new FormGroup({
[CONTRACT_FORM_FIELD]: contractControl,
});
expect(component.contractControl).toEqual(contractControl);
});
it(`should throw an error if form doesn't exist`, () => {
component.form = undefined;
expect(() => component.contractControl).toThrowError(`Form wasn't provided`);
});
it(`should throw an error if control doesn't exist on form`, () => {
component.form = new FormGroup({});
expect(() => component.contractControl).toThrowError(
`Form doesn't contains ${CONTRACT_FORM_FIELD} control`
);
});
});
describe('accountType checks', () => {
it(`should throw an error if form doesn't exist`, () => {
component.form = undefined;
expect(() => component.isNewBankAccount).toThrowError(`Form wasn't provided`);
expect(() => component.isExistingBankAccount).toThrowError(`Form wasn't provided`);
});
it(`should throw an error if control doesn't exist on form`, () => {
component.form = new FormGroup({});
expect(() => component.isNewBankAccount).toThrowError(
`Form doesn't contains ${BANK_ACCOUNT_TYPE_FIELD} control`
);
expect(() => component.isExistingBankAccount).toThrowError(
`Form doesn't contains ${BANK_ACCOUNT_TYPE_FIELD} control`
);
});
});
describe('isNewBankAccount', () => {
it('should return true if account type is new', () => {
const bankAccountTypeControl = new FormControl(BankAccountType.New);
component.form = new FormGroup({
[BANK_ACCOUNT_TYPE_FIELD]: bankAccountTypeControl,
});
expect(component.isNewBankAccount).toBe(true);
});
it('should return false if account type is existing', () => {
const bankAccountTypeControl = new FormControl(BankAccountType.Existing);
component.form = new FormGroup({
[BANK_ACCOUNT_TYPE_FIELD]: bankAccountTypeControl,
});
expect(component.isNewBankAccount).toBe(false);
});
});
describe('isExistingBankAccount', () => {
it('should return true if account type is existing', () => {
const bankAccountTypeControl = new FormControl(BankAccountType.Existing);
component.form = new FormGroup({
[BANK_ACCOUNT_TYPE_FIELD]: bankAccountTypeControl,
});
expect(component.isExistingBankAccount).toBe(true);
});
it('should return false if account type is new', () => {
const bankAccountTypeControl = new FormControl(BankAccountType.New);
component.form = new FormGroup({
[BANK_ACCOUNT_TYPE_FIELD]: bankAccountTypeControl,
});
expect(component.isExistingBankAccount).toBe(false);
});
});
describe('ngOnInit', () => {
let accountTypeControl: FormControl;
let newBankAccountControl: FormGroup;
let shopIdControl: FormControl;
beforeEach(() => {
accountTypeControl = component.form.get(BANK_ACCOUNT_TYPE_FIELD) as FormControl;
newBankAccountControl = component.form.get(NEW_BANK_ACCOUNT_FIELD) as FormGroup;
shopIdControl = component.form.get(BANK_SHOP_ID_FIELD) as FormControl;
});
it('should enable newBankAccount and disable bankShopIdControl if account type is new', () => {
accountTypeControl.setValue(BankAccountType.New);
expect(newBankAccountControl.disabled).toBe(false);
expect(shopIdControl.disabled).toBe(true);
});
it('should disable newBankAccount and enable bankShopIdControl if account type is existing', () => {
accountTypeControl.setValue(BankAccountType.Existing);
expect(newBankAccountControl.disabled).toBe(true);
expect(shopIdControl.disabled).toBe(false);
});
it('should disable newBankAccount and disable bankShopIdControl by default', () => {
accountTypeControl.setValue('');
expect(newBankAccountControl.disabled).toBe(true);
expect(shopIdControl.disabled).toBe(true);
});
});
});

View File

@ -1,10 +0,0 @@
// TODO: add typed form control lib to remove all of this constants
export const CONTRACT_FORM_FIELD = 'contract';
export const BANK_SHOP_ID_FIELD = 'bankShopID';
export const BANK_ACCOUNT_TYPE_FIELD = 'bankAccountType';
export const NEW_BANK_ACCOUNT_FIELD = 'newBankAccount';
export const NEW_BANK_ACCOUNT_SEARCH_FIELD = 'search';
export const NEW_BANK_ACCOUNT_BANK_NAME_FIELD = 'bankName';
export const NEW_BANK_ACCOUNT_BANK_BIK_FIELD = 'bankBik';
export const NEW_BANK_ACCOUNT_BANK_POST_ACCOUNT_FIELD = 'bankPostAccount';
export const NEW_BANK_ACCOUNT_ACCOUNT_FIELD = 'account';

View File

@ -1,112 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { cold } from 'jasmine-marbles';
import { of } from 'rxjs';
import { instance, mock, when } from 'ts-mockito';
import { FetchShopsService } from '../../../../services/fetch-shops/fetch-shops.service';
import { generateMockShopsList } from '../../../../tests/generate-mock-shops-list';
import { ShopOptionsSelectionService } from './shop-options-selection.service';
describe('ShopSelectorService', () => {
let service: ShopOptionsSelectionService;
let mockFetchShopsService: FetchShopsService;
const mainConfig = {
providers: [
ShopOptionsSelectionService,
{
provide: FetchShopsService,
useFactory: () => instance(mockFetchShopsService),
},
],
};
function configureTestingModule() {
TestBed.configureTestingModule(mainConfig);
service = TestBed.inject(ShopOptionsSelectionService);
}
beforeEach(() => {
mockFetchShopsService = mock(FetchShopsService);
when(mockFetchShopsService.allShops$).thenReturn(of([]));
});
describe('creation', () => {
beforeEach(() => {
configureTestingModule();
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
describe('options$', () => {
it('should map Shop objects in BaseOption', () => {
when(mockFetchShopsService.allShops$).thenReturn(of(generateMockShopsList(3)));
configureTestingModule();
const expected$ = cold('(a|)', {
a: [
{
id: 'mock_shop_1',
label: 'my name',
},
{
id: 'mock_shop_2',
label: 'my name',
},
{
id: 'mock_shop_3',
label: 'my name',
},
],
});
expect(service.options$).toBeObservable(expected$);
});
});
describe('selectedShop$', () => {
let expected$;
afterEach(() => {
expect(service.selectedShop$).toBeObservable(expected$);
});
it('should update selected value with found shop item', () => {
const mockList = generateMockShopsList(5);
when(mockFetchShopsService.allShops$).thenReturn(of(mockList));
configureTestingModule();
service.control.setValue({
id: 'mock_shop_2',
label: 'my name',
});
expected$ = cold('a', {
a: mockList[1],
});
});
it('should set selected value with nullable value if shop id was not found in shops list', () => {
const mockList = generateMockShopsList(5);
when(mockFetchShopsService.allShops$).thenReturn(of(mockList));
configureTestingModule();
service.control.setValue({
id: 'mock_shop_12',
label: 'my name',
});
expected$ = cold('a', {
a: null,
});
});
});
//
});

View File

@ -1,61 +0,0 @@
import { Injectable } from '@angular/core';
import { FormControl } from '@angular/forms';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import isNil from 'lodash-es/isNil';
import { Observable, ReplaySubject } from 'rxjs';
import { map, shareReplay, withLatestFrom } from 'rxjs/operators';
import { Shop } from '@dsh/api-codegen/capi';
import { BaseOption } from '@dsh/app/shared/components/selects/autocomplete-virtual-scroll/types/base-option';
import { SHARE_REPLAY_CONF } from '@dsh/operators';
import { FetchShopsService } from '../../../../services/fetch-shops/fetch-shops.service';
@UntilDestroy()
@Injectable()
export class ShopOptionsSelectionService {
control = new FormControl();
options$: Observable<BaseOption<string>[]>;
selectedShop$: Observable<Shop | null>;
private innerSelectedShop$ = new ReplaySubject<Shop>(1);
constructor(private shopsService: FetchShopsService) {
this.initShopOptions();
this.initSelectedShop();
}
private initShopOptions(): void {
this.options$ = this.shopsService.allShops$.pipe(
map((shopsList: Shop[]) => {
return shopsList.map((shop: Shop) => {
return {
id: shop.id,
label: shop.details.name,
};
});
}),
shareReplay(SHARE_REPLAY_CONF)
);
}
private initSelectedShop(): void {
this.control.valueChanges
.pipe(
withLatestFrom(this.shopsService.allShops$),
map(([selected, shopsList]: [BaseOption<string> | null, Shop[]]) => {
if (isNil(selected)) {
return null;
}
return shopsList.find((shop: Shop) => shop.id === selected.id);
}),
map((shop: Shop | undefined | null) => (isNil(shop) ? null : shop)),
untilDestroyed(this)
)
.subscribe((selectedShop: Shop | null) => {
this.innerSelectedShop$.next(selectedShop);
});
this.selectedShop$ = this.innerSelectedShop$.asObservable();
}
}

View File

@ -1 +0,0 @@
export type CreateShopDialogResponse = 'send' | 'canceled';

View File

@ -1 +0,0 @@
export * from './shop-creation.module';

View File

@ -1,109 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { TranslocoService } from '@ngneat/transloco';
import { Observable, of } from 'rxjs';
import { deepEqual, instance, mock, verify, when } from 'ts-mockito';
import { PaymentInstitutionRealm } from '@dsh/api/model/payment-institution-realm';
import { getTranslocoModule } from '@dsh/app/shared/tests/get-transloco-module';
import { CreateShopDialogComponent } from './components/create-shop-dialog/create-shop-dialog.component';
import { CreateShopDialogResponse } from './create-russian-shop-entity/types/create-shop-dialog-response';
import { ShopCreationService } from './shop-creation.service';
describe('CreateShopService', () => {
let service: ShopCreationService;
let mockMatDialog: MatDialog;
let mockMatDialogRef: MatDialogRef<CreateShopDialogComponent>;
let mockMatSnackBar: MatSnackBar;
let transloco: TranslocoService;
beforeEach(() => {
mockMatDialog = mock(MatDialog);
mockMatDialogRef = mock<MatDialogRef<CreateShopDialogComponent>>(MatDialogRef);
mockMatSnackBar = mock(MatSnackBar);
});
beforeEach(() => {
TestBed.configureTestingModule({
imports: [getTranslocoModule()],
providers: [
ShopCreationService,
{
provide: MatDialog,
useFactory: () => instance(mockMatDialog),
},
{
provide: MatSnackBar,
useFactory: () => instance(mockMatSnackBar),
},
],
});
service = TestBed.inject(ShopCreationService);
transloco = TestBed.inject(TranslocoService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
describe('createShop', () => {
beforeEach(() => {
when(
mockMatDialog.open(
CreateShopDialogComponent,
deepEqual({
data: {
realm: PaymentInstitutionRealm.Test,
},
})
)
).thenReturn(instance(mockMatDialogRef));
when(mockMatDialogRef.afterClosed()).thenReturn(of('canceled') as Observable<CreateShopDialogResponse>);
});
afterEach(() => {
expect().nothing();
});
it('should open dialog CreateShopDialogComponent with config', () => {
service.createShop({ realm: PaymentInstitutionRealm.Test });
verify(
mockMatDialog.open(
CreateShopDialogComponent,
deepEqual({
data: {
realm: PaymentInstitutionRealm.Test,
},
})
)
).once();
});
it('should open snackbar on send response from dialog', () => {
when(mockMatDialogRef.afterClosed()).thenReturn(of('send') as Observable<CreateShopDialogResponse>);
when(
mockMatSnackBar.open(transloco.translate('russianLegalEntity.created', null, 'create-shop'), 'OK')
).thenReturn(null);
service.createShop({ realm: PaymentInstitutionRealm.Test });
verify(
mockMatSnackBar.open(transloco.translate('russianLegalEntity.created', null, 'create-shop'), 'OK')
).once();
});
it('should do not open snackbar on canceled response from dialog', () => {
when(
mockMatSnackBar.open(transloco.translate('russianLegalEntity.created', null, 'create-shop'), 'OK')
).thenReturn(null);
service.createShop({ realm: PaymentInstitutionRealm.Test });
verify(
mockMatSnackBar.open(transloco.translate('russianLegalEntity.created', null, 'create-shop'), 'OK')
).never();
});
});
});

View File

@ -1,27 +0,0 @@
import { Injectable } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { TranslocoService } from '@ngneat/transloco';
import { filter, take } from 'rxjs/operators';
import { CreateShopDialogComponent } from './components/create-shop-dialog/create-shop-dialog.component';
import { CreateShopDialogResponse } from './create-russian-shop-entity/types/create-shop-dialog-response';
import { CreateShopDialogConfig } from './types/create-shop-dialog-config';
@Injectable()
export class ShopCreationService {
constructor(private dialog: MatDialog, private transloco: TranslocoService, private snackBar: MatSnackBar) {}
createShop(config: CreateShopDialogConfig): void {
this.dialog
.open<CreateShopDialogComponent, CreateShopDialogConfig>(CreateShopDialogComponent, { data: config })
.afterClosed()
.pipe(
take(1),
filter((response: CreateShopDialogResponse) => response === 'send')
)
.subscribe(() => {
this.snackBar.open(this.transloco.translate('russianLegalEntity.created', null, 'create-shop'), 'OK');
});
}
}

View File

@ -1,3 +0,0 @@
export interface CreateShopDialogConfig {
realm: string;
}

View File

@ -3,9 +3,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { of } from 'rxjs';
import { instance, mock, verify, when } from 'ts-mockito';
import { ShopContractDetailsService } from '@dsh/app/shared/services/shop-contract-details';
import { getTranslocoModule } from '@dsh/app/shared/tests/get-transloco-module';
import { ShopContractDetailsService } from '../../../../services/shop-contract-details/shop-contract-details.service';
import { ShopContractDetailsComponent } from './shop-contract-details.component';
describe('ShopContractDetailsComponent', () => {

View File

@ -1,12 +1,11 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { ShopContractDetailsService } from '../../../../services/shop-contract-details/shop-contract-details.service';
import { ShopContractDetailsService } from '@dsh/app/shared/services/shop-contract-details';
@Component({
selector: 'dsh-shop-contract-details',
templateUrl: 'shop-contract-details.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [ShopContractDetailsService],
})
export class ShopContractDetailsComponent {
@Input()

View File

@ -11,10 +11,10 @@ import { instance, mock, when } from 'ts-mockito';
import { CategoriesModule } from '@dsh/api/categories';
import { ContractsModule } from '@dsh/api/contracts';
import { ContractDetailsModule, PayoutToolModule } from '@dsh/app/shared/components';
import { ShopContractDetailsService } from '@dsh/app/shared/services/shop-contract-details';
import { ButtonModule } from '@dsh/components/buttons';
import { DetailsItemModule } from '@dsh/components/layout';
import { ShopContractDetailsService } from '../../services/shop-contract-details/shop-contract-details.service';
import { ShopPayoutToolDetailsService } from '../../services/shop-payout-tool-details/shop-payout-tool-details.service';
import { generateMockShopItem } from '../../tests/generate-shop-item';
import { ShopBalanceModule } from '../shop-balance';

View File

@ -10,6 +10,7 @@ import { TranslocoModule } from '@ngneat/transloco';
import { CategoriesModule } from '@dsh/api/categories';
import { ContractsModule } from '@dsh/api/contracts';
import { ContractDetailsModule, PayoutToolModule } from '@dsh/app/shared/components';
import { ShopContractDetailsModule } from '@dsh/app/shared/services/shop-contract-details';
import { ButtonModule } from '@dsh/components/buttons';
import { DetailsItemModule } from '@dsh/components/layout';
@ -38,6 +39,7 @@ import { ShopDetailsComponent } from './shop-details.component';
ContractsModule,
MatSnackBarModule,
MatDialogModule,
ShopContractDetailsModule,
],
declarations: [
ShopDetailsComponent,

View File

@ -0,0 +1,4 @@
:host {
display: block;
width: 100%;
}

View File

@ -2,25 +2,30 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FlexLayoutModule } from '@angular/flex-layout';
import { MatRadioModule } from '@angular/material/radio';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ActivatedRoute, RouterModule } from '@angular/router';
import { RouterModule } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { TranslocoTestingModule } from '@ngneat/transloco';
import { of } from 'rxjs';
import { deepEqual, instance, mock, verify, when } from 'ts-mockito';
import { anything, instance, mock, verify, when } from 'ts-mockito';
import { PaymentInstitutionRealm } from '@dsh/api/model';
import { PaymentInstitution } from '@dsh/api-codegen/capi';
import { ShopCreationService } from '@dsh/app/shared/components/shop-creation';
import { provideMockService } from '@dsh/app/shared/tests';
import { ButtonModule } from '@dsh/components/buttons';
import { PaymentInstitutionRealmService } from '../../services/payment-institution-realm/payment-institution-realm.service';
import { RealmShopsService } from '../../services/realm-shops/realm-shops.service';
import { FetchShopsService } from './services/fetch-shops/fetch-shops.service';
import { ShopsBalanceService } from './services/shops-balance/shops-balance.service';
import { ShopsFiltersStoreService } from './services/shops-filters-store/shops-filters-store.service';
import { ShopsFiltersService } from './services/shops-filters/shops-filters.service';
import { ShopCreationService } from './shop-creation/shop-creation.service';
import { ShopFiltersModule } from './shop-filters';
import { ShopsExpandedIdManagerService } from './shops-list/services/shops-expanded-id-manager/shops-expanded-id-manager.service';
import { ShopListModule } from './shops-list/shop-list.module';
import { ShopsComponent } from './shops.component';
import RealmEnum = PaymentInstitution.RealmEnum;
describe('ShopsComponent', () => {
let component: ShopsComponent;
let fixture: ComponentFixture<ShopsComponent>;
@ -29,8 +34,9 @@ describe('ShopsComponent', () => {
let mockShopsBalanceService: ShopsBalanceService;
let mockShopsFiltersService: ShopsFiltersService;
let mockShopsFiltersStoreService: ShopsFiltersStoreService;
let mockActivatedRoute: ActivatedRoute;
let mockShopCreationService: ShopCreationService;
let mockRealmShopsService: RealmShopsService;
let mockRealmService: PaymentInstitutionRealmService;
beforeEach(() => {
mockFetchShopsService = mock(FetchShopsService);
@ -38,8 +44,9 @@ describe('ShopsComponent', () => {
mockShopsBalanceService = mock(ShopsBalanceService);
mockShopsFiltersService = mock(ShopsFiltersService);
mockShopsFiltersStoreService = mock(ShopsFiltersStoreService);
mockActivatedRoute = mock(ActivatedRoute);
mockShopCreationService = mock(ShopCreationService);
mockRealmShopsService = mock(RealmShopsService);
mockRealmService = mock(PaymentInstitutionRealmService);
});
beforeEach(async () => {
@ -47,6 +54,7 @@ describe('ShopsComponent', () => {
when(mockFetchShopsService.shownShops$).thenReturn(of([]));
when(mockFetchShopsService.lastUpdated$).thenReturn(of(''));
when(mockFetchShopsService.hasMore$).thenReturn(of(false));
when(mockRealmService.realm$).thenReturn(of(RealmEnum.Test));
await TestBed.configureTestingModule({
imports: [
NoopAnimationsModule,
@ -82,10 +90,6 @@ describe('ShopsComponent', () => {
provide: ShopCreationService,
useFactory: () => instance(mockShopCreationService),
},
{
provide: ActivatedRoute,
useFactory: () => instance(mockActivatedRoute),
},
{
provide: ShopsBalanceService,
useFactory: () => instance(mockShopsBalanceService),
@ -98,6 +102,8 @@ describe('ShopsComponent', () => {
provide: ShopsFiltersStoreService,
useFactory: () => instance(mockShopsFiltersStoreService),
},
provideMockService(RealmShopsService, mockRealmShopsService),
provideMockService(PaymentInstitutionRealmService, mockRealmService),
],
})
.overrideComponent(ShopsComponent, {
@ -114,7 +120,6 @@ describe('ShopsComponent', () => {
});
beforeEach(() => {
when(mockActivatedRoute.params).thenReturn(of({ realm: PaymentInstitutionRealm.Test }));
when(mockShopsExpandedIdManagerService.expandedId$).thenReturn(of(-1));
when(mockShopsFiltersStoreService.data$).thenReturn(of({}));
});
@ -127,12 +132,12 @@ describe('ShopsComponent', () => {
describe('ngOnInit', () => {
it('should use realm data to init FetchShopsService', () => {
when(mockActivatedRoute.params).thenReturn(of({ realm: PaymentInstitutionRealm.Live }));
when(mockFetchShopsService.initRealm(PaymentInstitutionRealm.Live)).thenReturn(null);
when(mockRealmService.realm$).thenReturn(of(RealmEnum.Live));
when(mockFetchShopsService.initRealm(RealmEnum.Live)).thenReturn(null);
fixture.detectChanges();
verify(mockFetchShopsService.initRealm(PaymentInstitutionRealm.Live)).once();
verify(mockFetchShopsService.initRealm(RealmEnum.Live)).once();
expect().nothing();
});
@ -173,14 +178,9 @@ describe('ShopsComponent', () => {
describe('createShop', () => {
it('should call create shop with activated route realm', () => {
when(mockActivatedRoute.snapshot).thenReturn({ params: { realm: PaymentInstitutionRealm.Live } } as any);
when(mockShopCreationService.createShop(deepEqual({ realm: PaymentInstitutionRealm.Live }))).thenReturn(
null
);
component.createShop();
verify(mockShopCreationService.createShop(deepEqual({ realm: PaymentInstitutionRealm.Live }))).once();
verify(mockShopCreationService.createShop(anything())).once();
expect().nothing();
});
});

View File

@ -1,24 +1,17 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { pluck, take } from 'rxjs/operators';
import { take } from 'rxjs/operators';
import { PaymentInstitutionRealm } from '@dsh/api/model';
import { ShopCreationService } from '@dsh/app/shared/components/shop-creation';
import { PaymentInstitutionRealmService } from '../../services/payment-institution-realm/payment-institution-realm.service';
import { RealmShopsService } from '../../services/realm-shops/realm-shops.service';
import { FetchShopsService } from './services/fetch-shops/fetch-shops.service';
import { ShopCreationService } from './shop-creation/shop-creation.service';
import { ShopsExpandedIdManagerService } from './shops-list/services/shops-expanded-id-manager/shops-expanded-id-manager.service';
@Component({
selector: 'dsh-shops',
templateUrl: 'shops.component.html',
styles: [
`
:host {
display: block;
width: 100%;
}
`,
],
styleUrls: ['shops.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ShopsComponent implements OnInit {
@ -31,11 +24,12 @@ export class ShopsComponent implements OnInit {
private shopsService: FetchShopsService,
private expandedIdManager: ShopsExpandedIdManagerService,
private createShopService: ShopCreationService,
private route: ActivatedRoute
private realmShopsService: RealmShopsService,
private realmService: PaymentInstitutionRealmService
) {}
ngOnInit(): void {
this.route.params.pipe(take(1), pluck('realm')).subscribe((realm: PaymentInstitutionRealm) => {
this.realmService.realm$.pipe(take(1)).subscribe((realm) => {
this.shopsService.initRealm(realm);
});
this.expandedIdManager.expandedId$.pipe(take(1)).subscribe((offsetIndex: number) => {
@ -44,9 +38,7 @@ export class ShopsComponent implements OnInit {
}
createShop(): void {
this.createShopService.createShop({
realm: this.route.snapshot.params.realm,
});
this.createShopService.createShop({ shops$: this.realmShopsService.shops$ });
}
refreshData(): void {

View File

@ -4,13 +4,13 @@ import { FlexLayoutModule } from '@angular/flex-layout';
import { RouterModule } from '@angular/router';
import { TranslocoModule } from '@ngneat/transloco';
import { ShopCreationModule } from '@dsh/app/shared/components/shop-creation';
import { ButtonModule } from '@dsh/components/buttons';
import { FetchShopsService } from './services/fetch-shops/fetch-shops.service';
import { ShopsBalanceService } from './services/shops-balance/shops-balance.service';
import { ShopsFiltersStoreService } from './services/shops-filters-store/shops-filters-store.service';
import { ShopsFiltersService } from './services/shops-filters/shops-filters.service';
import { ShopCreationModule } from './shop-creation';
import { ShopFiltersModule } from './shop-filters';
import { ShopsExpandedIdManagerService } from './shops-list/services/shops-expanded-id-manager/shops-expanded-id-manager.service';
import { ShopListModule } from './shops-list/shop-list.module';

View File

@ -1,9 +1,10 @@
import { Component } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { filter, map } from 'rxjs/operators';
import { PaymentInstitution } from '@dsh/api-codegen/capi';
import { SHOPS } from '@dsh/app/shared/components/inputs/shop-field';
import { PaymentInstitutionRealmService } from './services/payment-institution-realm/payment-institution-realm.service';
import { RealmShopsService } from './services/realm-shops/realm-shops.service';
@ -12,23 +13,34 @@ import { RealmShopsService } from './services/realm-shops/realm-shops.service';
@Component({
templateUrl: 'payment-section.component.html',
styleUrls: ['payment-section.scss'],
providers: [PaymentInstitutionRealmService, RealmShopsService],
providers: [
PaymentInstitutionRealmService,
RealmShopsService,
{
provide: SHOPS,
deps: [RealmShopsService],
useFactory: (realmShopsService: RealmShopsService) => realmShopsService.shops$,
},
],
})
export class PaymentSectionComponent {
export class PaymentSectionComponent implements OnInit {
isTestRealm$ = this.realmService.realm$.pipe(map((realm) => realm === PaymentInstitution.RealmEnum.Test));
constructor(
private realmService: PaymentInstitutionRealmService,
private router: Router,
private route: ActivatedRoute
) {
) {}
ngOnInit(): void {
this.realmService.realm$
.pipe(
filter((realm) => !realm),
untilDestroyed(this)
)
.subscribe(
() => void this.router.navigate(['realm', PaymentInstitution.RealmEnum.Live], { relativeTo: route })
() =>
void this.router.navigate(['realm', PaymentInstitution.RealmEnum.Live], { relativeTo: this.route })
);
}
}

View File

@ -4,16 +4,16 @@ import { map } from 'rxjs/operators';
import { ApiShopsService } from '@dsh/api';
import { Shop } from '@dsh/api-codegen/capi';
import { publishReplayRefCount } from '@dsh/operators';
import { shareReplayRefCount } from '@dsh/operators';
import { getShopsByRealm } from '../../operations/operators';
import { PaymentInstitutionRealmService } from '../payment-institution-realm/payment-institution-realm.service';
@Injectable()
export class RealmShopsService {
shops$: Observable<Shop[]> = combineLatest(this.realmService.realm$, this.shopsService.shops$).pipe(
map(([realm, shops]) => getShopsByRealm(shops, realm)),
publishReplayRefCount()
shops$: Observable<Shop[]> = combineLatest(this.shopsService.shops$, this.realmService.realm$).pipe(
map(([shops, realm]) => getShopsByRealm(shops, realm)),
shareReplayRefCount()
);
constructor(private shopsService: ApiShopsService, private realmService: PaymentInstitutionRealmService) {}

View File

@ -1,3 +1,2 @@
export * from './filters';
export * from './api-model-details';
export * from './selects';

View File

@ -1,6 +1,6 @@
<dsh-autocomplete-field
<dsh-select-search-field
[label]="label"
[required]="required"
[options]="options$ | async"
[formControl]="formControl"
></dsh-autocomplete-field>
></dsh-select-search-field>

View File

@ -1,11 +1,12 @@
import { ChangeDetectionStrategy, Component, Injector, Input } from '@angular/core';
import { provideValueAccessor, WrappedFormControlSuperclass } from '@s-libs/ng-core';
import { Observable } from 'rxjs';
import { map, share } from 'rxjs/operators';
import { map } from 'rxjs/operators';
import { CategoriesService } from '@dsh/api';
import { Category } from '@dsh/api-codegen/capi';
import { Option } from '@dsh/components/form-controls/autocomplete-field';
import { Option } from '@dsh/components/form-controls/select-search-field';
import { shareReplayRefCount } from '@dsh/operators';
import { coerceBoolean } from '@dsh/utils';
@Component({
@ -22,7 +23,7 @@ export class CategoryAutocompleteFieldComponent extends WrappedFormControlSuperc
map((categories) =>
categories.map((category) => ({ label: `${category.categoryID} - ${category.name}`, value: category }))
),
share()
shareReplayRefCount()
);
constructor(injector: Injector, private categoriesService: CategoriesService) {

View File

@ -4,12 +4,20 @@ import { ReactiveFormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { AutocompleteFieldModule } from '@dsh/components/form-controls/autocomplete-field';
import { CategoriesModule } from '@dsh/api';
import { SelectSearchFieldModule } from '@dsh/components/form-controls/select-search-field';
import { CategoryAutocompleteFieldComponent } from './category-autocomplete-field.component';
@NgModule({
imports: [CommonModule, MatInputModule, MatFormFieldModule, ReactiveFormsModule, AutocompleteFieldModule],
imports: [
CommonModule,
MatInputModule,
MatFormFieldModule,
ReactiveFormsModule,
SelectSearchFieldModule,
CategoriesModule,
],
declarations: [CategoryAutocompleteFieldComponent],
exports: [CategoryAutocompleteFieldComponent],
})

View File

@ -1,7 +1,7 @@
<dsh-autocomplete-field
<dsh-select-search-field
*ngIf="countries$ | async"
[label]="label"
[required]="required"
[options]="options$ | async"
[formControl]="formControl"
></dsh-autocomplete-field>
></dsh-select-search-field>

View File

@ -5,7 +5,7 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { CountriesModule } from '@dsh/api';
import { AutocompleteFieldModule } from '@dsh/components/form-controls/autocomplete-field';
import { SelectSearchFieldModule } from '@dsh/components/form-controls/select-search-field';
import { CountryAutocompleteFieldComponent } from './countries-autocomplete-field.component';
@ -15,7 +15,7 @@ import { CountryAutocompleteFieldComponent } from './countries-autocomplete-fiel
MatInputModule,
MatFormFieldModule,
ReactiveFormsModule,
AutocompleteFieldModule,
SelectSearchFieldModule,
CountriesModule,
],
declarations: [CountryAutocompleteFieldComponent],

View File

@ -1,5 +1,5 @@
import { Country } from '@dsh/api-codegen/capi';
import { Option } from '@dsh/components/form-controls/autocomplete-field';
import { Option } from '@dsh/components/form-controls/select-search-field';
const countryToOption = (country: Country): Option<string> => ({
label: `${country?.id} - ${country?.name}`,

View File

@ -1,6 +1,6 @@
<dsh-autocomplete-field
<dsh-select-search-field
[label]="label"
[required]="required"
[options]="options$ | async"
[formControl]="formControl"
></dsh-autocomplete-field>
></dsh-select-search-field>

View File

@ -5,7 +5,7 @@ import { map, share } from 'rxjs/operators';
import { PaymentInstitution } from '@dsh/api-codegen/capi';
import { PaymentInstitutionsService } from '@dsh/api/payment-institutions';
import { Option } from '@dsh/components/form-controls/autocomplete-field';
import { Option } from '@dsh/components/form-controls/select-search-field';
import { coerceBoolean } from '@dsh/utils';
@Component({

View File

@ -4,12 +4,12 @@ import { ReactiveFormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { AutocompleteFieldModule } from '@dsh/components/form-controls/autocomplete-field';
import { SelectSearchFieldModule } from '@dsh/components/form-controls/select-search-field';
import { PaymentInstitutionAutocompleteFieldComponent } from './payment-institution-autocomplete-field.component';
@NgModule({
imports: [CommonModule, MatInputModule, MatFormFieldModule, ReactiveFormsModule, AutocompleteFieldModule],
imports: [CommonModule, MatInputModule, MatFormFieldModule, ReactiveFormsModule, SelectSearchFieldModule],
declarations: [PaymentInstitutionAutocompleteFieldComponent],
exports: [PaymentInstitutionAutocompleteFieldComponent],
})

View File

@ -1,2 +0,0 @@
export * from './shop-autocomplete-field.module';
export * from './shop-autocomplete-field.component';

View File

@ -1,27 +0,0 @@
import { ChangeDetectionStrategy, Component, Injector, Input } from '@angular/core';
import { provideValueAccessor, WrappedFormControlSuperclass } from '@s-libs/ng-core';
import { map } from 'rxjs/operators';
import { ApiShopsService } from '@dsh/api';
import { coerceBoolean } from '@dsh/utils';
import { ShopId } from './types';
import { shopsToOptions } from './utils';
@Component({
selector: 'dsh-shop-autocomplete-field',
templateUrl: 'shop-autocomplete-field.component.html',
providers: [provideValueAccessor(ShopAutocompleteFieldComponent)],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ShopAutocompleteFieldComponent extends WrappedFormControlSuperclass<ShopId> {
@Input() label: string;
@Input() @coerceBoolean required = false;
shops$ = this.apiShopsService.shops$;
options$ = this.shops$.pipe(map(shopsToOptions));
constructor(injector: Injector, private apiShopsService: ApiShopsService) {
super(injector);
}
}

View File

@ -1,5 +0,0 @@
import { Shop } from '@dsh/api-codegen/capi';
export type ShopId = Shop['id'];
export type ShopName = Shop['details']['name'];
export type DisplayWithFn = (value: ShopId) => string;

View File

@ -1 +0,0 @@
export * from './shops-to-options';

View File

@ -1,11 +0,0 @@
import { Shop } from '@dsh/api-codegen/capi';
import { Option } from '@dsh/components/form-controls/autocomplete-field';
import { ShopId } from '../types';
const shopToOption = (shop: Shop): Option<ShopId> => ({
label: shop?.details?.name,
value: shop?.id,
});
export const shopsToOptions = (shops: Shop[]): Option<ShopId>[] => shops.map(shopToOption);

View File

@ -0,0 +1,3 @@
export * from './shop-field.module';
export * from './shop-field.component';
export * from './shops-token';

View File

@ -1,7 +1,6 @@
<dsh-autocomplete-field
*ngIf="shops$ | async"
<dsh-select-search-field
[label]="label"
[required]="required"
[options]="options$ | async"
[formControl]="formControl"
></dsh-autocomplete-field>
></dsh-select-search-field>

View File

@ -0,0 +1,41 @@
import { ChangeDetectionStrategy, Component, Inject, Injector, Input, Optional } from '@angular/core';
import { provideValueAccessor, WrappedFormControlSuperclass } from '@s-libs/ng-core';
import { defer, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ApiShopsService, toLiveShops } from '@dsh/api';
import { Shop } from '@dsh/api-codegen/capi';
import { shopToOption } from '@dsh/app/shared/components/inputs/shop-field/utils/shops-to-options';
import { Option } from '@dsh/components/form-controls/select-search-field';
import { shareReplayRefCount } from '@dsh/operators';
import { coerceBoolean } from '@dsh/utils';
import { SHOPS } from './shops-token';
@Component({
selector: 'dsh-shop-field',
templateUrl: 'shop-field.component.html',
providers: [provideValueAccessor(ShopFieldComponent)],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ShopFieldComponent extends WrappedFormControlSuperclass<Shop> {
@Input() label: string;
@Input() @coerceBoolean required = false;
options$: Observable<Option<Shop>[]> = defer(
() => this.shops$ || this.shopsService.shops$.pipe(map(toLiveShops))
).pipe(
map((shops) => shops.map(shopToOption)),
shareReplayRefCount()
);
constructor(
injector: Injector,
private shopsService: ApiShopsService,
@Inject(SHOPS)
@Optional()
private shops$?: Observable<Shop[]>
) {
super(injector);
}
}

View File

@ -5,9 +5,9 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { ShopModule } from '@dsh/api';
import { AutocompleteFieldModule } from '@dsh/components/form-controls/autocomplete-field';
import { SelectSearchFieldModule } from '@dsh/components/form-controls/select-search-field';
import { ShopAutocompleteFieldComponent } from './shop-autocomplete-field.component';
import { ShopFieldComponent } from './shop-field.component';
@NgModule({
imports: [
@ -15,10 +15,10 @@ import { ShopAutocompleteFieldComponent } from './shop-autocomplete-field.compon
MatInputModule,
MatFormFieldModule,
ReactiveFormsModule,
AutocompleteFieldModule,
SelectSearchFieldModule,
ShopModule,
],
declarations: [ShopAutocompleteFieldComponent],
exports: [ShopAutocompleteFieldComponent],
declarations: [ShopFieldComponent],
exports: [ShopFieldComponent],
})
export class ShopAutocompleteFieldModule {}
export class ShopFieldModule {}

View File

@ -0,0 +1,6 @@
import { InjectionToken } from '@angular/core';
import { Observable } from 'rxjs';
import { Shop } from '@dsh/api-codegen/capi';
export const SHOPS = new InjectionToken<Observable<Shop[]>>('Shops');

View File

@ -0,0 +1,7 @@
import { Shop } from '@dsh/api-codegen/capi';
import { Option } from '@dsh/components/form-controls/select-search-field';
export const shopToOption = (shop: Shop): Option<Shop> => ({
label: shop?.details?.name,
value: shop,
});

View File

@ -2,10 +2,10 @@ import { Component, Injector, Input, OnChanges } from '@angular/core';
import { ComponentChanges } from '@rbkmoney/utils';
import { provideValueAccessor, WrappedFormControlSuperclass } from '@s-libs/ng-core';
import { defer, ReplaySubject } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators';
import { map } from 'rxjs/operators';
import { Shop } from '@dsh/api-codegen/capi';
import { SHARE_REPLAY_CONF } from '@dsh/operators';
import { shareReplayRefCount } from '@dsh/operators';
@Component({
selector: 'dsh-shops-field',
@ -17,7 +17,7 @@ export class ShopsFieldComponent extends WrappedFormControlSuperclass<Shop['id']
options$ = defer(() => this.shops$).pipe(
map((shops) => shops.map((shop) => ({ value: shop.id, label: shop.details.name }))),
shareReplay(SHARE_REPLAY_CONF)
shareReplayRefCount()
);
private shops$ = new ReplaySubject<Shop[]>();

View File

@ -1,5 +1,5 @@
import { Wallet } from '@dsh/api-codegen/wallet-api';
import { Option } from '@dsh/components/form-controls/autocomplete-field';
import { Option } from '@dsh/components/form-controls/select-search-field';
const walletToOption = (wallet: Wallet): Option<string> => ({
label: `${wallet?.id} - ${wallet?.name}`,

View File

@ -1,7 +1,7 @@
<dsh-autocomplete-field
<dsh-select-search-field
*ngIf="wallets$ | async"
[label]="label"
[required]="required"
[options]="options$ | async"
[formControl]="formControl"
></dsh-autocomplete-field>
></dsh-select-search-field>

View File

@ -4,12 +4,12 @@ import { ReactiveFormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { AutocompleteFieldModule } from '@dsh/components/form-controls/autocomplete-field';
import { SelectSearchFieldModule } from '@dsh/components/form-controls/select-search-field';
import { WalletAutocompleteFieldComponent } from './wallet-autocomplete-field.component';
@NgModule({
imports: [CommonModule, MatInputModule, MatFormFieldModule, ReactiveFormsModule, AutocompleteFieldModule],
imports: [CommonModule, MatInputModule, MatFormFieldModule, ReactiveFormsModule, SelectSearchFieldModule],
declarations: [WalletAutocompleteFieldComponent],
exports: [WalletAutocompleteFieldComponent],
})

View File

@ -1,32 +0,0 @@
<mat-form-field>
<mat-label *ngIf="label">{{ label }}</mat-label>
<input
type="text"
matInput
[formControl]="searchControl"
[placeholder]="placeholder"
[matAutocomplete]="autocomplete"
[required]="required"
/>
<button mat-button *ngIf="searchControl.value" matSuffix mat-icon-button aria-label="Clear" (click)="clearValue()">
<mat-icon svgIcon="cross"></mat-icon>
</button>
<mat-autocomplete #autocomplete="matAutocomplete" (opened)="panelOpened()">
<cdk-virtual-scroll-viewport
[itemSize]="itemSize"
[style.height.px]="listSize"
[minBufferPx]="itemSize * (listMultiplier + 1)"
[maxBufferPx]="itemSize * (listMultiplier * 2 + 1)"
>
<mat-option
*cdkVirtualFor="let option of filteredOptions"
[value]="option.label"
(onSelectionChange)="selectionChanged(option)"
>
{{ option.label }}
</mat-option>
</cdk-virtual-scroll-viewport>
</mat-autocomplete>
<mat-hint *ngIf="hint">{{ hint }}</mat-hint>
<mat-error *ngIf="control.invalid">{{ getErrorMessage() }}</mat-error>
</mat-form-field>

View File

@ -1,568 +0,0 @@
import { CdkVirtualScrollViewport, ScrollingModule } from '@angular/cdk/scrolling';
import { ChangeDetectionStrategy } from '@angular/core';
import { ComponentFixture, TestBed, TestModuleMetadata } from '@angular/core/testing';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { MatAutocomplete, MatAutocompleteModule, MatAutocompleteTrigger } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { Subject, Subscription } from 'rxjs';
import { instance, mock, verify, when } from 'ts-mockito';
import { VIRTUAL_SCROLL_ITEM_SIZE, VIRTUAL_SCROLL_LIST_MULTIPLIER } from '@dsh/app/shared/components';
import { AutocompleteVirtualScrollComponent } from './autocomplete-virtual-scroll.component';
async function makeTestingModule(config?: TestModuleMetadata) {
await TestBed.configureTestingModule({
imports: [
NoopAnimationsModule,
MatAutocompleteModule,
MatFormFieldModule,
ScrollingModule,
MatInputModule,
ReactiveFormsModule,
MatButtonModule,
MatIconModule,
],
declarations: [AutocompleteVirtualScrollComponent],
...config,
})
.overrideComponent(AutocompleteVirtualScrollComponent, {
set: {
changeDetection: ChangeDetectionStrategy.Default,
},
})
.compileComponents();
}
describe('AutocompleteVirtualScrollComponent', () => {
let component: AutocompleteVirtualScrollComponent;
let fixture: ComponentFixture<AutocompleteVirtualScrollComponent>;
let mockCdkVirtualScrollViewport: CdkVirtualScrollViewport;
let mockMatAutocompleteTrigger: MatAutocompleteTrigger;
let mockMatAutocomplete: MatAutocomplete;
let mockHTMLElement: HTMLElement;
async function initDefaultComponent() {
await makeTestingModule();
fixture = TestBed.createComponent(AutocompleteVirtualScrollComponent);
component = fixture.componentInstance;
component.optionsList = [];
component.control = new FormControl(null);
fixture.detectChanges();
}
beforeEach(() => {
mockCdkVirtualScrollViewport = mock(CdkVirtualScrollViewport);
mockMatAutocompleteTrigger = mock(MatAutocompleteTrigger);
mockMatAutocomplete = mock(MatAutocomplete);
mockHTMLElement = mock(HTMLElement);
});
describe('creation', () => {
beforeEach(async () => {
await makeTestingModule();
fixture = TestBed.createComponent(AutocompleteVirtualScrollComponent);
component = fixture.componentInstance;
});
it('should create', () => {
component.optionsList = [];
component.control = new FormControl(null);
fixture.detectChanges();
expect(component).toBeTruthy();
});
});
describe('itemSize', () => {
it(`should use default value if item size wasn't provided`, async () => {
await makeTestingModule();
fixture = TestBed.createComponent(AutocompleteVirtualScrollComponent);
component = fixture.componentInstance;
expect(component.itemSize).toBe(48);
});
it('should use provided value if item size was provided', async () => {
await makeTestingModule({
providers: [
{
provide: VIRTUAL_SCROLL_ITEM_SIZE,
useValue: 20,
},
],
});
fixture = TestBed.createComponent(AutocompleteVirtualScrollComponent);
component = fixture.componentInstance;
expect(component.itemSize).toBe(20);
});
});
describe('listMultiplier', () => {
it(`should use default value if list multiplier wasn't provided`, async () => {
await makeTestingModule();
fixture = TestBed.createComponent(AutocompleteVirtualScrollComponent);
component = fixture.componentInstance;
expect(component.listMultiplier).toBe(5);
});
it('should use provided value if list multiplier was provided', async () => {
await makeTestingModule({
providers: [
{
provide: VIRTUAL_SCROLL_LIST_MULTIPLIER,
useValue: 10,
},
],
});
fixture = TestBed.createComponent(AutocompleteVirtualScrollComponent);
component = fixture.componentInstance;
expect(component.listMultiplier).toBe(10);
});
});
describe('listSize', () => {
beforeEach(async () => {
await makeTestingModule({
providers: [
{
provide: VIRTUAL_SCROLL_ITEM_SIZE,
useValue: 10,
},
{
provide: VIRTUAL_SCROLL_LIST_MULTIPLIER,
useValue: 5,
},
],
});
fixture = TestBed.createComponent(AutocompleteVirtualScrollComponent);
component = fixture.componentInstance;
});
it(`should return zero if list doesn't exist`, () => {
component.filteredOptions = undefined;
expect(component.listSize).toBe(0);
});
it(`should return zero if list is empty`, () => {
component.filteredOptions = [];
expect(component.listSize).toBe(0);
});
it(`should return list multiplier multiplied on items size if list bigger than list multiplier`, () => {
component.filteredOptions = new Array(6).fill(null).map((_: null, index: number) => {
return {
id: index,
label: `name_${index}`,
};
});
expect(component.listSize).toBe(50);
});
it(`should return list length multiplied on items size if list smaller or equal to list multiplier`, () => {
component.filteredOptions = new Array(5).fill(null).map((_: null, index: number) => {
return {
id: index,
label: `name_${index}`,
};
});
expect(component.listSize).toBe(50);
component.filteredOptions = new Array(2).fill(null).map((_: null, index: number) => {
return {
id: index,
label: `name_${index}`,
};
});
expect(component.listSize).toBe(20);
});
});
describe('ngOnInit', () => {
it('should init search control value using external control label value', async () => {
await makeTestingModule();
fixture = TestBed.createComponent(AutocompleteVirtualScrollComponent);
component = fixture.componentInstance;
component.optionsList = [];
component.control = new FormControl({
id: 'id',
label: 'MyLabel',
});
fixture.detectChanges();
expect(component.searchControl.value).toBe('MyLabel');
});
it(`should init search control with empty string if external control doesn't contain base option value`, async () => {
await makeTestingModule();
fixture = TestBed.createComponent(AutocompleteVirtualScrollComponent);
component = fixture.componentInstance;
component.optionsList = [];
component.control = new FormControl(null);
fixture.detectChanges();
expect(component.searchControl.value).toBe('');
});
it('should add search listener that filter optionsList using search by label and filter using more relevant value first', async () => {
await makeTestingModule();
fixture = TestBed.createComponent(AutocompleteVirtualScrollComponent);
component = fixture.componentInstance;
component.optionsList = [
...new Array(5).fill(null).map((_: null, index: number) => {
return {
id: index,
label: `name_${index}`,
};
}),
{
id: 5,
label: 'test_1',
},
{
id: 6,
label: 'JustTest_2',
},
{
id: 7,
label: 'my_test',
},
];
component.control = new FormControl(null);
fixture.detectChanges();
component.searchControl.setValue('test');
expect(component.filteredOptions).toEqual([
{
id: 5,
label: 'test_1',
},
{
id: 7,
label: 'my_test',
},
{
id: 6,
label: 'JustTest_2',
},
]);
});
it('should make filtered options equal to options if init search was empty string', async () => {
await makeTestingModule();
fixture = TestBed.createComponent(AutocompleteVirtualScrollComponent);
component = fixture.componentInstance;
component.optionsList = new Array(5).fill(null).map((_: null, index: number) => {
return {
id: index,
label: `name_${index}`,
};
});
component.control = new FormControl(null);
fixture.detectChanges();
expect(component.filteredOptions).toEqual(component.optionsList);
});
it('should make init filter using external control value', async () => {
await makeTestingModule();
fixture = TestBed.createComponent(AutocompleteVirtualScrollComponent);
component = fixture.componentInstance;
component.optionsList = [
{
id: 5,
label: 'test_1',
},
{
id: 6,
label: 'JustTest_2',
},
{
id: 7,
label: 'my_test',
},
];
component.control = new FormControl({
id: 6,
label: 'JustTest_2',
});
fixture.detectChanges();
expect(component.filteredOptions).toEqual([
{
id: 6,
label: 'JustTest_2',
},
]);
});
});
describe('ngOnChanges', () => {
const eventEmitter = new Subject<void>();
const subscriptions = new Map<string, Subscription>();
beforeEach(async () => {
await initDefaultComponent();
});
beforeEach(() => {
when(mockHTMLElement.addEventListener).thenReturn((eventName, handler) => {
if (subscriptions.has(eventName)) {
subscriptions.get(eventName).unsubscribe();
}
subscriptions.set(
eventName,
eventEmitter.subscribe(() => {
handler();
})
);
});
when(mockHTMLElement.removeEventListener).thenReturn((eventName) => {
if (subscriptions.has(eventName)) {
subscriptions.get(eventName).unsubscribe();
}
});
when(mockMatAutocompleteTrigger.openPanel()).thenReturn();
when(mockMatAutocompleteTrigger.closePanel()).thenReturn();
});
afterEach(() => {
expect().nothing();
});
it('should add observable that call close panel on any scroll', () => {
when(mockMatAutocomplete.isOpen).thenReturn(true);
component.scrollableWindow = instance(mockHTMLElement);
component.autocomplete = instance(mockMatAutocomplete);
component.trigger = instance(mockMatAutocompleteTrigger);
// fixture detect changes doesn't call ngOnChanges hook
component.ngOnChanges({
scrollableWindow: {
previousValue: undefined,
currentValue: component.scrollableWindow,
isFirstChange(): boolean {
return false;
},
firstChange: false,
},
});
eventEmitter.next();
verify(mockMatAutocompleteTrigger.closePanel()).once();
});
it('should not close panel if panel already closed', () => {
when(mockMatAutocomplete.isOpen).thenReturn(false);
component.scrollableWindow = instance(mockHTMLElement);
component.autocomplete = instance(mockMatAutocomplete);
component.trigger = instance(mockMatAutocompleteTrigger);
// fixture detect changes doesn't call ngOnChanges hook
component.ngOnChanges({
scrollableWindow: {
previousValue: undefined,
currentValue: component.scrollableWindow,
isFirstChange(): boolean {
return false;
},
firstChange: false,
},
});
eventEmitter.next();
verify(mockMatAutocompleteTrigger.closePanel()).never();
});
});
describe('panelOpened', () => {
beforeEach(async () => {
await initDefaultComponent();
when(mockCdkVirtualScrollViewport.checkViewportSize()).thenReturn();
});
afterEach(() => {
expect().nothing();
});
it('should check viewport size on each panel opening', () => {
component.viewport = instance(mockCdkVirtualScrollViewport);
component.panelOpened();
verify(mockCdkVirtualScrollViewport.checkViewportSize()).once();
});
it(`shouldn't check viewport size if viewport wasn't provided`, () => {
component.viewport = null;
component.panelOpened();
verify(mockCdkVirtualScrollViewport.checkViewportSize()).never();
});
});
describe('getErrorMessage', () => {
beforeEach(async () => {
await initDefaultComponent();
});
it('should return empty string if control errors is empty object or null', () => {
component.control.setValidators(() => {
return null;
});
component.control.updateValueAndValidity();
expect(component.getErrorMessage()).toBe('');
});
it(`should use first truthy error from list and return it if it's an string`, () => {
component.control.setValidators([
() => {
return { first: null };
},
() => {
return { second: '' };
},
() => {
return { third: 0 };
},
() => {
return { fourth: false };
},
() => {
return { fifth: 'my error' };
},
() => {
return { sixth: 'another error' };
},
]);
component.control.updateValueAndValidity();
expect(component.getErrorMessage()).toBe('my error');
});
it(`should parse array error, using it's first element as an error message`, () => {
component.control.setValidators([
() => {
return { myError: ['my error'] };
},
]);
component.control.updateValueAndValidity();
expect(component.getErrorMessage()).toBe('my error');
});
it('should return empty string if error has empty array as a value', () => {
component.control.setValidators([
() => {
return { myError: [] };
},
]);
component.control.updateValueAndValidity();
expect(component.getErrorMessage()).toBe('');
});
});
describe('selectionChanged', () => {
beforeEach(async () => {
await initDefaultComponent();
});
it('should update component control value', () => {
expect(component.control.value).toBe(null);
component.selectionChanged({ id: 1, label: 'my option' });
expect(component.control.value).toEqual({ id: 1, label: 'my option' });
});
});
describe('clearValue', () => {
beforeEach(async () => {
await initDefaultComponent();
});
it('should clear control value', () => {
component.control.setValue({ id: 1, label: 'my option' });
component.clearValue();
expect(component.control.value).toBe(null);
});
it('should clear search control', () => {
component.searchControl.setValue('my search');
component.clearValue();
expect(component.searchControl.value).toBe('');
});
describe('panel open', () => {
beforeEach(() => {
when(mockMatAutocompleteTrigger.openPanel()).thenReturn();
});
it('should open panel after tick', async () => {
when(mockMatAutocomplete.isOpen).thenReturn(false);
component.trigger = instance(mockMatAutocompleteTrigger);
component.autocomplete = instance(mockMatAutocomplete);
component.clearValue();
await new Promise((resolve) => {
setTimeout(() => {
resolve(null);
}, 0);
});
verify(mockMatAutocompleteTrigger.openPanel()).once();
expect().nothing();
});
it('should not open panel after tick if panel was opened', async () => {
when(mockMatAutocomplete.isOpen).thenReturn(true);
component.trigger = instance(mockMatAutocompleteTrigger);
component.autocomplete = instance(mockMatAutocomplete);
component.clearValue();
await new Promise((resolve) => {
setTimeout(() => {
resolve(null);
}, 0);
});
verify(mockMatAutocompleteTrigger.openPanel()).never();
expect().nothing();
});
});
});
});

View File

@ -1,200 +0,0 @@
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import {
ChangeDetectionStrategy,
Component,
Inject,
Input,
NgZone,
OnChanges,
OnInit,
Optional,
ViewChild,
} from '@angular/core';
import { FormControl } from '@angular/forms';
import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autocomplete';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import isEmpty from 'lodash-es/isEmpty';
import isNil from 'lodash-es/isNil';
import isObject from 'lodash-es/isObject';
import { fromEvent } from 'rxjs';
import { map } from 'rxjs/operators';
import { BaseOption } from '@dsh/app/shared/components/selects/autocomplete-virtual-scroll/types/base-option';
import { ComponentChange, ComponentChanges } from '../../../../../type-utils';
import { VIRTUAL_SCROLL_ITEM_SIZE, VIRTUAL_SCROLL_LIST_MULTIPLIER } from '../tokens';
const ITEM_SIZE = 48;
const LIST_MULTIPLIER = 5;
const DEFAULT_PLACEHOLDER = 'Search ...';
@UntilDestroy()
@Component({
selector: 'dsh-autocomplete-virtual-scroll',
templateUrl: './autocomplete-virtual-scroll.component.html',
styleUrls: ['./autocomplete-virtual-scroll.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AutocompleteVirtualScrollComponent implements OnInit, OnChanges {
@ViewChild(CdkVirtualScrollViewport, { static: true }) viewport: CdkVirtualScrollViewport;
@ViewChild(MatAutocompleteTrigger, { static: true }) trigger: MatAutocompleteTrigger;
@ViewChild(MatAutocomplete, { static: true }) autocomplete: MatAutocomplete;
@Input() label: string;
@Input() control: FormControl;
@Input() optionsList: BaseOption[];
@Input() hint: string;
@Input() scrollableWindow: HTMLElement;
@Input() required: boolean;
@Input()
get placeholder(): string {
return this.innerPlaceholder;
}
set placeholder(value: string | undefined) {
if (isNil(value)) {
return;
}
this.innerPlaceholder = value;
}
filteredOptions: BaseOption[];
searchControl = new FormControl();
get itemSize(): number {
if (isNil(this.innerItemSize)) {
return ITEM_SIZE;
}
return this.innerItemSize;
}
get listMultiplier(): number {
if (isNil(this.innerListMultiplier)) {
return LIST_MULTIPLIER;
}
return this.innerListMultiplier;
}
get listSize(): number {
if (isNil(this.filteredOptions)) {
return 0;
}
const length = this.filteredOptions.length;
if (length >= this.listMultiplier) {
return this.listMultiplier * this.itemSize;
}
return length * this.itemSize;
}
private scrollableElement: HTMLElement;
private innerPlaceholder = DEFAULT_PLACEHOLDER;
constructor(
private zone: NgZone,
@Optional()
@Inject(VIRTUAL_SCROLL_LIST_MULTIPLIER)
private innerListMultiplier: number,
@Optional()
@Inject(VIRTUAL_SCROLL_ITEM_SIZE)
private innerItemSize: number
) {}
ngOnInit(): void {
this.initControls();
}
ngOnChanges(changes: ComponentChanges<AutocompleteVirtualScrollComponent>): void {
if (!isNil(changes.scrollableWindow)) {
this.initScrollableClose(changes.scrollableWindow);
}
}
panelOpened(): void {
if (!isNil(this.viewport)) {
this.viewport.checkViewportSize();
}
}
getErrorMessage(): string {
if (isEmpty(this.control.errors)) {
return '';
}
const [error] = Object.entries<string | string[] | null>(this.control.errors)
.map(([, value]: [string, string | string[] | null]) => value)
.filter(Boolean);
const errorMessage = Array.isArray(error) ? error[0] : error;
return isNil(errorMessage) ? '' : errorMessage;
}
selectionChanged(option: BaseOption): void {
this.control.setValue(option);
}
clearValue(): void {
this.control.setValue(null);
this.searchControl.setValue('');
// need to make update after cycle was completed once
setTimeout(() => {
this.openPanel();
}, 0);
}
private initControls(): void {
this.searchControl.valueChanges
.pipe(
map((search: string) => search.toLowerCase()),
untilDestroyed(this)
)
.subscribe((search: string) => {
this.filterOptions(search);
});
const initValue = isObject(this.control.value as BaseOption) ? this.control.value.label : '';
this.searchControl.setValue(initValue);
}
private initScrollableClose(change: ComponentChange<AutocompleteVirtualScrollComponent, 'scrollableWindow'>): void {
if (isNil(change.currentValue) || !isNil(this.scrollableElement)) {
return;
}
this.scrollableElement = change.currentValue;
this.zone.runOutsideAngular(() => {
fromEvent(this.scrollableWindow, 'scroll')
.pipe(untilDestroyed(this))
.subscribe(() => {
this.closePanel();
});
});
}
private openPanel(): void {
if (!this.autocomplete.isOpen) {
this.trigger.openPanel();
}
}
private closePanel(): void {
if (this.autocomplete.isOpen) {
this.trigger.closePanel();
}
}
private filterOptions(search: string): void {
this.filteredOptions = this.optionsList
.filter(({ label }: BaseOption) => label.toLowerCase().includes(search))
.map((option: BaseOption) => {
return {
...option,
indexOf: option.label.toLowerCase().indexOf(search),
};
})
.sort(({ indexOf: aIndex }, { indexOf: bIndex }) => {
return aIndex >= bIndex ? 1 : -1;
})
.map(({ id, label }) => ({ id, label }));
}
}

View File

@ -1,27 +0,0 @@
import { ScrollingModule } from '@angular/cdk/scrolling';
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { AutocompleteVirtualScrollComponent } from './autocomplete-virtual-scroll.component';
@NgModule({
imports: [
CommonModule,
MatAutocompleteModule,
MatFormFieldModule,
ScrollingModule,
MatInputModule,
ReactiveFormsModule,
MatButtonModule,
MatIconModule,
],
declarations: [AutocompleteVirtualScrollComponent],
exports: [AutocompleteVirtualScrollComponent],
})
export class AutocompleteVirtualScrollModule {}

View File

@ -1 +0,0 @@
export * from './autocomplete-virtual-scroll.module';

View File

@ -1,4 +0,0 @@
export interface BaseOption<Id = number | string> {
id: Id;
label: string;
}

View File

@ -1,3 +0,0 @@
export * from './tokens';
export * from './selects.module';
export * from './autocomplete-virtual-scroll';

View File

@ -1,10 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { AutocompleteVirtualScrollModule } from '@dsh/app/shared/components/selects/autocomplete-virtual-scroll';
@NgModule({
imports: [CommonModule, AutocompleteVirtualScrollModule],
exports: [AutocompleteVirtualScrollModule],
})
export class SelectsModule {}

View File

@ -1,4 +0,0 @@
import { InjectionToken } from '@angular/core';
export const VIRTUAL_SCROLL_LIST_MULTIPLIER = new InjectionToken('VirtualScrollListMultiplier');
export const VIRTUAL_SCROLL_ITEM_SIZE = new InjectionToken('VirtualScrollItemSize');

View File

@ -1,14 +1,14 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { MatDialogRef } from '@angular/material/dialog';
import { MatRadioModule } from '@angular/material/radio';
import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { TranslocoTestingModule } from '@ngneat/transloco';
import { instance, mock, verify } from 'ts-mockito';
import { ShopType } from '../../../types/shop-type';
import { CreateShopDialogComponent } from './create-shop-dialog.component';
import { ShopType } from './types/shop-type';
@Component({ template: '' })
class MockOnBoardingComponent {}
@ -51,12 +51,6 @@ describe('CreateShopDialogComponent', () => {
provide: MatDialogRef,
useFactory: () => instance(mockDialogRef),
},
{
provide: MAT_DIALOG_DATA,
useValue: {
realm: 'my_realm',
},
},
],
})
.overrideComponent(CreateShopDialogComponent, {

View File

@ -0,0 +1,57 @@
import { Component } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { Observable } from 'rxjs';
import { Shop } from '@dsh/api-codegen/capi';
import { BaseDialogResponseStatus } from '@dsh/app/shared/components/dialog/base-dialog';
import { SHOPS } from '@dsh/app/shared/components/inputs/shop-field';
import { ShopType } from './types/shop-type';
export interface CreateShopDialogData {
shops$?: Observable<Shop[]>;
}
@Component({
selector: 'dsh-create-shop-dialog',
templateUrl: 'create-shop-dialog.component.html',
styleUrls: ['create-shop-dialog.component.scss'],
providers: [
{
provide: SHOPS,
deps: [MAT_DIALOG_DATA],
useFactory: ({ shops$ }: CreateShopDialogData = {}) => shops$,
},
],
})
export class CreateShopDialogComponent {
selectedShopType: ShopType;
selectionConfirmed = false;
shopType = ShopType;
constructor(
public dialogRef: MatDialogRef<CreateShopDialogComponent, BaseDialogResponseStatus>,
private router: Router
) {}
onTypeChange(type: ShopType): void {
this.selectedShopType = type;
}
next(): void {
if (this.selectedShopType === ShopType.New) {
this.dialogRef.close();
void this.router.navigate(['claim-section', 'onboarding']);
}
this.selectionConfirmed = true;
}
sendClaim(): void {
this.dialogRef.close(BaseDialogResponseStatus.Success);
}
cancelClaim(): void {
this.dialogRef.close(BaseDialogResponseStatus.Cancelled);
}
}

Some files were not shown because too many files have changed in this diff Show More