mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-30 11:02:16 +00:00
feat: Use V2 API's in Console (#9312)
# Which Problems Are Solved Solves #8976 # Additional Changes I have done some intensive refactorings and we are using the new @zitadel/client package for GRPC access. # Additional Context - Closes #8976 --------- Co-authored-by: Max Peintner <[email protected]>
This commit is contained in:
@@ -4,6 +4,7 @@ import { AuthConfig } from 'angular-oauth2-oidc';
|
||||
import { Session, User, UserState } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
import { AuthenticationService } from 'src/app/services/authentication.service';
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-accounts-card',
|
||||
@@ -18,6 +19,8 @@ export class AccountsCardComponent implements OnInit {
|
||||
public sessions: Session.AsObject[] = [];
|
||||
public loadingUsers: boolean = false;
|
||||
public UserState: any = UserState;
|
||||
private labelpolicy = toSignal(this.userService.labelpolicy$, { initialValue: undefined });
|
||||
|
||||
constructor(
|
||||
public authService: AuthenticationService,
|
||||
private router: Router,
|
||||
@@ -68,7 +71,7 @@ export class AccountsCardComponent implements OnInit {
|
||||
}
|
||||
|
||||
public logout(): void {
|
||||
const lP = JSON.stringify(this.userService.labelpolicy.getValue());
|
||||
const lP = JSON.stringify(this.labelpolicy());
|
||||
localStorage.setItem('labelPolicyOnSignout', lP);
|
||||
|
||||
this.authService.signout();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="footer-wrapper">
|
||||
<div class="footer-row">
|
||||
<div class="footer-links" *ngIf="authService.privacypolicy | async as pP">
|
||||
<div class="footer-links" *ngIf="authService.privacypolicy$ | async as pP">
|
||||
<a target="_blank" *ngIf="pP?.tosLink" rel="noreferrer" [href]="pP?.tosLink" external>
|
||||
<span>{{ 'FOOTER.LINKS.TOS' | translate }}</span>
|
||||
<i class="las la-external-link-alt"></i>
|
||||
@@ -11,7 +11,7 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="footer-socials" *ngIf="authService.labelpolicy | async as lP">
|
||||
<div class="footer-socials" *ngIf="authService.labelpolicy$ | async as lP">
|
||||
<ng-container *ngIf="lP?.disableWatermark === false">
|
||||
<a target="_blank" rel="noreferrer" href="https://github.com/zitadel">
|
||||
<i class="text-3xl lab la-github"></i>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<mat-spinner [diameter]="20"></mat-spinner>
|
||||
</div>
|
||||
<ng-template #logo>
|
||||
<a class="title custom" [routerLink]="['/']" *ngIf="authService.labelpolicy | async as lP; else defaultHome">
|
||||
<a class="title custom" [routerLink]="['/']" *ngIf="authService.labelpolicy$ | async as lP; else defaultHome">
|
||||
<img
|
||||
class="logo"
|
||||
alt="home logo"
|
||||
@@ -24,7 +24,7 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-template #defaultHome>
|
||||
<a class="title" [routerLink]="authService.zitadelPermissions.getValue().length === 0 ? ['/users', 'me'] : ['/']">
|
||||
<a class="title" [routerLink]="(authService.zitadelPermissions | async)?.length ? ['/'] : ['/users', 'me']">
|
||||
<img
|
||||
class="logo"
|
||||
alt="zitadel logo"
|
||||
@@ -168,7 +168,7 @@
|
||||
|
||||
<span class="fill-space"></span>
|
||||
|
||||
<ng-container *ngIf="authService.privacypolicy | async as pP">
|
||||
<ng-container *ngIf="authService.privacypolicy$ | async as pP">
|
||||
<a class="custom-link" *ngIf="pP.customLink" href="{{ pP.customLink }}" mat-stroked-button target="_blank">
|
||||
{{ pP.customLinkText }}
|
||||
</a>
|
||||
|
||||
@@ -9,26 +9,26 @@
|
||||
inactive: user.state === UserState.USER_STATE_INACTIVE,
|
||||
}"
|
||||
>
|
||||
{{ 'USER.DATA.STATE' + user.state | translate }}
|
||||
{{ (isV2(user) ? 'USER.STATEV2.' : 'USER.STATE.') + user.state | translate }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="info-wrapper">
|
||||
<p class="info-row-title">{{ 'USER.ID' | translate }}</p>
|
||||
<p *ngIf="user && user.id" class="info-row-desc">{{ user.id }}</p>
|
||||
<p *ngIf="userId" class="info-row-desc">{{ userId }}</p>
|
||||
</div>
|
||||
|
||||
<div class="info-wrapper">
|
||||
<p class="info-row-title">{{ 'USER.DETAILS.DATECREATED' | translate }}</p>
|
||||
<p *ngIf="user && user.details && user.details.creationDate" class="info-row-desc">
|
||||
{{ user.details.creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
|
||||
<p *ngIf="creationDate" class="info-row-desc">
|
||||
{{ creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="info-wrapper">
|
||||
<p class="info-row-title">{{ 'USER.DETAILS.DATECHANGED' | translate }}</p>
|
||||
<p *ngIf="user && user.details && user.details.changeDate" class="info-row-desc">
|
||||
{{ user.details.changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
|
||||
<p *ngIf="changeDate" class="info-row-desc">
|
||||
{{ changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ import { Org, OrgState } from 'src/app/proto/generated/zitadel/org_pb';
|
||||
import { LoginPolicy } from 'src/app/proto/generated/zitadel/policy_pb';
|
||||
import { GrantedProject, Project, ProjectGrantState, ProjectState } from 'src/app/proto/generated/zitadel/project_pb';
|
||||
import { User, UserState } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
import { User as UserV1 } from '@zitadel/proto/zitadel/user_pb';
|
||||
import { User as UserV2 } from '@zitadel/proto/zitadel/user/v2/user_pb';
|
||||
import { LoginPolicy as LoginPolicyV2 } from '@zitadel/proto/zitadel/policy_pb';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-info-row',
|
||||
@@ -13,14 +16,14 @@ import { User, UserState } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
styleUrls: ['./info-row.component.scss'],
|
||||
})
|
||||
export class InfoRowComponent {
|
||||
@Input() public user!: User.AsObject;
|
||||
@Input() public user?: User.AsObject | UserV2 | UserV1;
|
||||
@Input() public org!: Org.AsObject;
|
||||
@Input() public instance!: InstanceDetail.AsObject;
|
||||
@Input() public app!: App.AsObject;
|
||||
@Input() public idp!: IDP.AsObject;
|
||||
@Input() public project!: Project.AsObject;
|
||||
@Input() public grantedProject!: GrantedProject.AsObject;
|
||||
@Input() public loginPolicy?: LoginPolicy.AsObject;
|
||||
@Input() public loginPolicy?: LoginPolicy.AsObject | LoginPolicyV2;
|
||||
|
||||
public UserState: any = UserState;
|
||||
public State: any = State;
|
||||
@@ -35,25 +38,77 @@ export class InfoRowComponent {
|
||||
constructor() {}
|
||||
|
||||
public get loginMethods(): Set<string> {
|
||||
const methods = this.user?.loginNamesList;
|
||||
let email: string = '';
|
||||
let phone: string = '';
|
||||
if (this.loginPolicy) {
|
||||
if (
|
||||
!this.loginPolicy?.disableLoginWithEmail &&
|
||||
this.user.human?.email?.email &&
|
||||
this.user.human.email.isEmailVerified
|
||||
) {
|
||||
email = this.user.human?.email?.email;
|
||||
}
|
||||
if (
|
||||
!this.loginPolicy?.disableLoginWithPhone &&
|
||||
this.user.human?.phone?.phone &&
|
||||
this.user.human.phone.isPhoneVerified
|
||||
) {
|
||||
phone = this.user.human?.phone?.phone;
|
||||
}
|
||||
if (!this.user) {
|
||||
return new Set();
|
||||
}
|
||||
return new Set([email, phone, ...methods].filter((method) => !!method));
|
||||
|
||||
const methods = '$typeName' in this.user ? this.user.loginNames : this.user.loginNamesList;
|
||||
|
||||
const loginPolicy = this.loginPolicy;
|
||||
if (!loginPolicy) {
|
||||
return new Set([...methods]);
|
||||
}
|
||||
|
||||
let email = !loginPolicy.disableLoginWithEmail ? this.getEmail(this.user) : '';
|
||||
let phone = !loginPolicy.disableLoginWithPhone ? this.getPhone(this.user) : '';
|
||||
|
||||
return new Set([email, phone, ...methods].filter(Boolean));
|
||||
}
|
||||
|
||||
public get userId() {
|
||||
if (!this.user) {
|
||||
return undefined;
|
||||
}
|
||||
if ('$typeName' in this.user && this.user.$typeName === 'zitadel.user.v2.User') {
|
||||
return this.user.userId;
|
||||
}
|
||||
return this.user.id;
|
||||
}
|
||||
|
||||
public get changeDate() {
|
||||
return this?.user?.details?.changeDate;
|
||||
}
|
||||
|
||||
public get creationDate() {
|
||||
if (!this.user) {
|
||||
return undefined;
|
||||
}
|
||||
return '$typeName' in this.user ? undefined : this.user.details?.creationDate;
|
||||
}
|
||||
|
||||
private getEmail(user: User.AsObject | UserV2 | UserV1) {
|
||||
const human = this.human(user);
|
||||
if (!human) {
|
||||
return '';
|
||||
}
|
||||
if ('$typeName' in human && human.$typeName === 'zitadel.user.v2.HumanUser') {
|
||||
return human.email?.isVerified ? human.email.email : '';
|
||||
}
|
||||
return human.email?.isEmailVerified ? human.email.email : '';
|
||||
}
|
||||
|
||||
private getPhone(user: User.AsObject | UserV2 | UserV1) {
|
||||
const human = this.human(user);
|
||||
if (!human) {
|
||||
return '';
|
||||
}
|
||||
if ('$typeName' in human && human.$typeName === 'zitadel.user.v2.HumanUser') {
|
||||
return human.phone?.isVerified ? human.phone.phone : '';
|
||||
}
|
||||
return human.phone?.isPhoneVerified ? human.phone.phone : '';
|
||||
}
|
||||
|
||||
public human(user: User.AsObject | UserV2 | UserV1) {
|
||||
if (!('$typeName' in user)) {
|
||||
return user.human;
|
||||
}
|
||||
return user.type.case === 'human' ? user.type.value : undefined;
|
||||
}
|
||||
|
||||
public isV2(user: User.AsObject | UserV2 | UserV1) {
|
||||
if ('$typeName' in user) {
|
||||
return user.$typeName === 'zitadel.user.v2.User';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
|
||||
import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
import { Metadata as MetadataV2 } from '@zitadel/proto/zitadel/metadata_pb';
|
||||
import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb';
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
export type MetadataDialogData = {
|
||||
metadata: (Metadata.AsObject | MetadataV2)[];
|
||||
setFcn: (key: string, value: string) => Promise<any>;
|
||||
removeFcn: (key: string) => Promise<any>;
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-metadata-dialog',
|
||||
@@ -10,72 +18,63 @@ import { ToastService } from 'src/app/services/toast.service';
|
||||
styleUrls: ['./metadata-dialog.component.scss'],
|
||||
})
|
||||
export class MetadataDialogComponent {
|
||||
public metadata: Partial<Metadata.AsObject>[] = [];
|
||||
public metadata: { key: string; value: string }[] = [];
|
||||
public ts!: Timestamp.AsObject | undefined;
|
||||
|
||||
constructor(
|
||||
private toast: ToastService,
|
||||
public dialogRef: MatDialogRef<MetadataDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
@Inject(MAT_DIALOG_DATA) public data: MetadataDialogData,
|
||||
) {
|
||||
this.metadata = data.metadata;
|
||||
this.metadata = data.metadata.map(({ key, value }) => ({
|
||||
key,
|
||||
value: typeof value === 'string' ? value : Buffer.from(value as unknown as string, 'base64').toString('utf8'),
|
||||
}));
|
||||
}
|
||||
|
||||
public addEntry(): void {
|
||||
const newGroup = {
|
||||
this.metadata.push({
|
||||
key: '',
|
||||
value: '',
|
||||
};
|
||||
|
||||
this.metadata.push(newGroup);
|
||||
});
|
||||
}
|
||||
|
||||
public removeEntry(index: number): void {
|
||||
public async removeEntry(index: number) {
|
||||
const key = this.metadata[index].key;
|
||||
if (key) {
|
||||
this.removeMetadata(key).then(() => {
|
||||
this.metadata.splice(index, 1);
|
||||
if (this.metadata.length === 0) {
|
||||
this.addEntry();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (!key) {
|
||||
this.metadata.splice(index, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.data.removeFcn(key);
|
||||
} catch (error) {
|
||||
this.toast.showError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
this.toast.showInfo('METADATA.REMOVESUCCESS', true);
|
||||
this.metadata.splice(index, 1);
|
||||
if (this.metadata.length === 0) {
|
||||
this.addEntry();
|
||||
}
|
||||
}
|
||||
|
||||
public saveElement(index: number): void {
|
||||
const metadataElement = this.metadata[index];
|
||||
public async saveElement(index: number) {
|
||||
const { key, value } = this.metadata[index];
|
||||
|
||||
if (metadataElement.key && metadataElement.value) {
|
||||
this.setMetadata(metadataElement.key, metadataElement.value as string);
|
||||
if (!key || !value) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public setMetadata(key: string, value: string): void {
|
||||
if (key && value) {
|
||||
this.data
|
||||
.setFcn(key, value)
|
||||
.then(() => {
|
||||
this.toast.showInfo('METADATA.SETSUCCESS', true);
|
||||
})
|
||||
.catch((error: any) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
try {
|
||||
await this.data.setFcn(key, value);
|
||||
this.toast.showInfo('METADATA.SETSUCCESS', true);
|
||||
} catch (error) {
|
||||
this.toast.showError(error);
|
||||
}
|
||||
}
|
||||
|
||||
public removeMetadata(key: string): Promise<void> {
|
||||
return this.data
|
||||
.removeFcn(key)
|
||||
.then((resp: any) => {
|
||||
this.toast.showInfo('METADATA.REMOVESUCCESS', true);
|
||||
})
|
||||
.catch((error: any) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
closeDialog(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<cnsl-card class="metadata-details" [title]="'DESCRIPTIONS.METADATA_TITLE' | translate" [description]="description">
|
||||
<mat-spinner card-actions class="spinner" diameter="20" *ngIf="loading"></mat-spinner>
|
||||
|
||||
<cnsl-refresh-table [loading]="loading$ | async" (refreshed)="refresh.emit()" [dataSize]="dataSource.data.length">
|
||||
<cnsl-refresh-table
|
||||
*ngIf="dataSource$ | async as dataSource"
|
||||
[loading]="loading"
|
||||
(refreshed)="refresh.emit()"
|
||||
[dataSize]="dataSource.data.length"
|
||||
>
|
||||
<button actions [disabled]="disabled" mat-raised-button color="primary" class="edit" (click)="editClicked.emit()">
|
||||
{{ 'ACTIONS.EDIT' | translate }}
|
||||
</button>
|
||||
@@ -28,7 +33,7 @@
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||
</table>
|
||||
<div *ngIf="(loading$ | async) === false && !dataSource?.data?.length" class="no-content-row">
|
||||
<div *ngIf="!loading && !dataSource?.data?.length" class="no-content-row">
|
||||
<i class="las la-exclamation"></i>
|
||||
<span>{{ 'USER.MFA.EMPTY' | translate }}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, ViewChild } from '@angular/core';
|
||||
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTable, MatTableDataSource } from '@angular/material/table';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { Observable, ReplaySubject } from 'rxjs';
|
||||
import { Metadata as MetadataV2 } from '@zitadel/proto/zitadel/metadata_pb';
|
||||
import { map, startWith } from 'rxjs/operators';
|
||||
import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb';
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
type StringMetadata = {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-metadata',
|
||||
templateUrl: './metadata.component.html',
|
||||
styleUrls: ['./metadata.component.scss'],
|
||||
})
|
||||
export class MetadataComponent implements OnChanges {
|
||||
@Input() public metadata: Metadata.AsObject[] = [];
|
||||
export class MetadataComponent implements OnInit {
|
||||
@Input({ required: true }) public set metadata(metadata: (Metadata.AsObject | MetadataV2)[]) {
|
||||
this.metadata$.next(metadata);
|
||||
}
|
||||
@Input() public disabled: boolean = false;
|
||||
@Input() public loading: boolean = false;
|
||||
@Input({ required: true }) public description!: string;
|
||||
@@ -18,18 +28,23 @@ export class MetadataComponent implements OnChanges {
|
||||
@Output() public refresh: EventEmitter<void> = new EventEmitter();
|
||||
|
||||
public displayedColumns: string[] = ['key', 'value'];
|
||||
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
|
||||
public metadata$ = new ReplaySubject<(Metadata.AsObject | MetadataV2)[]>(1);
|
||||
public dataSource$?: Observable<MatTableDataSource<StringMetadata>>;
|
||||
|
||||
@ViewChild(MatTable) public table!: MatTable<Metadata.AsObject>;
|
||||
@ViewChild(MatSort) public sort!: MatSort;
|
||||
public dataSource: MatTableDataSource<Metadata.AsObject> = new MatTableDataSource<Metadata.AsObject>([]);
|
||||
|
||||
constructor() {}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['metadata']?.currentValue) {
|
||||
this.dataSource = new MatTableDataSource<Metadata.AsObject>(changes['metadata'].currentValue);
|
||||
}
|
||||
ngOnInit() {
|
||||
this.dataSource$ = this.metadata$.pipe(
|
||||
map((metadata) =>
|
||||
metadata.map(({ key, value }) => ({
|
||||
key,
|
||||
value: Buffer.from(value as any as string, 'base64').toString('utf-8'),
|
||||
})),
|
||||
),
|
||||
startWith([] as StringMetadata[]),
|
||||
map((metadata) => new MatTableDataSource(metadata)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { Timestamp } from 'src/app/proto/generated/google/protobuf/timestamp_pb';
|
||||
import { Timestamp as ConnectTimestamp } from '@bufbuild/protobuf/wkt';
|
||||
|
||||
export interface PageEvent {
|
||||
length: number;
|
||||
@@ -14,7 +15,7 @@ export interface PageEvent {
|
||||
styleUrls: ['./paginator.component.scss'],
|
||||
})
|
||||
export class PaginatorComponent {
|
||||
@Input() public timestamp: Timestamp.AsObject | undefined = undefined;
|
||||
@Input() public timestamp: Timestamp.AsObject | ConnectTimestamp | undefined = undefined;
|
||||
@Input() public length: number = 0;
|
||||
@Input() public pageSize: number = 10;
|
||||
@Input() public pageIndex: number = 0;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { SelectionModel } from '@angular/cdk/collections';
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
|
||||
import { RefreshService } from 'src/app/services/refresh.service';
|
||||
import { Timestamp as ConnectTimestamp } from '@bufbuild/protobuf/wkt';
|
||||
|
||||
import { ActionKeysType } from '../action-keys/action-keys.component';
|
||||
|
||||
@@ -27,7 +28,7 @@ const rotate = animation([
|
||||
})
|
||||
export class RefreshTableComponent implements OnInit {
|
||||
@Input() public selection: SelectionModel<any> = new SelectionModel<any>(true, []);
|
||||
@Input() public timestamp: Timestamp.AsObject | undefined = undefined;
|
||||
@Input() public timestamp: Timestamp.AsObject | ConnectTimestamp | undefined = undefined;
|
||||
@Input() public dataSize: number = 0;
|
||||
@Input() public emitRefreshAfterTimeoutInMs: number = 0;
|
||||
@Input() public loading: boolean | null = false;
|
||||
|
||||
@@ -51,15 +51,17 @@ export class SidenavComponent implements ControlValueAccessor {
|
||||
}
|
||||
|
||||
if (this.queryParam && setting) {
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: {
|
||||
[this.queryParam]: setting,
|
||||
},
|
||||
replaceUrl: true,
|
||||
queryParamsHandling: 'merge',
|
||||
skipLocationChange: false,
|
||||
});
|
||||
this.router
|
||||
.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: {
|
||||
[this.queryParam]: setting,
|
||||
},
|
||||
replaceUrl: true,
|
||||
queryParamsHandling: 'merge',
|
||||
skipLocationChange: false,
|
||||
})
|
||||
.then();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user