mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-31 19:42:15 +00:00
feat(console): user memberships, generic member create dialog, fix user autocomplete emitter (#606)
* load manager mgmtservice, user service * add org memberships * membership component, generic member creation * refactor member create dialog * project autocomplete context * create batch managers in user component * project context wrapper * emit on user removal, preselect user on init * membership avatar style, service * auth user memberships, navigate to target * cursor fix, avatar gen * lint * i18n fix * remove role translations * membership detail page, i18n * fix role label i18n, after view init loader * remove projectid from grant remove * fix iam race condition * refresh table ts, fix no permission project search * change membership colors * refresh table everywhere, replace assets, routing * fix logo header size * lint, fix project grant removal * timestmp for p mem, user list, grants, p list (#615) * npm audit * update deps, resolve vulnerability * fix tslint config * update lock * load 20 changes at once * Update console/src/assets/i18n/de.json Co-authored-by: Florian Forster <[email protected]> * Update console/src/assets/i18n/de.json Co-authored-by: Florian Forster <[email protected]> * Update console/src/assets/i18n/en.json Co-authored-by: Florian Forster <[email protected]> * membership i18n Co-authored-by: Florian Forster <[email protected]>
This commit is contained in:
co-authored by
Florian Forster
parent
d49fee23bf
commit
193cfb45f6
@@ -2,15 +2,39 @@
|
||||
<span class="title">{{'MEMBER.ADD' | translate}}</span>
|
||||
</h1>
|
||||
<p class="desc"> {{'ORG_DETAIL.MEMBER.ADDDESCRIPTION' | translate}}</p>
|
||||
|
||||
<div mat-dialog-content>
|
||||
<app-search-user-autocomplete (selectionChanged)="users = $event"></app-search-user-autocomplete>
|
||||
<!-- if no context -->
|
||||
<ng-container *ngIf="showCreationTypeSelector">
|
||||
<mat-form-field class="full-width" appearance="outline">
|
||||
<mat-label>{{ 'MEMBER.CREATIONTYPE' | translate }}</mat-label>
|
||||
<mat-select [(ngModel)]="creationType" (selectionChange)="loadRoles()">
|
||||
<mat-option *ngFor="let type of creationTypes" [value]="type">
|
||||
{{ 'MEMBER.CREATIONTYPES.'+type | translate}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<ng-container
|
||||
*ngIf="creationType === CreationType.PROJECT_OWNED || creationType === CreationType.PROJECT_GRANTED">
|
||||
<p>{{'PROJECT.GRANT.CREATE.SEL_PROJECT' | translate}}</p>
|
||||
<app-search-project-autocomplete class="block" singleOutput="true"
|
||||
(selectionChanged)="selectProject($event)"
|
||||
[autocompleteType]="creationType === CreationType.PROJECT_OWNED ? ProjectAutocompleteType.PROJECT_OWNED : creationType === CreationType.PROJECT_GRANTED ? ProjectAutocompleteType.PROJECT_GRANTED : undefined">
|
||||
</app-search-project-autocomplete>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<!-- if no context end -->
|
||||
|
||||
<app-search-user-autocomplete [users]="preselectedUsers" (selectionChanged)="users = $event">
|
||||
</app-search-user-autocomplete>
|
||||
|
||||
<mat-form-field class="full-width" appearance="outline"
|
||||
*ngIf="creationType === CreationType.PROJECT_OWNED || creationType === CreationType.PROJECT_GRANTED || creationType === CreationType.IAM">
|
||||
<mat-label>{{ 'PROJECT.GRANT.TITLE' | translate }}</mat-label>
|
||||
<mat-label>{{ 'ROLESLABEL' | translate }}</mat-label>
|
||||
<mat-select [(ngModel)]="roles" multiple>
|
||||
<mat-option *ngFor="let role of memberRoleOptions" [value]="role">
|
||||
{{ 'ROLES.'+role | translate }}
|
||||
{{ role }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { ProjectRole, User } from 'src/app/proto/generated/management_pb';
|
||||
import { ProjectGrantView, ProjectRole, ProjectView, User } from 'src/app/proto/generated/management_pb';
|
||||
import { AdminService } from 'src/app/services/admin.service';
|
||||
import { ProjectService } from 'src/app/services/project.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
import { ProjectAutocompleteType } from '../search-project-autocomplete/search-project-autocomplete.component';
|
||||
|
||||
export enum CreationType {
|
||||
PROJECT_OWNED = 0,
|
||||
PROJECT_GRANTED = 1,
|
||||
@@ -17,40 +19,78 @@ export enum CreationType {
|
||||
styleUrls: ['./member-create-dialog.component.scss'],
|
||||
})
|
||||
export class MemberCreateDialogComponent {
|
||||
public projectId: string = '';
|
||||
private projectId: string = '';
|
||||
private grantId: string = '';
|
||||
public preselectedUsers: Array<User.AsObject> = [];
|
||||
|
||||
|
||||
public creationType!: CreationType;
|
||||
public creationTypes: CreationType[] = [
|
||||
CreationType.IAM,
|
||||
CreationType.ORG,
|
||||
CreationType.PROJECT_OWNED,
|
||||
CreationType.PROJECT_GRANTED,
|
||||
];
|
||||
public users: Array<User.AsObject> = [];
|
||||
public roles: Array<ProjectRole.AsObject> | string[] = [];
|
||||
public CreationType: any = CreationType;
|
||||
public ProjectAutocompleteType: any = ProjectAutocompleteType;
|
||||
public memberRoleOptions: string[] = [];
|
||||
|
||||
public showCreationTypeSelector: boolean = false;
|
||||
constructor(
|
||||
private projectService: ProjectService,
|
||||
private adminService: AdminService,
|
||||
public dialogRef: MatDialogRef<MemberCreateDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
toastService: ToastService,
|
||||
private toastService: ToastService,
|
||||
) {
|
||||
this.creationType = data.creationType;
|
||||
this.projectId = data.projectId;
|
||||
if (data?.projectId) {
|
||||
this.projectId = data.projectId;
|
||||
}
|
||||
if (data?.user) {
|
||||
this.preselectedUsers = [data.user];
|
||||
this.users = [data.user];
|
||||
}
|
||||
|
||||
if (this.creationType === CreationType.PROJECT_GRANTED) {
|
||||
this.projectService.GetProjectGrantMemberRoles().then(resp => {
|
||||
this.memberRoleOptions = resp.toObject().rolesList;
|
||||
}).catch(error => {
|
||||
toastService.showError(error);
|
||||
});
|
||||
} else if (this.creationType === CreationType.PROJECT_OWNED) {
|
||||
this.projectService.GetProjectMemberRoles().then(resp => {
|
||||
this.memberRoleOptions = resp.toObject().rolesList;
|
||||
}).catch(error => {
|
||||
toastService.showError(error);
|
||||
});
|
||||
} else if (this.creationType === CreationType.IAM) {
|
||||
this.adminService.GetIamMemberRoles().then(resp => {
|
||||
this.memberRoleOptions = resp.toObject().rolesList;
|
||||
}).catch(error => {
|
||||
toastService.showError(error);
|
||||
});
|
||||
if (data?.creationType !== undefined) {
|
||||
this.creationType = data.creationType;
|
||||
this.loadRoles();
|
||||
} else {
|
||||
this.showCreationTypeSelector = true;
|
||||
}
|
||||
}
|
||||
|
||||
public loadRoles(): void {
|
||||
switch (this.creationType) {
|
||||
case CreationType.PROJECT_GRANTED:
|
||||
this.projectService.GetProjectGrantMemberRoles().then(resp => {
|
||||
this.memberRoleOptions = resp.toObject().rolesList;
|
||||
}).catch(error => {
|
||||
this.toastService.showError(error);
|
||||
});
|
||||
break;
|
||||
case CreationType.PROJECT_OWNED:
|
||||
this.projectService.GetProjectMemberRoles().then(resp => {
|
||||
this.memberRoleOptions = resp.toObject().rolesList;
|
||||
}).catch(error => {
|
||||
this.toastService.showError(error);
|
||||
});
|
||||
break;
|
||||
case CreationType.IAM:
|
||||
this.adminService.GetIamMemberRoles().then(resp => {
|
||||
this.memberRoleOptions = resp.toObject().rolesList;
|
||||
}).catch(error => {
|
||||
this.toastService.showError(error);
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public selectProject(project: ProjectView.AsObject | ProjectGrantView.AsObject | any): void {
|
||||
this.projectId = project.projectId;
|
||||
if (project.id) {
|
||||
this.grantId = project.id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +99,13 @@ export class MemberCreateDialogComponent {
|
||||
}
|
||||
|
||||
public closeDialogWithSuccess(): void {
|
||||
this.dialogRef.close({ users: this.users, roles: this.roles });
|
||||
this.dialogRef.close({
|
||||
users: this.users,
|
||||
roles: this.roles,
|
||||
creationType: this.creationType,
|
||||
projectId: this.projectId,
|
||||
grantId: this.grantId,
|
||||
});
|
||||
}
|
||||
|
||||
public setOrgMemberRoles(roles: string[]): void {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { SearchUserAutocompleteModule } from 'src/app/modules/search-user-autoco
|
||||
import {
|
||||
OrgMemberRolesAutocompleteModule,
|
||||
} from '../../pages/orgs/org-member-roles-autocomplete/org-member-roles-autocomplete.module';
|
||||
import { SearchProjectAutocompleteModule } from '../search-project-autocomplete/search-project-autocomplete.module';
|
||||
import { SearchRolesAutocompleteModule } from '../search-roles-autocomplete/search-roles-autocomplete.module';
|
||||
import { MemberCreateDialogComponent } from './member-create-dialog.component';
|
||||
|
||||
@@ -26,6 +27,7 @@ import { MemberCreateDialogComponent } from './member-create-dialog.component';
|
||||
FormsModule,
|
||||
SearchUserAutocompleteModule,
|
||||
SearchRolesAutocompleteModule,
|
||||
SearchProjectAutocompleteModule,
|
||||
OrgMemberRolesAutocompleteModule,
|
||||
],
|
||||
})
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
@mixin changes-theme($theme) {
|
||||
.scroll-container {
|
||||
max-height: 60vh;
|
||||
max-height: 50vh;
|
||||
overflow-y: scroll;
|
||||
|
||||
.item {
|
||||
|
||||
@@ -45,13 +45,13 @@ export class ChangesComponent implements OnInit {
|
||||
private init(): void {
|
||||
let first: Promise<Changes>;
|
||||
switch (this.changeType) {
|
||||
case ChangeType.MYUSER: first = this.authUserService.GetMyUserChanges(10, 0);
|
||||
case ChangeType.MYUSER: first = this.authUserService.GetMyUserChanges(20, 0);
|
||||
break;
|
||||
case ChangeType.USER: first = this.mgmtUserService.UserChanges(this.id, 10, 0);
|
||||
case ChangeType.USER: first = this.mgmtUserService.UserChanges(this.id, 20, 0);
|
||||
break;
|
||||
case ChangeType.PROJECT: first = this.mgmtUserService.ProjectChanges(this.id, 20, 0);
|
||||
break;
|
||||
case ChangeType.ORG: first = this.mgmtUserService.OrgChanges(this.id, 10, 0);
|
||||
case ChangeType.ORG: first = this.mgmtUserService.OrgChanges(this.id, 20, 0);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -70,13 +70,13 @@ export class ChangesComponent implements OnInit {
|
||||
let more: Promise<Changes>;
|
||||
|
||||
switch (this.changeType) {
|
||||
case ChangeType.MYUSER: more = this.authUserService.GetMyUserChanges(10, cursor);
|
||||
case ChangeType.MYUSER: more = this.authUserService.GetMyUserChanges(20, cursor);
|
||||
break;
|
||||
case ChangeType.USER: more = this.mgmtUserService.UserChanges(this.id, 10, cursor);
|
||||
case ChangeType.USER: more = this.mgmtUserService.UserChanges(this.id, 20, cursor);
|
||||
break;
|
||||
case ChangeType.PROJECT: more = this.mgmtUserService.ProjectChanges(this.id, 10, cursor);
|
||||
case ChangeType.PROJECT: more = this.mgmtUserService.ProjectChanges(this.id, 20, cursor);
|
||||
break;
|
||||
case ChangeType.ORG: more = this.mgmtUserService.OrgChanges(this.id, 10, cursor);
|
||||
case ChangeType.ORG: more = this.mgmtUserService.OrgChanges(this.id, 20, cursor);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,10 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.avatar {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.margin-neg {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
|
||||
import { BehaviorSubject, from, Observable, of } from 'rxjs';
|
||||
import { catchError, finalize, map } from 'rxjs/operators';
|
||||
import { ProjectMember, ProjectMemberSearchResponse, ProjectType } from 'src/app/proto/generated/management_pb';
|
||||
@@ -11,6 +12,8 @@ import { ProjectService } from 'src/app/services/project.service';
|
||||
*/
|
||||
export class ProjectMembersDataSource extends DataSource<ProjectMember.AsObject> {
|
||||
public totalResult: number = 0;
|
||||
public viewTimestamp!: Timestamp.AsObject;
|
||||
|
||||
public membersSubject: BehaviorSubject<ProjectMember.AsObject[]> = new BehaviorSubject<ProjectMember.AsObject[]>([]);
|
||||
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
|
||||
@@ -35,8 +38,12 @@ export class ProjectMembersDataSource extends DataSource<ProjectMember.AsObject>
|
||||
if (promise) {
|
||||
from(promise).pipe(
|
||||
map(resp => {
|
||||
this.totalResult = resp.toObject().totalResult;
|
||||
return resp.toObject().resultList;
|
||||
const response = resp.toObject();
|
||||
this.totalResult = response.totalResult;
|
||||
if (response.viewTimestamp) {
|
||||
this.viewTimestamp = response.viewTimestamp;
|
||||
}
|
||||
return response.resultList;
|
||||
}),
|
||||
catchError(() => of([])),
|
||||
finalize(() => this.loadingSubject.next(false)),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
title="{{projectName}} {{ 'PROJECT.MEMBER.TITLE' | translate }}"
|
||||
description="{{ 'PROJECT.MEMBER.DESCRIPTION' | translate }}">
|
||||
<app-refresh-table *ngIf="project" (refreshed)="changePage()" [dataSize]="dataSource.totalResult"
|
||||
[selection]="selection" [loading]="dataSource?.loading$ | async">
|
||||
[timestamp]="dataSource.viewTimestamp" [selection]="selection" [loading]="dataSource?.loading$ | async">
|
||||
<ng-template appHasRole actions
|
||||
[appHasRole]="['project.member.delete:' + project.projectId, 'project.member.delete']">
|
||||
<button (click)="removeProjectMemberSelection()" color="warn"
|
||||
@@ -68,15 +68,15 @@
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="roles">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.ROLES' | translate }} </th>
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'ROLESLABEL' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let member">
|
||||
<mat-form-field class="form-field" appearance="outline" *ngIf="project">
|
||||
<mat-label>{{ 'PROJECT.GRANT.TITLE' | translate }}</mat-label>
|
||||
<mat-label>{{ 'ROLESLABEL' | translate }}</mat-label>
|
||||
<mat-select [(ngModel)]="member.rolesList" multiple
|
||||
[disabled]="([('project.member.write:' + project.projectId), 'project.member.write'] | hasRole | async) == false"
|
||||
(selectionChange)="updateRoles(member, $event)">
|
||||
<mat-option *ngFor="let role of memberRoleOptions" [value]="role">
|
||||
{{ 'ROLES.'+role | translate }}
|
||||
{{ role }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
@@ -122,7 +122,6 @@ export class ProjectMembersComponent {
|
||||
const dialogRef = this.dialog.open(MemberCreateDialogComponent, {
|
||||
data: {
|
||||
creationType: CreationType.PROJECT_OWNED,
|
||||
projectId: this.project.projectId,
|
||||
},
|
||||
width: '400px',
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
|
||||
import { BehaviorSubject, from, Observable, of } from 'rxjs';
|
||||
import { catchError, finalize, map } from 'rxjs/operators';
|
||||
import { ProjectRole } from 'src/app/proto/generated/management_pb';
|
||||
@@ -11,6 +12,8 @@ import { ProjectService } from 'src/app/services/project.service';
|
||||
*/
|
||||
export class ProjectRolesDataSource extends DataSource<ProjectRole.AsObject> {
|
||||
public totalResult: number = 0;
|
||||
public viewTimestamp!: Timestamp.AsObject;
|
||||
|
||||
public rolesSubject: BehaviorSubject<ProjectRole.AsObject[]> = new BehaviorSubject<ProjectRole.AsObject[]>([]);
|
||||
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
|
||||
@@ -25,7 +28,11 @@ export class ProjectRolesDataSource extends DataSource<ProjectRole.AsObject> {
|
||||
this.loadingSubject.next(true);
|
||||
from(this.projectService.SearchProjectRoles(projectId, pageSize, offset)).pipe(
|
||||
map(resp => {
|
||||
this.totalResult = resp.toObject().totalResult;
|
||||
const response = resp.toObject();
|
||||
this.totalResult = response.totalResult;
|
||||
if (response.viewTimestamp) {
|
||||
this.viewTimestamp = response.viewTimestamp;
|
||||
}
|
||||
return resp.toObject().resultList;
|
||||
}),
|
||||
catchError(() => of([])),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<app-refresh-table *ngIf="projectId" (refreshed)="refreshPage()" [dataSize]="dataSource.totalResult"
|
||||
[selection]="selection" [loading]="dataSource.loading$ | async">
|
||||
<app-refresh-table *ngIf="projectId" (refreshed)="refreshPage()" [dataSize]="dataSource?.totalResult"
|
||||
[selection]="selection" [loading]="dataSource?.loading$ | async" [timestamp]="dataSource?.viewTimestamp">
|
||||
<ng-template appHasRole [appHasRole]="['project.role.delete', 'project.role.delete:' + projectId]" actions>
|
||||
<button color="warn" class="icon-button" [disabled]="disabled"
|
||||
matTooltip="{{'PROJECT.ROLE.DELETE' | translate}}" (click)="deleteSelectedRoles()" mat-icon-button
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<div class="table-header-row">
|
||||
<div class="col">
|
||||
<span class="desc"
|
||||
*ngIf="timestamp">{{timestamp | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'}}</span>
|
||||
<ng-container *ngIf="!selection.hasValue()">
|
||||
<span class="desc">{{'ORG_DETAIL.TABLE.TOTAL' | translate}}</span>
|
||||
<span class="count">{{dataSize}}</span>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { animate, animation, keyframes, style, transition, trigger, useAnimation } from '@angular/animations';
|
||||
import { SelectionModel } from '@angular/cdk/collections';
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
|
||||
|
||||
const rotate = animation([
|
||||
animate(
|
||||
@@ -27,6 +28,7 @@ const rotate = animation([
|
||||
})
|
||||
export class RefreshTableComponent implements OnInit {
|
||||
@Input() public selection: SelectionModel<any> = new SelectionModel<any>(true, []);
|
||||
@Input() public timestamp!: Timestamp.AsObject;
|
||||
@Input() public dataSize: number = 0;
|
||||
@Input() public emitRefreshAfterTimeoutInMs: number = 0;
|
||||
@Input() public loading: boolean = false;
|
||||
|
||||
@@ -6,6 +6,8 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe.module';
|
||||
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe.module';
|
||||
|
||||
import { RefreshTableComponent } from './refresh-table.component';
|
||||
|
||||
@@ -21,6 +23,8 @@ import { RefreshTableComponent } from './refresh-table.component';
|
||||
FormsModule,
|
||||
MatTooltipModule,
|
||||
MatProgressSpinnerModule,
|
||||
TimestampToDatePipeModule,
|
||||
LocalizedDatePipeModule,
|
||||
],
|
||||
exports: [
|
||||
RefreshTableComponent,
|
||||
|
||||
+41
-7
@@ -6,14 +6,22 @@ import { MatChipInputEvent } from '@angular/material/chips';
|
||||
import { forkJoin, from } from 'rxjs';
|
||||
import { debounceTime, switchMap, tap } from 'rxjs/operators';
|
||||
import {
|
||||
ProjectGrantSearchResponse,
|
||||
ProjectGrantView,
|
||||
ProjectSearchKey,
|
||||
ProjectSearchQuery,
|
||||
ProjectSearchResponse,
|
||||
ProjectView,
|
||||
SearchMethod,
|
||||
} from 'src/app/proto/generated/management_pb';
|
||||
import { ProjectService } from 'src/app/services/project.service';
|
||||
|
||||
|
||||
export enum ProjectAutocompleteType {
|
||||
PROJECT_OWNED = 0,
|
||||
PROJECT_GRANTED = 1,
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-search-project-autocomplete',
|
||||
templateUrl: './search-project-autocomplete.component.html',
|
||||
@@ -32,6 +40,7 @@ export class SearchProjectAutocompleteComponent {
|
||||
@ViewChild('nameInput') public nameInput!: ElementRef<HTMLInputElement>;
|
||||
@ViewChild('auto') public matAutocomplete!: MatAutocomplete;
|
||||
@Input() public singleOutput: boolean = false;
|
||||
@Input() public autocompleteType!: ProjectAutocompleteType;
|
||||
@Output() public selectionChanged: EventEmitter<
|
||||
ProjectGrantView.AsObject[]
|
||||
| ProjectGrantView.AsObject
|
||||
@@ -48,14 +57,39 @@ export class SearchProjectAutocompleteComponent {
|
||||
query.setKey(ProjectSearchKey.PROJECTSEARCHKEY_PROJECT_NAME);
|
||||
query.setValue(value);
|
||||
query.setMethod(SearchMethod.SEARCHMETHOD_CONTAINS_IGNORE_CASE);
|
||||
return forkJoin([
|
||||
from(this.projectService.SearchGrantedProjects(10, 0, [query])),
|
||||
from(this.projectService.SearchProjects(10, 0, [query])),
|
||||
]);
|
||||
|
||||
switch (this.autocompleteType) {
|
||||
case ProjectAutocompleteType.PROJECT_GRANTED:
|
||||
return from(this.projectService.SearchGrantedProjects(10, 0, [query]));
|
||||
case ProjectAutocompleteType.PROJECT_OWNED:
|
||||
return from(this.projectService.SearchProjects(10, 0, [query]));
|
||||
default:
|
||||
return forkJoin([
|
||||
from(this.projectService.SearchGrantedProjects(10, 0, [query])),
|
||||
from(this.projectService.SearchProjects(10, 0, [query])),
|
||||
]);
|
||||
}
|
||||
}),
|
||||
).subscribe(([granted, owned]) => {
|
||||
this.isLoading = false;
|
||||
this.filteredProjects = [...owned.toObject().resultList, ...granted.toObject().resultList];
|
||||
).subscribe((returnValue) => {
|
||||
switch (this.autocompleteType) {
|
||||
case ProjectAutocompleteType.PROJECT_GRANTED:
|
||||
this.isLoading = false;
|
||||
this.filteredProjects = [...(returnValue as ProjectGrantSearchResponse).toObject().resultList];
|
||||
break;
|
||||
case ProjectAutocompleteType.PROJECT_OWNED:
|
||||
this.isLoading = false;
|
||||
this.filteredProjects = [...(returnValue as ProjectSearchResponse).toObject().resultList];
|
||||
break;
|
||||
default:
|
||||
this.isLoading = false;
|
||||
this.filteredProjects = [
|
||||
...(returnValue as (ProjectSearchResponse | ProjectGrantSearchResponse)[])[0]
|
||||
.toObject().resultList,
|
||||
...(returnValue as (ProjectSearchResponse | ProjectGrantSearchResponse)[])[1]
|
||||
.toObject().resultList,
|
||||
];
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -29,7 +29,7 @@ export class SearchUserAutocompleteComponent {
|
||||
public globalEmailControl: FormControl = new FormControl();
|
||||
|
||||
public emails: string[] = [];
|
||||
public users: Array<User.AsObject> = [];
|
||||
@Input() public users: Array<User.AsObject> = [];
|
||||
public filteredUsers: Array<User.AsObject> = [];
|
||||
public isLoading: boolean = false;
|
||||
public target: UserTarget = UserTarget.SELF;
|
||||
@@ -39,6 +39,7 @@ export class SearchUserAutocompleteComponent {
|
||||
@ViewChild('auto') public matAutocomplete!: MatAutocomplete;
|
||||
@Output() public selectionChanged: EventEmitter<User.AsObject | User.AsObject[]> = new EventEmitter();
|
||||
@Input() public singleOutput: boolean = false;
|
||||
|
||||
private unsubscribed$: Subject<void> = new Subject();
|
||||
constructor(private userService: MgmtUserService, private toast: ToastService) {
|
||||
this.getFilteredResults();
|
||||
@@ -102,6 +103,7 @@ export class SearchUserAutocompleteComponent {
|
||||
|
||||
if (index >= 0) {
|
||||
this.users.splice(index, 1);
|
||||
this.selectionChanged.emit(this.users);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
|
||||
import { BehaviorSubject, from, Observable, of } from 'rxjs';
|
||||
import { catchError, finalize, map } from 'rxjs/operators';
|
||||
import {
|
||||
@@ -20,6 +21,8 @@ export enum UserGrantContext {
|
||||
|
||||
export class UserGrantsDataSource extends DataSource<UserGrant.AsObject> {
|
||||
public totalResult: number = 0;
|
||||
public viewTimestamp!: Timestamp.AsObject;
|
||||
|
||||
public grantsSubject: BehaviorSubject<UserGrantView.AsObject[]> = new BehaviorSubject<UserGrantView.AsObject[]>([]);
|
||||
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
|
||||
@@ -104,8 +107,12 @@ export class UserGrantsDataSource extends DataSource<UserGrant.AsObject> {
|
||||
private loadResponse(promise: Promise<UserGrantSearchResponse>): void {
|
||||
from(promise).pipe(
|
||||
map(resp => {
|
||||
this.totalResult = resp.toObject().totalResult;
|
||||
return resp.toObject().resultList;
|
||||
const response = resp.toObject();
|
||||
this.totalResult = response.totalResult;
|
||||
if (response.viewTimestamp) {
|
||||
this.viewTimestamp = response.viewTimestamp;
|
||||
}
|
||||
return response.resultList;
|
||||
}),
|
||||
catchError(() => of([])),
|
||||
finalize(() => this.loadingSubject.next(false)),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<app-refresh-table [loading]="dataSource?.loading$ | async" (refreshed)="changePage()"
|
||||
[dataSize]="dataSource.totalResult" [selection]="selection">
|
||||
[timestamp]="dataSource?.viewTimestamp" [dataSize]="dataSource?.totalResult" [selection]="selection">
|
||||
<button color="warn" matTooltip="{{'GRANTS.DELETE' | translate}}" class="icon-button" mat-icon-button actions
|
||||
(click)="deleteGrantSelection()" *ngIf="selection.hasValue() && allowDelete">
|
||||
<i class="las la-trash"></i>
|
||||
|
||||
Reference in New Issue
Block a user