mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-15 18:02:13 +00:00
fix(console): remove cropper, set avatar url if available (#1865)
* set avatarurl if available * lint * force sidemargin * dont load image via asset * rm log * stylelint * add ZITADEL domain to csp img src * sanitize url * fix undefined link projects * use name as fallback * operator: rename uploadServiceURL to assetServiceURL in environment json for console * remove data * rm logs * center crop image * add avatar to changes Co-authored-by: Livio Amstutz <livio.a@gmail.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
<app-avatar [routerLink]="['/users/me']"
|
||||
*ngIf="user && (user.human?.profile?.displayName || (user.human?.profile?.firstName && user.human?.profile?.lastName))"
|
||||
class="avatar"
|
||||
[avatarUrl]="user.human?.profile?.avatarUrl || ''"
|
||||
[forColor]="user?.preferredLoginName"
|
||||
[name]="user.human?.profile?.displayName ? user.human?.profile?.displayName : (user.human?.profile?.firstName + ' '+ user.human?.profile?.lastName)"
|
||||
[size]="100">
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns;"
|
||||
[routerLink]="['/granted-projects', row.projectId, 'grant', row.id]"></tr>
|
||||
[routerLink]="['/granted-projects', row.projectId, 'grant', row.grantId]"></tr>
|
||||
|
||||
</table>
|
||||
<div *ngIf="(loading$ | async) == false && !dataSource?.data?.length" class="no-content-row">
|
||||
|
||||
@@ -66,9 +66,9 @@
|
||||
<ng-container matColumnDef="actions" stickyEnd>
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell *matCellDef="let project">
|
||||
<button class="dlt-button" *ngIf="project.projectId !== zitadelProjectId" color="warn"
|
||||
<button class="dlt-button" *ngIf="project.id !== zitadelProjectId" color="warn"
|
||||
mat-icon-button matTooltip="{{'ACTIONS.DELETE' | translate}}"
|
||||
(click)="deleteProject(project.projectId)">
|
||||
(click)="deleteProject(project.id)">
|
||||
<i class="las la-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
@@ -76,7 +76,7 @@
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns;"
|
||||
[routerLink]="['/projects', row.projectId]"></tr>
|
||||
[routerLink]="['/projects', row.id]"></tr>
|
||||
|
||||
</table>
|
||||
<div *ngIf="(loading$ | async) == false && !dataSource?.data?.length" class="no-content-row">
|
||||
|
||||
@@ -5,10 +5,9 @@
|
||||
<div class="i-wrapper" *ngIf="showEditImage">
|
||||
<i class="las la-camera"></i>
|
||||
</div>
|
||||
<img class="pic" [src]="profilePic" *ngIf="profilePic"/>
|
||||
<app-avatar
|
||||
*ngIf="!profilePic && user && user.profile?.displayName && user.profile?.firstName && user?.profile.lastName"
|
||||
class="avatar" [name]="user.profile?.displayName" [forColor]="preferredLoginName" [size]="80">
|
||||
*ngIf="user && user.profile?.displayName && user.profile?.firstName && user?.profile.lastName"
|
||||
class="avatar" [name]="user.profile?.displayName" [avatarUrl]="user?.profile?.avatarUrl || ''" [forColor]="preferredLoginName" [size]="80">
|
||||
</app-avatar>
|
||||
</button>
|
||||
|
||||
|
||||
@@ -46,13 +46,6 @@
|
||||
background-color: #00000080;
|
||||
}
|
||||
}
|
||||
|
||||
.pic {
|
||||
height: 80px;
|
||||
width: 80px;
|
||||
object-fit: contain;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.formfield {
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { Component, EventEmitter, Input, OnChanges, OnDestroy, Output } from '@angular/core';
|
||||
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { Gender, Human, User } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
import { AssetService } from 'src/app/services/asset.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
import { ProfilePictureComponent } from './profile-picture/profile-picture.component';
|
||||
|
||||
@@ -25,7 +22,6 @@ export class DetailFormComponent implements OnDestroy, OnChanges {
|
||||
@Output() public submitData: EventEmitter<User> = new EventEmitter<User>();
|
||||
@Output() public changedLanguage: EventEmitter<string> = new EventEmitter<string>();
|
||||
|
||||
public profilePic: any = null;
|
||||
public profileForm!: FormGroup;
|
||||
|
||||
private sub: Subscription = new Subscription();
|
||||
@@ -33,9 +29,6 @@ export class DetailFormComponent implements OnDestroy, OnChanges {
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private dialog: MatDialog,
|
||||
private assetService: AssetService,
|
||||
private toast: ToastService,
|
||||
private sanitizer: DomSanitizer,
|
||||
) {
|
||||
this.profileForm = this.fb.group({
|
||||
userName: [{ value: '', disabled: true }, [
|
||||
@@ -48,8 +41,6 @@ export class DetailFormComponent implements OnDestroy, OnChanges {
|
||||
gender: [{ value: 0, disabled: this.disabled }],
|
||||
preferredLanguage: [{ value: '', disabled: this.disabled }],
|
||||
});
|
||||
|
||||
this.loadAvatar();
|
||||
}
|
||||
|
||||
public ngOnChanges(): void {
|
||||
@@ -85,7 +76,7 @@ export class DetailFormComponent implements OnDestroy, OnChanges {
|
||||
public openUploadDialog(): void {
|
||||
const dialogRef = this.dialog.open(ProfilePictureComponent, {
|
||||
data: {
|
||||
profilePic: this.profilePic,
|
||||
profilePic: this.user.profile?.avatarUrl,
|
||||
},
|
||||
width: '400px',
|
||||
});
|
||||
@@ -96,15 +87,6 @@ export class DetailFormComponent implements OnDestroy, OnChanges {
|
||||
});
|
||||
}
|
||||
|
||||
public loadAvatar(): Promise<any> {
|
||||
return this.assetService.load(`users/me/avatar`).then(data => {
|
||||
const objectURL = URL.createObjectURL(data);
|
||||
this.profilePic = this.sanitizer.bypassSecurityTrustUrl(objectURL);
|
||||
}).catch(error => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
public get userName(): AbstractControl | null {
|
||||
return this.profileForm.get('userName');
|
||||
}
|
||||
|
||||
@@ -2,38 +2,15 @@
|
||||
<div mat-dialog-content>
|
||||
<p class="desc">{{'USER.PROFILE.AVATAR.CURRENT' | translate}}</p>
|
||||
<div class="current-pic-wrapper">
|
||||
<img class="pic" [src]="data.profilePic" *ngIf="data.profilePic"/>
|
||||
<span class="fill-space"></span>
|
||||
<input #selectedFile style="display: none;" class="file-input" type="file" (change)="onDrop($event)">
|
||||
<button class="btn" mat-stroked-button type="button" (click)="selectedFile.click();">{{'USER.PROFILE.AVATAR.UPLOADBTN' | translate}}</button>
|
||||
<button *ngIf="data.profilePic" matTooltip="{{'ACTIONS.DELETE' | translate}}" color="warn" (click)="deletePic()" mat-icon-button><mat-icon>remove_circle</mat-icon></button>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="imageChangedEvent">
|
||||
<p class="desc">{{'USER.PROFILE.AVATAR.PREVIEW' | translate}}</p>
|
||||
|
||||
<div class="cropped-preview" *ngIf="croppedImage">
|
||||
<img class="pic" [src]="croppedImage"/>
|
||||
<button color="primary" mat-raised-button (click)="upload()">{{'USER.PROFILE.AVATAR.UPLOAD' | translate}}</button>
|
||||
</div>
|
||||
|
||||
<p class="error" *ngIf="showCropperError">{{'USER.PROFILE.AVATAR.CROPPERERROR' | translate}}</p>
|
||||
|
||||
<image-cropper
|
||||
class="cropper"
|
||||
[imageChangedEvent]="imageChangedEvent"
|
||||
[maintainAspectRatio]="true"
|
||||
[aspectRatio]="4 / 4"
|
||||
[roundCropper]="true"
|
||||
[autoCrop]="true"
|
||||
(imageCropped)="imageCropped($event)"
|
||||
(loadImageFailed)="loadImageFailed()"
|
||||
></image-cropper>
|
||||
</ng-container>
|
||||
|
||||
<img class="pic" [src]="data.profilePic" *ngIf="data.profilePic"/>
|
||||
<span class="fill-space"></span>
|
||||
<input #selectedFile style="display: none;" class="file-input" type="file" (change)="onDrop($event)">
|
||||
<button class="btn" mat-raised-button color="primary" type="button" (click)="selectedFile.click();">{{'USER.PROFILE.AVATAR.UPLOADBTN' | translate}}</button>
|
||||
<button *ngIf="data.profilePic" matTooltip="{{'ACTIONS.DELETE' | translate}}" color="warn" (click)="deletePic()" mat-icon-button><mat-icon>remove_circle</mat-icon></button>
|
||||
</div>
|
||||
</div>
|
||||
<div mat-dialog-actions class="action">
|
||||
<button color="primary" mat-raised-button class="ok-button" (click)="closeDialog()">
|
||||
<button color="primary" mat-stroked-button class="ok-button" (click)="closeDialog()">
|
||||
{{'ACTIONS.CLOSE' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
@@ -22,7 +22,8 @@
|
||||
.pic {
|
||||
height: 80px;
|
||||
width: 80px;
|
||||
object-fit: contain;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
border-radius: 50%;
|
||||
background-color: #00000030;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { ImageCroppedEvent } from 'ngx-image-cropper';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { AssetService } from 'src/app/services/asset.service';
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
@@ -10,40 +10,29 @@ import { ToastService } from 'src/app/services/toast.service';
|
||||
templateUrl: './profile-picture.component.html',
|
||||
styleUrls: ['./profile-picture.component.scss'],
|
||||
})
|
||||
export class ProfilePictureComponent implements OnInit {
|
||||
public isHovering: boolean = false;
|
||||
|
||||
public imageChangedEvent: any = '';
|
||||
public imageChangedFormat: string = '';
|
||||
public croppedImage: any = '';
|
||||
public showCropperError: boolean = false;
|
||||
|
||||
export class ProfilePictureComponent {
|
||||
constructor(
|
||||
private authService: GrpcAuthService,
|
||||
public dialogRef: MatDialogRef<ProfilePictureComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
private toast: ToastService,
|
||||
private assetService: AssetService) { }
|
||||
|
||||
public ngOnInit(): void {
|
||||
}
|
||||
|
||||
public toggleHover(isHovering: boolean): void {
|
||||
this.isHovering = isHovering;
|
||||
private assetService: AssetService,
|
||||
private sanitizer: DomSanitizer,
|
||||
) {
|
||||
}
|
||||
|
||||
public onDrop(event: any): Promise<any> | void {
|
||||
const filelist: FileList = event.target.files;
|
||||
console.log(event.target.files);
|
||||
this.imageChangedEvent = event;
|
||||
const file = filelist.item(0);
|
||||
|
||||
if (file) {
|
||||
this.imageChangedFormat = file.type;
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
return this.handleUploadPromise(this.assetService.upload('users/me/avatar', formData));
|
||||
}
|
||||
}
|
||||
|
||||
public deletePic(): void {
|
||||
console.log('delete');
|
||||
this.authService.removeMyAvatar().then(() => {
|
||||
this.toast.showInfo('USER.PROFILE.AVATAR.DELETESUCCESS', true);
|
||||
this.data.profilePic = null;
|
||||
@@ -55,45 +44,17 @@ export class ProfilePictureComponent implements OnInit {
|
||||
private handleUploadPromise(task: Promise<any>): Promise<any> {
|
||||
return task.then(() => {
|
||||
this.toast.showInfo('POLICY.TOAST.UPLOADSUCCESS', true);
|
||||
this.data.profilePic = this.croppedImage;
|
||||
this.assetService.load('users/me/avatar').then(data => {
|
||||
const objectURL = URL.createObjectURL(data);
|
||||
const pic = this.sanitizer.bypassSecurityTrustUrl(objectURL);
|
||||
this.data.profilePic = pic;
|
||||
}).catch(error => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}).catch(error => this.toast.showError(error));
|
||||
}
|
||||
|
||||
public fileChangeEvent(event: any): void {
|
||||
this.imageChangedEvent = event;
|
||||
}
|
||||
|
||||
public imageCropped(event: ImageCroppedEvent): void {
|
||||
this.showCropperError = false;
|
||||
this.croppedImage = event.base64;
|
||||
}
|
||||
|
||||
public upload(): Promise<any> | void {
|
||||
const formData = new FormData();
|
||||
const splitted = this.croppedImage.split(';base64,');
|
||||
if (splitted[1]) {
|
||||
const blob = this.base64toBlob(splitted[1]);
|
||||
formData.append('file', blob);
|
||||
return this.handleUploadPromise(this.assetService.upload('users/me/avatar', formData));
|
||||
}
|
||||
}
|
||||
|
||||
public loadImageFailed(): void {
|
||||
this.showCropperError = true;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
|
||||
public base64toBlob(b64: string): Blob {
|
||||
const byteCharacters = atob(b64);
|
||||
const byteNumbers = new Array(byteCharacters.length);
|
||||
for (let i = 0; i < byteCharacters.length; i++) {
|
||||
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
||||
}
|
||||
const byteArray = new Uint8Array(byteNumbers);
|
||||
const blob = new Blob([byteArray], { type: this.imageChangedFormat });
|
||||
return blob;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
<app-avatar
|
||||
*ngIf="user.human && user.human.profile.displayName && user.human?.profile.firstName && user.human?.profile.lastName; else cog"
|
||||
class="avatar" [name]="user.human.profile.displayName" [forColor]="user?.preferredLoginName" [size]="32">
|
||||
class="avatar" [name]="user.human.profile.displayName" [avatarUrl]="user.human?.profile?.avatarUrl || ''" [forColor]="user?.preferredLoginName" [size]="32">
|
||||
</app-avatar>
|
||||
<ng-template #cog>
|
||||
<div class="sa-icon" *ngIf="user.machine">
|
||||
|
||||
Reference in New Issue
Block a user