Browse Source

Task/improve type safety in user account access component (#6934)

* feat(client): resolve errors

* feat(client): replace constructor based DI with inject function

* feat(client): enforce encapsulation

* feat(client): enforce immutability

* feat(client): replace deprecated getDeviceInfo

* fix(client): use AccessPermission enum to replace hard coded string
pull/6938/head
Kenrick Tandrian 1 day ago
committed by GitHub
parent
commit
0e15d14861
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 35
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts
  2. 2
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/interfaces/interfaces.ts
  3. 93
      apps/client/src/app/components/user-account-access/user-account-access.component.ts

35
apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts

@ -29,6 +29,7 @@ import {
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { AccessPermission } from '@prisma/client';
import { StatusCodes } from 'http-status-codes'; import { StatusCodes } from 'http-status-codes';
import { EMPTY, catchError } from 'rxjs'; import { EMPTY, catchError } from 'rxjs';
@ -52,7 +53,7 @@ import { CreateOrUpdateAccessDialogParams } from './interfaces/interfaces';
}) })
export class GfCreateOrUpdateAccessDialogComponent implements OnInit { export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
protected accessForm: FormGroup; protected accessForm: FormGroup;
protected mode: 'create' | 'update'; protected readonly mode: 'create' | 'update';
private readonly changeDetectorRef = inject(ChangeDetectorRef); private readonly changeDetectorRef = inject(ChangeDetectorRef);
@ -69,21 +70,25 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
private readonly notificationService = inject(NotificationService); private readonly notificationService = inject(NotificationService);
public constructor() { public constructor() {
this.mode = this.data.access?.id ? 'update' : 'create'; this.mode = this.data.access ? 'update' : 'create';
} }
public ngOnInit() { public ngOnInit() {
const isPublic = this.data.access.type === 'PUBLIC'; const access = this.data?.access;
const isPublic = access?.type === 'PUBLIC';
this.accessForm = this.formBuilder.group({ this.accessForm = this.formBuilder.group({
alias: [this.data.access.alias], alias: [access?.alias ?? ''],
granteeUserId: [ granteeUserId: [
this.data.access.grantee, access?.grantee ?? null,
isPublic ? null : Validators.required isPublic ? null : Validators.required
], ],
permissions: [this.data.access.permissions[0], Validators.required], permissions: [
access?.permissions[0] ?? AccessPermission.READ_RESTRICTED,
Validators.required
],
type: [ type: [
{ disabled: this.mode === 'update', value: this.data.access.type }, { disabled: this.mode === 'update', value: access?.type ?? 'PRIVATE' },
Validators.required Validators.required
] ]
}); });
@ -100,7 +105,9 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
} else { } else {
granteeUserIdControl?.clearValidators(); granteeUserIdControl?.clearValidators();
granteeUserIdControl?.setValue(null); granteeUserIdControl?.setValue(null);
permissionsControl?.setValue(this.data.access.permissions[0]); permissionsControl?.setValue(
access?.permissions[0] ?? AccessPermission.READ_RESTRICTED
);
} }
granteeUserIdControl?.updateValueAndValidity(); granteeUserIdControl?.updateValueAndValidity();
@ -109,11 +116,11 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
}); });
} }
public onCancel() { protected onCancel() {
this.dialogRef.close(); this.dialogRef.close();
} }
public async onSubmit() { protected async onSubmit() {
if (this.mode === 'create') { if (this.mode === 'create') {
await this.createAccess(); await this.createAccess();
} else { } else {
@ -158,10 +165,16 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
} }
private async updateAccess() { private async updateAccess() {
const accessId = this.data.access?.id;
if (!accessId) {
return;
}
const access: UpdateAccessDto = { const access: UpdateAccessDto = {
alias: this.accessForm.get('alias')?.value, alias: this.accessForm.get('alias')?.value,
granteeUserId: this.accessForm.get('granteeUserId')?.value, granteeUserId: this.accessForm.get('granteeUserId')?.value,
id: this.data.access.id, id: accessId,
permissions: [this.accessForm.get('permissions')?.value] permissions: [this.accessForm.get('permissions')?.value]
}; };

2
apps/client/src/app/components/user-account-access/create-or-update-access-dialog/interfaces/interfaces.ts

@ -1,5 +1,5 @@
import { Access } from '@ghostfolio/common/interfaces'; import { Access } from '@ghostfolio/common/interfaces';
export interface CreateOrUpdateAccessDialogParams { export interface CreateOrUpdateAccessDialogParams {
access: Access; access?: Access;
} }

93
apps/client/src/app/components/user-account-access/user-account-access.component.ts

@ -12,12 +12,19 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
computed,
CUSTOM_ELEMENTS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA,
DestroyRef, DestroyRef,
inject,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; import {
FormControl,
FormGroup,
ReactiveFormsModule,
Validators
} from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
@ -53,30 +60,35 @@ import { CreateOrUpdateAccessDialogParams } from './create-or-update-access-dial
templateUrl: './user-account-access.html' templateUrl: './user-account-access.html'
}) })
export class GfUserAccountAccessComponent implements OnInit { export class GfUserAccountAccessComponent implements OnInit {
public accessesGet: Access[]; protected accessesGet: Access[];
public accessesGive: Access[]; protected accessesGive: Access[];
public deviceType: string; protected hasPermissionToCreateAccess: boolean;
public hasPermissionToCreateAccess: boolean; protected hasPermissionToDeleteAccess: boolean;
public hasPermissionToDeleteAccess: boolean; protected hasPermissionToUpdateOwnAccessToken: boolean;
public hasPermissionToUpdateOwnAccessToken: boolean; protected isAccessTokenHidden = true;
public isAccessTokenHidden = true; protected readonly updateOwnAccessTokenForm = new FormGroup({
public updateOwnAccessTokenForm = this.formBuilder.group({ accessToken: new FormControl<string>('', {
accessToken: ['', Validators.required] nonNullable: true,
validators: [Validators.required]
})
}); });
public user: User; protected user: User;
public constructor( private readonly deviceType = computed(
private changeDetectorRef: ChangeDetectorRef, () => this.deviceDetectorService.deviceInfo().deviceType
private dataService: DataService, );
private destroyRef: DestroyRef,
private deviceDetectorService: DeviceDetectorService, private readonly changeDetectorRef = inject(ChangeDetectorRef);
private dialog: MatDialog, private readonly dataService = inject(DataService);
private formBuilder: FormBuilder, private readonly destroyRef = inject(DestroyRef);
private notificationService: NotificationService, private readonly deviceDetectorService = inject(DeviceDetectorService);
private route: ActivatedRoute, private readonly dialog = inject(MatDialog);
private router: Router, private readonly notificationService = inject(NotificationService);
private userService: UserService private readonly route = inject(ActivatedRoute);
) { private readonly router = inject(Router);
private readonly userService = inject(UserService);
public constructor() {
const { globalPermissions } = this.dataService.fetchInfo(); const { globalPermissions } = this.dataService.fetchInfo();
this.hasPermissionToDeleteAccess = hasPermission( this.hasPermissionToDeleteAccess = hasPermission(
@ -123,12 +135,10 @@ export class GfUserAccountAccessComponent implements OnInit {
} }
public ngOnInit() { public ngOnInit() {
this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType;
this.update(); this.update();
} }
public onDeleteAccess(aId: string) { protected onDeleteAccess(aId: string) {
this.dataService this.dataService
.deleteAccess(aId) .deleteAccess(aId)
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
@ -139,12 +149,13 @@ export class GfUserAccountAccessComponent implements OnInit {
}); });
} }
public onGenerateAccessToken() { protected onGenerateAccessToken() {
this.notificationService.confirm({ this.notificationService.confirm({
confirmFn: () => { confirmFn: () => {
this.dataService this.dataService
.updateOwnAccessToken({ .updateOwnAccessToken({
accessToken: this.updateOwnAccessTokenForm.get('accessToken').value accessToken:
this.updateOwnAccessTokenForm.controls.accessToken.value
}) })
.pipe( .pipe(
catchError(() => { catchError(() => {
@ -173,7 +184,7 @@ export class GfUserAccountAccessComponent implements OnInit {
}); });
} }
public onUpdateAccess(aId: string) { protected onUpdateAccess(aId: string) {
this.router.navigate([], { this.router.navigate([], {
queryParams: { accessId: aId, editDialog: true } queryParams: { accessId: aId, editDialog: true }
}); });
@ -184,17 +195,9 @@ export class GfUserAccountAccessComponent implements OnInit {
GfCreateOrUpdateAccessDialogComponent, GfCreateOrUpdateAccessDialogComponent,
CreateOrUpdateAccessDialogParams CreateOrUpdateAccessDialogParams
>(GfCreateOrUpdateAccessDialogComponent, { >(GfCreateOrUpdateAccessDialogComponent, {
data: { data: {} satisfies CreateOrUpdateAccessDialogParams,
access: { height: this.deviceType() === 'mobile' ? '98vh' : undefined,
alias: '', width: this.deviceType() === 'mobile' ? '100vw' : '50rem'
grantee: null,
id: null,
permissions: ['READ_RESTRICTED'],
type: 'PRIVATE'
}
},
height: this.deviceType === 'mobile' ? '98vh' : undefined,
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
}); });
dialogRef.afterClosed().subscribe((access: CreateAccessDto | null) => { dialogRef.afterClosed().subscribe((access: CreateAccessDto | null) => {
@ -222,14 +225,14 @@ export class GfUserAccountAccessComponent implements OnInit {
data: { data: {
access: { access: {
alias: access.alias, alias: access.alias,
grantee: access.grantee === 'Public' ? null : access.grantee, grantee: access.grantee === 'Public' ? undefined : access.grantee,
id: access.id, id: access.id,
permissions: access.permissions, permissions: access.permissions,
type: access.type type: access.type
} }
}, } satisfies CreateOrUpdateAccessDialogParams,
height: this.deviceType === 'mobile' ? '98vh' : undefined, height: this.deviceType() === 'mobile' ? '98vh' : undefined,
width: this.deviceType === 'mobile' ? '100vw' : '50rem' width: this.deviceType() === 'mobile' ? '100vw' : '50rem'
}); });
dialogRef.afterClosed().subscribe((result) => { dialogRef.afterClosed().subscribe((result) => {
@ -244,9 +247,9 @@ export class GfUserAccountAccessComponent implements OnInit {
private update() { private update() {
this.accessesGet = this.user.access.map(({ alias, id, permissions }) => { this.accessesGet = this.user.access.map(({ alias, id, permissions }) => {
return { return {
alias,
id, id,
permissions, permissions,
alias: alias ?? '',
grantee: $localize`Me`, grantee: $localize`Me`,
type: 'PRIVATE' type: 'PRIVATE'
}; };

Loading…
Cancel
Save