Skip to content

Commit e35b840

Browse files
committed
feat: chart, votings via gRPC API. Profile photo, item logo and picture upload via REST API
1 parent 6b0bbf2 commit e35b840

26 files changed

+3648
-1338
lines changed

package-lock.json

Lines changed: 966 additions & 825 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"ngx-autosize": "^2.0.4",
4848
"ngx-pipes": "^3.2.0",
4949
"npm": "^10 || ^11",
50+
"object-typed": "^1.0.0",
5051
"sprintf-js": "^1.1.2",
5152
"tslib": "^2.5.0",
5253
"typescript": ">=5.4.0 && <5.8.0",

src/app/account/profile/profile.component.html

Lines changed: 71 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -7,83 +7,83 @@ <h2 i18n>Personal info</h2>
77
<div class="card card-body mb-4">
88
<p>{{ user.name }} (<a [href]="changeProfileUrl" i18n>Change</a>)</p>
99
</div>
10-
}
1110

12-
<h2 i18n>Photo</h2>
13-
<div class="card card-body mb-4">
14-
<app-markdown i18n-markdown markdown="You can upload photo or use [Gravatar](http://gravatar.com/)"></app-markdown>
15-
@if (photo) {
16-
<div>
17-
<img alt="" [src]="photo.src" class="mb-4" />
18-
@for (message of photoInvalidParams | invalidParams: 'file'; track message) {
19-
<p [textContent]="message" class="invalid-feedback" style="display: block"></p>
20-
}
21-
</div>
22-
}
23-
<div class="row">
24-
<div class="col-6">
25-
<input type="file" accept="image/*" #input class="form-control" (change)="onChange($event)" />
26-
</div>
27-
<div class="col-6">
28-
@if (photo) {
29-
<button type="submit" class="btn btn-danger" (click)="resetPhoto()">
30-
<i class="bi bi-x" aria-hidden="true"></i>
31-
<ng-container i18n>Delete photo</ng-container>
32-
</button>
33-
}
11+
<h2 i18n>Photo</h2>
12+
<div class="card card-body mb-4">
13+
<app-markdown i18n-markdown markdown="You can upload photo or use [Gravatar](http://gravatar.com/)"></app-markdown>
14+
@if (photo) {
15+
<div>
16+
<img alt="" [src]="photo.src" class="mb-4" />
17+
@for (message of photoInvalidParams | invalidParams: 'file'; track message) {
18+
<p [textContent]="message" class="invalid-feedback" style="display: block"></p>
19+
}
20+
</div>
21+
}
22+
<div class="row">
23+
<div class="col-6">
24+
<input type="file" accept="image/*" #input class="form-control" (change)="onChange($event)" />
25+
</div>
26+
<div class="col-6">
27+
@if (photo) {
28+
<button type="submit" class="btn btn-danger" (click)="resetPhoto(user.id)">
29+
<i class="bi bi-x" aria-hidden="true"></i>
30+
<ng-container i18n>Delete photo</ng-container>
31+
</button>
32+
}
33+
</div>
3434
</div>
3535
</div>
36-
</div>
3736

38-
<h2 i18n>Other</h2>
39-
<div class="card card-body mb-4">
40-
@if (votesLeft >= 0) {
41-
<p><ng-container i18n>Votes per day</ng-container>: {{ votesPerDay }}</p>
42-
<p><ng-container i18n>Votes left</ng-container>: {{ votesLeft }}</p>
43-
}
44-
<form method="post" class="form-horizontal" (submit)="sendSettings()">
45-
<div class="mb-3 row">
46-
<label class="col-md-3 col-form-label" for="language" i18n>Language</label>
47-
<div class="col-md-9">
48-
<select
49-
name="language"
50-
id="language"
51-
class="form-select"
52-
[class.is-invalid]="settingsInvalidParams['language']"
53-
[(ngModel)]="settings.language"
54-
>
55-
@for (language of languages; track language.value) {
56-
<option [value]="language.value">{{ language.name }}</option>
37+
<h2 i18n>Other</h2>
38+
<div class="card card-body mb-4">
39+
@if (votesLeft >= 0) {
40+
<p><ng-container i18n>Votes per day</ng-container>: {{ votesPerDay }}</p>
41+
<p><ng-container i18n>Votes left</ng-container>: {{ votesLeft }}</p>
42+
}
43+
<form method="post" class="form-horizontal" (submit)="sendSettings(user.id)">
44+
<div class="mb-3 row">
45+
<label class="col-md-3 col-form-label" for="language" i18n>Language</label>
46+
<div class="col-md-9">
47+
<select
48+
name="language"
49+
id="language"
50+
class="form-select"
51+
[class.is-invalid]="settingsInvalidParams['language']"
52+
[(ngModel)]="settings.language"
53+
>
54+
@for (language of languages; track language.value) {
55+
<option [value]="language.value">{{ language.name }}</option>
56+
}
57+
</select>
58+
@for (message of settingsInvalidParams | invalidParams: 'language'; track message) {
59+
<p [textContent]="message" class="invalid-feedback"></p>
5760
}
58-
</select>
59-
@for (message of settingsInvalidParams | invalidParams: 'language'; track message) {
60-
<p [textContent]="message" class="invalid-feedback"></p>
61-
}
61+
</div>
6262
</div>
63-
</div>
64-
<div class="mb-3 row">
65-
<label class="col-md-3 col-form-label" for="timezone" i18n>Timezone</label>
66-
<div class="col-md-9">
67-
<select
68-
name="timezone"
69-
id="timezone"
70-
class="form-select"
71-
[(ngModel)]="settings.timezone"
72-
[class.is-invalid]="settingsInvalidParams['timezone']"
73-
>
74-
@for (timezone of timezones$ | async; track timezone) {
75-
<option [value]="timezone">{{ timezone }}</option>
63+
<div class="mb-3 row">
64+
<label class="col-md-3 col-form-label" for="timezone" i18n>Timezone</label>
65+
<div class="col-md-9">
66+
<select
67+
name="timezone"
68+
id="timezone"
69+
class="form-select"
70+
[(ngModel)]="settings.timezone"
71+
[class.is-invalid]="settingsInvalidParams['timezone']"
72+
>
73+
@for (timezone of timezones$ | async; track timezone) {
74+
<option [value]="timezone">{{ timezone }}</option>
75+
}
76+
</select>
77+
@for (message of settingsInvalidParams | invalidParams: 'timezone'; track message) {
78+
<p [textContent]="message" class="invalid-feedback"></p>
7679
}
77-
</select>
78-
@for (message of settingsInvalidParams | invalidParams: 'timezone'; track message) {
79-
<p [textContent]="message" class="invalid-feedback"></p>
80-
}
80+
</div>
8181
</div>
82-
</div>
83-
<div class="row">
84-
<div class="col-md-9 offset-md-3">
85-
<button type="submit" class="btn btn-primary" i18n>send</button>
82+
<div class="row">
83+
<div class="col-md-9 offset-md-3">
84+
<button type="submit" class="btn btn-primary" i18n>send</button>
85+
</div>
8686
</div>
87-
</div>
88-
</form>
89-
</div>
87+
</form>
88+
</div>
89+
}

src/app/account/profile/profile.component.ts

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import {AsyncPipe} from '@angular/common';
2-
import {HttpErrorResponse} from '@angular/common/http';
2+
import {HttpClient, HttpErrorResponse} from '@angular/common/http';
33
import {Component, ElementRef, inject, OnDestroy, OnInit, ViewChild} from '@angular/core';
44
import {FormsModule} from '@angular/forms';
55
import {environment} from '@environment/environment';
6-
import {APIImage, APIMeRequest, APIUser, UserFields} from '@grpc/spec.pb';
6+
import {APIImage, APIMeRequest, APIUser, DeleteUserPhotoRequest, UpdateUserRequest, UserFields} from '@grpc/spec.pb';
77
import {UsersClient} from '@grpc/spec.pbsc';
8-
import {APIService} from '@services/api.service';
8+
import {GrpcStatusEvent} from '@ngx-grpc/common';
9+
import {FieldMask} from '@ngx-grpc/well-known-types';
910
import {AuthService} from '@services/auth.service';
1011
import {LanguageService} from '@services/language';
1112
import {PageEnvService} from '@services/page-env.service';
@@ -16,6 +17,7 @@ import Keycloak from 'keycloak-js';
1617
import {EMPTY, of, Subscription} from 'rxjs';
1718
import {catchError, switchMap, tap} from 'rxjs/operators';
1819

20+
import {extractFieldViolations, fieldViolations2InvalidParams} from '../../grpc';
1921
import {ToastsService} from '../../toasts/toasts.service';
2022

2123
@Component({
@@ -24,7 +26,7 @@ import {ToastsService} from '../../toasts/toasts.service';
2426
templateUrl: './profile.component.html',
2527
})
2628
export class AccountProfileComponent implements OnDestroy, OnInit {
27-
readonly #api = inject(APIService);
29+
readonly #http = inject(HttpClient);
2830
readonly #languageService = inject(LanguageService);
2931
readonly #keycloak = inject(Keycloak);
3032
readonly #auth = inject(AuthService);
@@ -113,30 +115,37 @@ export class AccountProfileComponent implements OnDestroy, OnInit {
113115
this.#toastService.success($localize`Data saved`);
114116
}
115117

116-
protected sendSettings() {
118+
protected sendSettings(id: string) {
117119
this.settingsInvalidParams = {};
118120

119-
this.#api.request$<void>('PUT', 'user/me', {body: this.settings}).subscribe({
120-
error: (response: unknown) => {
121-
if (response instanceof HttpErrorResponse && response.status === 400) {
122-
this.settingsInvalidParams = response.error.invalid_params;
123-
} else {
124-
this.#toastService.handleError(response);
125-
}
126-
},
127-
next: () => {
128-
this.showSavedMessage();
129-
},
130-
});
121+
this.#usersClient
122+
.updateUser(
123+
new UpdateUserRequest({
124+
updateMask: new FieldMask({paths: ['language', 'timezone']}),
125+
user: new APIUser({
126+
id,
127+
language: this.settings.language || undefined,
128+
timezone: this.settings.timezone || undefined,
129+
}),
130+
}),
131+
)
132+
.subscribe({
133+
error: (response: unknown) => {
134+
if (response instanceof GrpcStatusEvent) {
135+
const fieldViolations = extractFieldViolations(response);
136+
this.settingsInvalidParams = fieldViolations2InvalidParams(fieldViolations);
137+
} else {
138+
this.#toastService.handleError(response);
139+
}
140+
},
141+
next: () => {
142+
this.showSavedMessage();
143+
},
144+
});
131145
}
132146

133-
/*protected showFileSelectDialog() {
134-
this.photoInvalidParams = {};
135-
this.fileInput.nativeElement.click();
136-
}*/
137-
138-
protected resetPhoto() {
139-
this.#api.request$('DELETE', 'user/me/photo').subscribe({
147+
protected resetPhoto(id: string) {
148+
this.#usersClient.deleteUserPhoto(new DeleteUserPhotoRequest({id})).subscribe({
140149
error: (response: unknown) => this.#toastService.handleError(response),
141150
next: () => {
142151
if (this.user) {
@@ -158,8 +167,8 @@ export class AccountProfileComponent implements OnDestroy, OnInit {
158167
const formData: FormData = new FormData();
159168
formData.append('file', file);
160169

161-
return this.#api
162-
.request$('POST', 'user/me/photo', {body: formData})
170+
return this.#http
171+
.request('POST', '/api/user/me/photo', {body: formData})
163172
.pipe(
164173
catchError((response: unknown) => {
165174
if (this.input) {

src/app/app.config.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {NgbCollapseModule, NgbDropdownModule, NgbModule, NgbTooltipModule} from
1010
import {GRPC_INTERCEPTORS, GrpcCoreModule} from '@ngx-grpc/core';
1111
import {GrpcWebClientModule} from '@ngx-grpc/grpc-web-client';
1212
import {ACLService, APIACL} from '@services/acl.service';
13-
import {APIService, authInterceptor$, GrpcAuthInterceptor, GrpcLogInterceptor} from '@services/api.service';
13+
import {authInterceptor$, GrpcAuthInterceptor, GrpcLogInterceptor} from '@services/api.service';
1414
import {AuthService} from '@services/auth.service';
1515
import {ContactsService} from '@services/contacts';
1616
import {ContentLanguageService} from '@services/content-language';
@@ -71,7 +71,6 @@ export const appConfig: ApplicationConfig = {
7171
}),
7272
AutoRefreshTokenService,
7373
UserActivityService,
74-
APIService,
7574
APIACL,
7675
AuthService,
7776
ACLService,

src/app/catalogue/vehicles/pictures/picture/picture.component.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@ import {PicturesClient} from '@grpc/spec.pbsc';
1717
import {LanguageService} from '@services/language';
1818
import {PageEnvService} from '@services/page-env.service';
1919
import {BehaviorSubject, combineLatest, EMPTY, Observable, of} from 'rxjs';
20-
import {debounceTime, distinctUntilChanged, map, shareReplay, switchMap, tap} from 'rxjs/operators';
20+
import {catchError, debounceTime, distinctUntilChanged, map, shareReplay, switchMap, tap} from 'rxjs/operators';
2121

2222
import {CommentsComponent} from '../../../../comments/comments/comments.component';
2323
import {PictureComponent} from '../../../../picture/picture.component';
24+
import {ToastsService} from '../../../../toasts/toasts.service';
2425
import {CatalogueService} from '../../../catalogue-service';
2526

2627
@Component({
@@ -35,6 +36,7 @@ export class CatalogueVehiclesPicturesPictureComponent {
3536
readonly #router = inject(Router);
3637
readonly #picturesClient = inject(PicturesClient);
3738
readonly #languageService = inject(LanguageService);
39+
readonly #toastService = inject(ToastsService);
3840

3941
readonly #changed$ = new BehaviorSubject<void>(void 0);
4042

@@ -145,6 +147,10 @@ export class CatalogueVehiclesPicturesPictureComponent {
145147
}),
146148
);
147149
}),
150+
catchError((err: unknown) => {
151+
this.#toastService.handleError(err);
152+
return EMPTY;
153+
}),
148154
switchMap((picture) => {
149155
if (!picture) {
150156
this.#router.navigate(['/error-404'], {

src/app/categories/list-item.component.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import {AsyncPipe} from '@angular/common';
22
import {Component, inject, Input} from '@angular/core';
33
import {RouterLink} from '@angular/router';
4-
import {APIItem, ItemType, Picture} from '@grpc/spec.pb';
4+
import {APIImage, APIItem, ItemType, Picture} from '@grpc/spec.pb';
55
import {ACLService, Privilege, Resource} from '@services/acl.service';
6-
import {APIImage} from '@services/api.service';
76
import {ItemHeaderComponent} from '@utils/item-header/item-header.component';
87
import {MarkdownComponent} from '@utils/markdown/markdown.component';
98
import {BehaviorSubject, combineLatest, EMPTY, Observable, of} from 'rxjs';

src/app/chart/chart.component.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ <h1>Charts</h1>
1212

1313
<div>
1414
<div class="nav nav-pills">
15-
@for (param of parameters; track param) {
15+
@for (param of parameters; track param.id; let idx = $index) {
1616
<a
17-
[class.active]="param.active"
17+
[class.active]="idx === activeParameter"
1818
class="nav-link"
1919
href="#"
20-
(click)="selectParam(param)"
20+
(click)="selectParam(idx)"
2121
[textContent]="param.name"
2222
></a>
2323
}

0 commit comments

Comments
 (0)