Skip to content

Commit d61e02f

Browse files
dmitriy-borzenkoDmitriy Borzenko
and
Dmitriy Borzenko
authored
Kamu UI 504 simplify boileplate code to read route and query parameters from activated route (#568)
* refactored @input() query search params for account.ts * refactored search.component.ts * refactored account-settings.component * changed solution for refactored components * refactored dataset-view.component * refactored github.callback component * refactored query-explainer component * refactored global-query compoonent * refactored dataset-flow-details component * returned router.events * implemented blockMeatadata resolver * added hash from route * fixed search by dataset name * returned old code * fixed search component * returned for search component previous version * changed routing for /v/settings route * changed CHANGELOG.md * added searchResolver * fixed search tests * added tests for search and block-metadata resolvers * added addPollingSourceResolver * added setTransformResolver * added addPushSourceResolver * refactored routing * refactored block service * fixed unit tests * changed routing * added input decorators for events components * minor changes for routing * remove MaybeIndefined type from resolvers * changed some styles * fixed unit tests --------- Co-authored-by: Dmitriy Borzenko <[email protected]>
1 parent a34e378 commit d61e02f

File tree

62 files changed

+897
-753
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+897
-753
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88
### Added
9+
- Added resolver `BlockMetadataResolver` for metadata block page
910
- Added `DEVELOPER.md` file
1011
- Added license header for all *.ts files
1112
- Account page: added account name
1213
### Changed
1314
- Modified `README.md` file
15+
- Changed the method of getting route and query parameters via input decorator for some components
1416
### Fixed
1517
- Remove "Dashboard" link from application header
1618
- Disabled "keywords" feature for "demo" mode

src/app/account/account.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<div class="mx-4 mt-4 p-responsive clearfix" *ngIf="user$ | async as user">
2-
<div class="custom-container" *ngIf="activeTab$ | async as activeTab">
2+
<div class="custom-container">
33
<div class="container-sidebar">
44
<div class="d-flex mb-3 justify-content-center align-items-center">
55
<a *ngIf="isLoggedUser; else notLoggedUserLink">

src/app/account/account.component.spec.ts

Lines changed: 18 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ describe("AccountComponent", () => {
8282
provide: ActivatedRoute,
8383
useValue: {
8484
queryParams: mockQueryParams.asObservable(),
85-
params: mockParams.asObservable(),
8685
},
8786
},
8887
],
@@ -100,7 +99,7 @@ describe("AccountComponent", () => {
10099

101100
fixture = TestBed.createComponent(AccountComponent);
102101
component = fixture.componentInstance;
103-
fixture.detectChanges();
102+
component.accountName = TEST_LOGIN;
104103
});
105104

106105
it("should create", () => {
@@ -128,11 +127,7 @@ describe("AccountComponent", () => {
128127
});
129128

130129
it("should check API calls when account is found", () => {
131-
fetchAccountByNameSpy.calls.reset();
132-
getDatasetsByAccountNameSpy.calls.reset();
133-
134-
mockParams.next({ [ProjectLinks.URL_PARAM_ACCOUNT_NAME]: mockAccountDetails.accountName });
135-
130+
fixture.detectChanges();
136131
expect(fetchAccountByNameSpy).toHaveBeenCalledOnceWith(mockAccountDetails.accountName);
137132
expect(getDatasetsByAccountNameSpy).toHaveBeenCalledOnceWith(
138133
mockAccountDetails.accountName,
@@ -143,54 +138,47 @@ describe("AccountComponent", () => {
143138
it("should check API calls when account is not found", fakeAsync(() => {
144139
fetchAccountByNameSpy = fetchAccountByNameSpy.and.returnValue(of(null).pipe(delay(0)));
145140
getDatasetsByAccountNameSpy = getDatasetsByAccountNameSpy.and.stub();
146-
fetchAccountByNameSpy.calls.reset();
147-
getDatasetsByAccountNameSpy.calls.reset();
148141

149-
mockParams.next({ [ProjectLinks.URL_PARAM_ACCOUNT_NAME]: TEST_LOGIN });
142+
fixture.detectChanges();
150143

151144
expect(fetchAccountByNameSpy).toHaveBeenCalledOnceWith(TEST_LOGIN);
152145
expect(getDatasetsByAccountNameSpy).not.toHaveBeenCalled();
153-
154-
expect(() => tick()).toThrow(new AccountNotFoundError());
155-
expect(() => flush()).toThrow(new AccountNotFoundError());
146+
expect(() => {
147+
tick();
148+
}).toThrow(new AccountNotFoundError());
149+
expect(() => {
150+
flush();
151+
}).toThrow(new AccountNotFoundError());
156152

157153
flush();
158154
}));
159155

160156
it("should check activeTab when URL not exist query param tab", () => {
157+
fixture.detectChanges();
161158
mockQueryParams.next({ page: 1 });
159+
component.tab = undefined;
162160

163-
let nCalls = 0;
164-
component.activeTab$.subscribe((activeTab: AccountTabs) => {
165-
// Ignore first call (default event)
166-
if (nCalls == 1) {
167-
expect(activeTab).toEqual(AccountTabs.DATASETS);
168-
} else if (nCalls > 2) {
169-
fail("Unexpected number of calls");
170-
}
171-
nCalls++;
172-
});
173-
expect(nCalls).toBeLessThan(3);
161+
expect(component.activeTab).toEqual(AccountTabs.DATASETS);
174162
});
175163
// TODO: test wrong tab
176164

177165
it("should check page when not specified in the query", () => {
178-
getDatasetsByAccountNameSpy.calls.reset();
179-
180-
mockQueryParams.next({ tab: AccountTabs.DATASETS });
166+
component.activeTab = AccountTabs.DATASETS;
167+
fixture.detectChanges();
181168

182169
expect(getDatasetsByAccountNameSpy).toHaveBeenCalledOnceWith(mockAccountDetails.accountName, 0);
183170
});
184171

185172
it("should check page when specified in the query", () => {
186-
getDatasetsByAccountNameSpy.calls.reset();
187-
188-
mockQueryParams.next({ tab: AccountTabs.DATASETS, page: 3 });
173+
component.page = 3;
174+
component.activeTab = AccountTabs.DATASETS;
175+
fixture.detectChanges();
189176

190177
expect(getDatasetsByAccountNameSpy).toHaveBeenCalledOnceWith(mockAccountDetails.accountName, 2 /* 3 - 1 */);
191178
});
192179

193180
it("should check routers link ", () => {
181+
fixture.detectChanges();
194182
const datasetsTabLink = findElementByDataTestId(fixture, "link-account-datasets-tab") as HTMLLinkElement;
195183
expect(datasetsTabLink.href).toContain(`?tab=${AccountTabs.DATASETS}`);
196184

src/app/account/account.component.ts

Lines changed: 24 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,16 @@
66
*/
77

88
import ProjectLinks from "src/app/project-links";
9-
import { BaseComponent } from "src/app/common/components/base.component";
10-
import { ChangeDetectionStrategy, Component, inject, OnInit } from "@angular/core";
9+
import { ChangeDetectionStrategy, Component, inject, Input, numberAttribute, OnInit } from "@angular/core";
1110
import { AccountFragment } from "src/app/api/kamu.graphql.interface";
1211
import { AccountTabs } from "./account.constants";
13-
import { ActivatedRoute, Params } from "@angular/router";
1412
import AppValues from "src/app/common/values/app.values";
1513
import { promiseWithCatch } from "src/app/common/helpers/app.helpers";
1614
import { AccountService } from "src/app/account/account.service";
1715
import { DatasetsAccountResponse } from "src/app/interface/dataset.interface";
1816
import { distinctUntilChanged, map, shareReplay, switchMap } from "rxjs/operators";
19-
import { Observable, combineLatest } from "rxjs";
20-
import { MaybeNull } from "src/app/interface/app.types";
17+
import { Observable, combineLatest, of } from "rxjs";
18+
import { MaybeNull, MaybeUndefined } from "src/app/interface/app.types";
2119
import { AccountNotFoundError } from "src/app/common/values/errors";
2220
import { AccountPageQueryParams } from "./account.component.model";
2321
import { ModalService } from "../common/components/modal/modal.service";
@@ -29,39 +27,36 @@ import { LoggedUserService } from "../auth/logged-user.service";
2927
styleUrls: ["./account.component.scss"],
3028
changeDetection: ChangeDetectionStrategy.OnPush,
3129
})
32-
export class AccountComponent extends BaseComponent implements OnInit {
30+
export class AccountComponent implements OnInit {
31+
@Input(ProjectLinks.URL_PARAM_ACCOUNT_NAME) public accountName: string;
32+
@Input(ProjectLinks.URL_QUERY_PARAM_TAB) public set tab(value: MaybeUndefined<AccountTabs>) {
33+
this.activeTab = value ?? AccountTabs.DATASETS;
34+
}
35+
@Input({ transform: numberAttribute, alias: ProjectLinks.URL_QUERY_PARAM_PAGE }) public set currentPage(
36+
value: number,
37+
) {
38+
this.page = value ?? 1;
39+
}
40+
41+
public activeTab: AccountTabs;
42+
public page: number;
43+
3344
public readonly AccountTabs = AccountTabs;
3445
public isDropdownMenu = false;
3546
public user$: Observable<AccountFragment>;
3647
public datasetsAccount$: Observable<DatasetsAccountResponse>;
37-
public activeTab$: Observable<AccountTabs>;
3848

39-
private route = inject(ActivatedRoute);
4049
private modalService = inject(ModalService);
4150
private accountService = inject(AccountService);
4251
private loggedUserService = inject(LoggedUserService);
4352

4453
public ngOnInit(): void {
45-
const accountName$ = this.route.params.pipe(
46-
map((params: Params) => params[ProjectLinks.URL_PARAM_ACCOUNT_NAME] as string),
47-
);
54+
const queryParams$ = of({
55+
tab: this.activeTab,
56+
page: this.page,
57+
}).pipe(shareReplay());
4858

49-
const queryParams$ = this.route.queryParams.pipe(
50-
map((queryParams: Params) => {
51-
return {
52-
tab: queryParams.tab ? (queryParams.tab as AccountTabs) : undefined,
53-
page: queryParams.page ? (queryParams.page as number) : undefined,
54-
} as AccountPageQueryParams;
55-
}),
56-
shareReplay(),
57-
);
58-
59-
this.activeTab$ = queryParams$.pipe(
60-
map((accountPageParams: AccountPageQueryParams) => accountPageParams.tab ?? AccountTabs.DATASETS),
61-
distinctUntilChanged(),
62-
);
63-
64-
this.user$ = this.pipelineAccountByName(accountName$);
59+
this.user$ = this.pipelineAccountByName(this.accountName);
6560
this.datasetsAccount$ = this.pipelineAccountDatasets(this.user$, queryParams$);
6661
}
6762

@@ -91,11 +86,8 @@ export class AccountComponent extends BaseComponent implements OnInit {
9186
);
9287
}
9388

94-
private pipelineAccountByName(accountName$: Observable<string>): Observable<AccountFragment> {
95-
return accountName$.pipe(
96-
switchMap((accountName: string) => {
97-
return this.accountService.fetchAccountByName(accountName);
98-
}),
89+
private pipelineAccountByName(accountName: string): Observable<AccountFragment> {
90+
return this.accountService.fetchAccountByName(accountName).pipe(
9991
shareReplay(),
10092
map((account: MaybeNull<AccountFragment>) => {
10193
if (account) return account;

src/app/account/settings/account-settings.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@
286286
</div>
287287
<div class="content">
288288
<ng-container *ngIf="activeTab === AccountSettingsTabs.ACCESS_TOKENS">
289-
<app-access-tokens-tab [account]="user" />
289+
<app-access-tokens-tab [currentPage]="currentPage" [account]="user" />
290290
</ng-container>
291291
<ng-container *ngIf="activeTab === AccountSettingsTabs.EMAILS">
292292
<app-emails-tab [account]="user" (accountEmailChange)="changeAccountEmail()" />

src/app/account/settings/account-settings.component.spec.ts

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import { mockAccountDetails, mockAccountDetailsWithEmail } from "../../api/mock/auth.mock";
99
import { RouterTestingModule } from "@angular/router/testing";
1010
import { ComponentFixture, TestBed } from "@angular/core/testing";
11-
import { ActivatedRoute, Router } from "@angular/router";
1211
import { ApolloTestingModule } from "apollo-angular/testing";
1312
import { AccountSettingsComponent } from "./account-settings.component";
1413
import { HttpClientTestingModule } from "@angular/common/http/testing";
@@ -20,20 +19,18 @@ import {
2019
} from "src/app/common/helpers/base-test.helpers.spec";
2120
import { AccountSettingsTabs } from "./account-settings.constants";
2221
import { of } from "rxjs";
23-
import ProjectLinks from "src/app/project-links";
2422
import { LoginService } from "../../auth/login/login.service";
2523
import { MatIconModule } from "@angular/material/icon";
2624
import { ToastrModule } from "ngx-toastr";
2725
import { AccountEmailService } from "src/app/account/settings/tabs/emails-tab/account-email.service";
26+
import { AccountSettingsModule } from "./account-settings.module";
2827

2928
describe("AccountSettingsComponent", () => {
3029
let component: AccountSettingsComponent;
3130
let fixture: ComponentFixture<AccountSettingsComponent>;
3231
let loggedUserService: LoggedUserService;
3332
let loginService: LoginService;
34-
let activatedRoute: ActivatedRoute;
3533
let accountEmailService: AccountEmailService;
36-
let router: Router;
3734

3835
beforeEach(async () => {
3936
await TestBed.configureTestingModule({
@@ -44,13 +41,12 @@ describe("AccountSettingsComponent", () => {
4441
RouterTestingModule,
4542
HttpClientTestingModule,
4643
MatIconModule,
44+
AccountSettingsModule,
4745
],
4846
}).compileComponents();
4947

5048
registerMatSvgIcons();
5149

52-
activatedRoute = TestBed.inject(ActivatedRoute);
53-
router = TestBed.inject(Router);
5450
accountEmailService = TestBed.inject(AccountEmailService);
5551

5652
loginService = TestBed.inject(LoginService);
@@ -86,8 +82,9 @@ describe("AccountSettingsComponent", () => {
8682
});
8783

8884
it("should open profile tab by default", () => {
85+
component.category = null;
8986
fixture.detectChanges();
90-
expect(component.activeTab).toEqual(AccountSettingsTabs.PROFILE);
87+
expect(component.activeTab).toEqual(AccountSettingsTabs.ACCESS_TOKENS);
9188
});
9289

9390
[
@@ -102,19 +99,15 @@ describe("AccountSettingsComponent", () => {
10299
AccountSettingsTabs.SECURITY,
103100
].forEach((tab: AccountSettingsTabs) => {
104101
it(`should activate ${tab} tab`, () => {
105-
activatedRoute.snapshot.params = {
106-
[ProjectLinks.URL_PARAM_CATEGORY]: tab,
107-
};
108-
component.ngOnInit();
102+
component.category = tab;
103+
fixture.detectChanges();
109104
expect(component.activeTab).toEqual(tab);
110105
});
111106
});
112107

113-
it("should open profile tab for a wrong tab", async () => {
114-
activatedRoute.snapshot.params = {
115-
[ProjectLinks.URL_PARAM_CATEGORY]: "wrong",
116-
};
117-
await router.navigate(["/"]);
118-
expect(component.activeTab).toEqual(AccountSettingsTabs.PROFILE);
108+
it("should open access token tab for a wrong tab", () => {
109+
component.category = "wrong-tab" as AccountSettingsTabs;
110+
fixture.detectChanges();
111+
expect(component.activeTab).toEqual(AccountSettingsTabs.ACCESS_TOKENS);
119112
});
120113
});

src/app/account/settings/account-settings.component.ts

Lines changed: 14 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,12 @@
88
import ProjectLinks from "src/app/project-links";
99
import { AccountWithEmailFragment } from "src/app/api/kamu.graphql.interface";
1010
import { AccountSettingsTabs } from "./account-settings.constants";
11-
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit } from "@angular/core";
12-
import { ActivatedRoute, NavigationEnd, Router } from "@angular/router";
13-
import { filter, switchMap } from "rxjs/operators";
11+
import { ChangeDetectionStrategy, Component, inject, Input, OnInit } from "@angular/core";
12+
import { switchMap } from "rxjs/operators";
1413
import { BaseComponent } from "src/app/common/components/base.component";
1514
import AppValues from "src/app/common/values/app.values";
16-
import { MaybeNull, MaybeUndefined } from "src/app/interface/app.types";
15+
import { MaybeNull } from "src/app/interface/app.types";
1716
import { EMPTY, Observable } from "rxjs";
18-
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
1917
import { AccountEmailService } from "src/app/account/settings/tabs/emails-tab/account-email.service";
2018
import { LoggedUserService } from "../../auth/logged-user.service";
2119

@@ -26,29 +24,26 @@ import { LoggedUserService } from "../../auth/logged-user.service";
2624
changeDetection: ChangeDetectionStrategy.OnPush,
2725
})
2826
export class AccountSettingsComponent extends BaseComponent implements OnInit {
27+
@Input(ProjectLinks.URL_PARAM_CATEGORY) public set category(value: MaybeNull<AccountSettingsTabs>) {
28+
this.activeTab =
29+
value && Object.values(AccountSettingsTabs).includes(value) ? value : AccountSettingsTabs.ACCESS_TOKENS;
30+
}
31+
@Input(ProjectLinks.URL_QUERY_PARAM_PAGE) public set page(value: number) {
32+
this.currentPage = value ?? 1;
33+
}
34+
35+
public activeTab: AccountSettingsTabs;
36+
public currentPage: number;
37+
2938
public readonly DEFAULT_AVATAR_URL = AppValues.DEFAULT_AVATAR_URL;
3039
public readonly AccountSettingsTabs: typeof AccountSettingsTabs = AccountSettingsTabs;
3140

32-
public activeTab: AccountSettingsTabs = AccountSettingsTabs.PROFILE;
3341
public user$: Observable<MaybeNull<AccountWithEmailFragment>>;
3442

35-
private router = inject(Router);
36-
private route = inject(ActivatedRoute);
3743
private accountEmailService = inject(AccountEmailService);
3844
private loggedUserService = inject(LoggedUserService);
39-
private cdr = inject(ChangeDetectorRef);
4045

4146
public ngOnInit(): void {
42-
this.router.events
43-
.pipe(
44-
filter((event) => event instanceof NavigationEnd),
45-
takeUntilDestroyed(this.destroyRef),
46-
)
47-
.subscribe(() => {
48-
this.extractActiveTabFromRoute();
49-
});
50-
51-
this.extractActiveTabFromRoute();
5247
this.fetchAccountInfo();
5348
}
5449

@@ -71,20 +66,4 @@ export class AccountSettingsComponent extends BaseComponent implements OnInit {
7166
public changeAccountEmail(): void {
7267
this.fetchAccountInfo();
7368
}
74-
75-
private extractActiveTabFromRoute(): void {
76-
const categoryParam: MaybeUndefined<string> = this.route.snapshot.params[
77-
ProjectLinks.URL_PARAM_CATEGORY
78-
] as MaybeUndefined<string>;
79-
80-
if (categoryParam) {
81-
const category = categoryParam as AccountSettingsTabs;
82-
if (Object.values(AccountSettingsTabs).includes(category)) {
83-
this.activeTab = category;
84-
return;
85-
}
86-
}
87-
88-
this.activeTab = AccountSettingsTabs.PROFILE;
89-
}
9069
}

src/app/account/settings/tabs/access-tokens-tab/access-tokens-tab.component.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ describe("AccessTokensTabComponent", () => {
9191
accessTokenService = TestBed.inject(AccessTokenService);
9292
modalService = TestBed.inject(ModalService);
9393
component = fixture.componentInstance;
94+
component.currentPage = 1;
9495
spyOn(accessTokenService, "listAccessTokens").and.returnValue(
9596
of(mockListAccessTokensQuery.auth.listAccessTokens as AccessTokenConnection),
9697
);

0 commit comments

Comments
 (0)