Skip to content

Commit ae1a799

Browse files
honzikecJan Kaiser
andauthored
[angular-ngrx-scss] issues tab (#1941)
* wip: issues tab * chore: cleanup and comments * feat: use search api, connect filters * fix: use interface in test * fix: make pagination fetch correct page --------- Co-authored-by: Jan Kaiser <honzikec@Jan-MacBook-Air.local>
1 parent d576e9b commit ae1a799

26 files changed

Lines changed: 637 additions & 90 deletions

angular-ngrx-scss/src/app/fixtures/repository.fixtures.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import {
2-
PR_STATE,
2+
ISSUE_STATE,
33
PullRequestAPIResponse,
44
PullRequestItemAPIResponse,
5-
PullRequestLabel,
5+
IssueLabel,
66
RepoPullRequests,
77
} from '../state/repository';
88

99
export const generatePullRequestAPIResponseFixture = (
10-
state: PR_STATE = 'open',
10+
state: ISSUE_STATE = 'open',
1111
): PullRequestAPIResponse => {
1212
const closedDate = new Date(2022, 2, 1).toISOString();
1313
return {
@@ -34,7 +34,7 @@ export const generatePullRequestAPIResponseFixture = (
3434
labels: [
3535
{
3636
name: 'bugs',
37-
} as PullRequestLabel,
37+
} as IssueLabel,
3838
],
3939
comments: 305,
4040
} as PullRequestItemAPIResponse,
Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,42 @@
11
<div class="filters-container">
22
<div class="issue-status">
3-
<a href="" class="active">
4-
<span class="icon" appOcticon="no-entry" size="16"></span>
5-
<span>412 Open</span>
6-
</a>
7-
<a href="">
3+
<button [ngClass]="{ active: viewState === 'open' }" (click)="selectOpen()">
84
<span class="icon" appOcticon="check" size="16"></span
9-
><span>4877 Open</span></a
5+
><span>{{ openIssues?.total ?? 0 }} Open</span>
6+
</button>
7+
<button
8+
[ngClass]="{ active: viewState === 'closed' }"
9+
(click)="selectClosed()"
1010
>
11+
<span class="icon" appOcticon="no-entry" size="16"></span>
12+
<span>{{ closedIssues?.total ?? 0 }} Closed</span>
13+
</button>
1114
</div>
1215
<div class="issue-filters">
1316
<app-filter-dropdown
1417
name="Label"
1518
description="Select label"
1619
[isRepo]="true"
20+
[items]="(labels$ | async) || []"
21+
[toggle]="true"
22+
(setFilter)="setLabel($event)"
23+
[current]="filterParams.labels"
1724
></app-filter-dropdown>
1825
<app-filter-dropdown
1926
name="Milestones"
2027
description="Select milestone"
2128
[isRepo]="true"
29+
[items]="(milestones$ | async) || []"
30+
(setFilter)="setMilestone($event)"
31+
[current]="filterParams.milestone"
2232
></app-filter-dropdown>
2333
<app-filter-dropdown
2434
name="Sort"
2535
description="Select sort"
2636
[isRepo]="true"
37+
[items]="sortOptions"
38+
(setFilter)="setSort($event)"
39+
[current]="filterParams.sort"
2740
></app-filter-dropdown>
2841
</div>
2942
</div>

angular-ngrx-scss/src/app/issues/components/issues-header/issues-header.component.scss

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@
2222
gap: variables.$padding--l;
2323
color: variables.$gray600;
2424

25-
a {
25+
button {
2626
color: inherit;
27+
border: none;
28+
cursor: pointer;
2729

2830
.icon {
2931
margin-right: functions.rem(5);
Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,101 @@
1-
import { Component } from '@angular/core';
1+
import { Component, EventEmitter, Input, Output } from '@angular/core';
2+
import { Store } from '@ngrx/store';
3+
import { Observable, map } from 'rxjs';
4+
import { Sort } from 'src/app/repository/services/repository.interfaces';
5+
import { FilterOption } from 'src/app/shared/components/filter-dropdown/filter-dropdown.component';
6+
import { SORTING_OPTIONS } from 'src/app/shared/constants';
7+
import {
8+
ISSUE_STATE,
9+
RepoIssues,
10+
fetchIssues,
11+
selectLabels,
12+
selectMilestones,
13+
} from 'src/app/state/repository';
214

315
@Component({
416
selector: 'app-issues-header',
517
templateUrl: './issues-header.component.html',
618
styleUrls: ['./issues-header.component.scss'],
719
})
8-
export class IssuesHeaderComponent {}
20+
export class IssuesHeaderComponent {
21+
@Input() owner!: string;
22+
23+
@Input() repoName!: string;
24+
25+
filterParams: { labels?: string; milestone?: string; sort: Sort } = {
26+
sort: 'created',
27+
};
28+
29+
sortOptions: FilterOption[] = SORTING_OPTIONS;
30+
31+
milestones$: Observable<FilterOption[]> = this.store
32+
.select(selectMilestones)
33+
.pipe(
34+
map((milestones) =>
35+
milestones.map((milestone) => ({
36+
label: milestone.title,
37+
value: milestone.title,
38+
})),
39+
),
40+
);
41+
42+
labels$: Observable<FilterOption[]> = this.store
43+
.select(selectLabels)
44+
.pipe(
45+
map((labels) =>
46+
labels.map((label) => ({ label: label.name, value: label.name })),
47+
),
48+
);
49+
50+
@Input() viewState: ISSUE_STATE = 'open';
51+
52+
@Input()
53+
openIssues: RepoIssues | null = null;
54+
55+
@Input()
56+
closedIssues: RepoIssues | null = null;
57+
58+
@Output() viewStateChange = new EventEmitter<ISSUE_STATE>();
59+
60+
constructor(private store: Store) {}
61+
62+
selectOpen() {
63+
this.viewStateChange.emit('open');
64+
}
65+
66+
selectClosed() {
67+
this.viewStateChange.emit('closed');
68+
}
69+
70+
setLabel(label: string) {
71+
this.filterParams.labels = label;
72+
this.refetchIssues();
73+
}
74+
75+
setMilestone(milestone: string) {
76+
this.filterParams.milestone = milestone;
77+
this.refetchIssues();
78+
}
79+
80+
setSort(sort: string) {
81+
this.filterParams.sort = sort as Sort;
82+
this.refetchIssues();
83+
}
84+
85+
private refetchIssues() {
86+
this.store.dispatch(
87+
fetchIssues({
88+
owner: this.owner,
89+
repoName: this.repoName,
90+
params: { state: 'open', ...this.filterParams },
91+
}),
92+
);
93+
this.store.dispatch(
94+
fetchIssues({
95+
owner: this.owner,
96+
repoName: this.repoName,
97+
params: { state: 'closed', ...this.filterParams },
98+
}),
99+
);
100+
}
101+
}

angular-ngrx-scss/src/app/issues/components/issues-list/issues-list.component.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<div class="issue">
2-
<div class="issues-list-container">
3-
<ng-container *ngFor="let issue of issues">
2+
<div class="issues-list-container" *ngIf="issues">
3+
<ng-container *ngFor="let issue of issues.issues">
44
<app-repo-issue-pull-card [item]="issue"></app-repo-issue-pull-card>
55
</ng-container>
66
</div>
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { Component, Input } from '@angular/core';
2-
import { Issue } from 'src/app/repository/services/repository.interfaces';
2+
import { RepoIssues } from 'src/app/state/repository';
33

44
@Component({
55
selector: 'app-issues-list',
66
templateUrl: './issues-list.component.html',
77
})
88
export class IssuesListComponent {
9-
@Input() issues: Issue[] = [];
9+
@Input() issues: RepoIssues | null = null;
1010
}
Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
<div class="container issues-container">
2-
<app-issues-header></app-issues-header>
3-
<app-issues-list></app-issues-list>
2+
<app-issues-header
3+
[viewState]="viewState"
4+
[openIssues]="openIssues$ | async"
5+
[closedIssues]="closedIssues$ | async"
6+
[owner]="owner"
7+
[repoName]="repoName"
8+
(viewStateChange)="viewStateChange($event)"
9+
></app-issues-header>
10+
<app-issues-list
11+
[issues]="
12+
viewState === 'open' ? (openIssues$ | async) : (closedIssues$ | async)
13+
"
14+
></app-issues-list>
415
</div>
5-
<app-pagination></app-pagination>
16+
<app-pagination
17+
[params]="
18+
viewState === 'open'
19+
? (openIssuesPaginationParams$ | async)
20+
: (closedIssuesPaginationParams$ | async)
21+
"
22+
(pageChange)="pageChange($event)"
23+
></app-pagination>

angular-ngrx-scss/src/app/issues/components/issues.component.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
.issues-container {
44
border-radius: variables.$padding;
55
border: 1px solid variables.$gray200;
6+
margin: 1rem auto;
67
}
Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,68 @@
1-
import { Component } from '@angular/core';
1+
import { Component, OnInit } from '@angular/core';
2+
import { ActivatedRoute } from '@angular/router';
3+
import { Store } from '@ngrx/store';
4+
import {
5+
ISSUE_STATE,
6+
fetchIssues,
7+
selectClosedIssuePaginationParams,
8+
selectClosedIssues,
9+
selectOpenIssuePaginationParams,
10+
selectOpenIssues,
11+
} from 'src/app/state/repository';
212

313
@Component({
414
selector: 'app-issues',
515
templateUrl: './issues.component.html',
616
styleUrls: ['./issues.component.scss'],
717
})
8-
export class IssuesComponent {}
18+
export class IssuesComponent implements OnInit {
19+
owner!: string;
20+
repoName!: string;
21+
openIssues$ = this.store.select(selectOpenIssues);
22+
closedIssues$ = this.store.select(selectClosedIssues);
23+
viewState: ISSUE_STATE = 'open';
24+
25+
openIssuesPaginationParams$ = this.store.select(
26+
selectOpenIssuePaginationParams,
27+
);
28+
closedIssuesPaginationParams$ = this.store.select(
29+
selectClosedIssuePaginationParams,
30+
);
31+
32+
constructor(private route: ActivatedRoute, private store: Store) {}
33+
34+
ngOnInit(): void {
35+
this.owner = this.route.snapshot.paramMap.get('owner') as string;
36+
this.repoName = this.route.snapshot.paramMap.get('repo') as string;
37+
38+
this.store.dispatch(
39+
fetchIssues({
40+
owner: this.owner,
41+
repoName: this.repoName,
42+
params: { state: 'open' },
43+
}),
44+
);
45+
46+
this.store.dispatch(
47+
fetchIssues({
48+
owner: this.owner,
49+
repoName: this.repoName,
50+
params: { state: 'closed' },
51+
}),
52+
);
53+
}
54+
55+
pageChange(page: number) {
56+
this.store.dispatch(
57+
fetchIssues({
58+
owner: this.owner,
59+
repoName: this.repoName,
60+
params: { state: this.viewState, page },
61+
}),
62+
);
63+
}
64+
65+
viewStateChange(viewState: ISSUE_STATE) {
66+
this.viewState = viewState;
67+
}
68+
}

angular-ngrx-scss/src/app/pull-requests/components/pull-requests-header/pull-requests-header.component.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
Input,
66
Output,
77
} from '@angular/core';
8-
import { PR_STATE, RepoPullRequests } from '../../../state/repository';
8+
import { ISSUE_STATE, RepoPullRequests } from '../../../state/repository';
99

1010
@Component({
1111
selector: 'app-pull-requests-header',
@@ -16,10 +16,10 @@ import { PR_STATE, RepoPullRequests } from '../../../state/repository';
1616
export class PullRequestsHeaderComponent {
1717
@Input() openPullRequests!: RepoPullRequests | null;
1818
@Input() closedPullRequests!: RepoPullRequests | null;
19-
@Input() viewState: PR_STATE = 'open';
20-
@Output() viewStateChange = new EventEmitter<PR_STATE>();
19+
@Input() viewState: ISSUE_STATE = 'open';
20+
@Output() viewStateChange = new EventEmitter<ISSUE_STATE>();
2121

22-
changeViewState(state: PR_STATE) {
22+
changeViewState(state: ISSUE_STATE) {
2323
this.viewState = state;
2424
this.viewStateChange.emit(this.viewState);
2525
}

0 commit comments

Comments
 (0)