feat: show all available organizations when creating project grants (#6040)

* feat: show available orgs (project) grants

* feat: add e2e for project grant

* feat: add bulgarian missing translations

* feat: update docs

* fix: add @peintnermax suggested changes

---------

Co-authored-by: Max Peintner <[email protected]>
This commit is contained in:
Miguel Cabrerizo
2023-07-18 06:45:34 +00:00
committed by GitHub
co-authored by Max Peintner
parent e1b3cda98a
commit 7b44209bfd
22 changed files with 328 additions and 70 deletions
@@ -16,6 +16,7 @@
color="primary"
class="cnsl-action-button"
mat-raised-button
data-e2e="create-project-role-button"
>
<mat-icon data-e2e="add-new-role" class="icon">add</mat-icon>
<span>{{ 'ACTIONS.NEW' | translate }}</span>
@@ -0,0 +1,32 @@
<form>
<cnsl-form-field class="full-width">
<cnsl-label>{{ 'PROJECT.GRANT.CREATE.SEL_ORG_FORMFIELD' | translate }}</cnsl-label>
<input
cnslInput
type="text"
placeholder="Organization XY"
#nameInput
[formControl]="myControl"
[matAutocomplete]="auto"
data-e2e="add-org-input"
/>
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)" [displayWith]="displayFn">
<mat-option *ngIf="isLoading" class="is-loading">
<mat-spinner diameter="30"></mat-spinner>
</mat-option>
<mat-option *ngFor="let org of filteredOrgs" [value]="org">
<div class="org-option" data-e2e="org-option">
<div class="org-option-column">
<span>{{ org.name }}</span>
<span class="fill-space"></span>
<span class="smaller cnsl-secondary-text">{{ org.primaryDomain }}</span>
</div>
</div>
</mat-option>
</mat-autocomplete>
<span class="org-autocomplete-target-desc">
{{ 'PROJECT.GRANT.CREATE.SEL_ORG_DESC' | translate }}
</span>
</cnsl-form-field>
</form>
@@ -0,0 +1,38 @@
.full-width {
width: 100%;
}
input {
max-width: 500px;
}
.org-option {
display: flex;
align-items: center;
.org-option-column {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
width: 100%;
span {
line-height: normal;
}
.fill-space {
flex: 1;
}
.smaller {
font-size: 13px;
}
}
}
.org-autocomplete-target-desc {
font-size: 14px;
display: block;
margin-top: 0.5rem;
}
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { SearchOrgAutocompleteComponent } from './search-org-autocomplete.component';
describe('SearchOrgComponent', () => {
let component: SearchOrgAutocompleteComponent;
let fixture: ComponentFixture<SearchOrgAutocompleteComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [SearchOrgAutocompleteComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SearchOrgAutocompleteComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,83 @@
import { Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
import { UntypedFormControl } from '@angular/forms';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MatLegacyAutocomplete as MatAutocomplete } from '@angular/material/legacy-autocomplete';
import { debounceTime, from, map, Subject, switchMap, takeUntil, tap } from 'rxjs';
import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_pb';
import { Org, OrgNameQuery, OrgQuery, OrgState, OrgStateQuery } from 'src/app/proto/generated/zitadel/org_pb';
import { AuthenticationService } from 'src/app/services/authentication.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
@Component({
selector: 'cnsl-search-org-autocomplete',
templateUrl: './search-org-autocomplete.component.html',
styleUrls: ['./search-org-autocomplete.component.scss'],
})
export class SearchOrgAutocompleteComponent implements OnInit, OnDestroy {
public selectable: boolean = true;
public myControl: UntypedFormControl = new UntypedFormControl();
public filteredOrgs: Array<Org.AsObject> = [];
public isLoading: boolean = false;
@ViewChild('auto') public matAutocomplete!: MatAutocomplete;
@Output() public selectionChanged: EventEmitter<Org.AsObject> = new EventEmitter();
private unsubscribed$: Subject<void> = new Subject();
constructor(public authService: AuthenticationService, private auth: GrpcAuthService) {
this.myControl.valueChanges
.pipe(
takeUntil(this.unsubscribed$),
debounceTime(200),
tap(() => (this.isLoading = true)),
switchMap((value) => {
const stateQuery = new OrgQuery();
const orgStateQuery = new OrgStateQuery();
orgStateQuery.setState(OrgState.ORG_STATE_ACTIVE);
stateQuery.setStateQuery(orgStateQuery);
let queries: OrgQuery[] = [stateQuery];
if (value) {
const nameQuery = new OrgQuery();
const orgNameQuery = new OrgNameQuery();
orgNameQuery.setName(value);
orgNameQuery.setMethod(TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE);
nameQuery.setNameQuery(orgNameQuery);
queries = [stateQuery, nameQuery];
}
return from(this.auth.listMyProjectOrgs(undefined, 0, queries)).pipe(
map((resp) => {
return resp.resultList.sort((left, right) => left.name.localeCompare(right.name));
}),
);
}),
)
.subscribe((returnValue) => {
this.isLoading = false;
this.filteredOrgs = returnValue;
});
}
public ngOnInit(): void {
const query = new OrgQuery();
const orgStateQuery = new OrgStateQuery();
orgStateQuery.setState(OrgState.ORG_STATE_ACTIVE);
query.setStateQuery(orgStateQuery);
this.auth.listMyProjectOrgs(undefined, 0, [query]).then((orgs) => {
this.filteredOrgs = orgs.resultList;
});
}
public ngOnDestroy(): void {
this.unsubscribed$.next();
}
public displayFn(org?: Org.AsObject): string {
return org ? `${org.name}` : '';
}
public selected(event: MatAutocompleteSelectedEvent): void {
this.selectionChanged.emit(event.option.value);
}
}
@@ -0,0 +1,32 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatIconModule } from '@angular/material/icon';
import { MatLegacyAutocompleteModule as MatAutocompleteModule } from '@angular/material/legacy-autocomplete';
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatLegacyChipsModule as MatChipsModule } from '@angular/material/legacy-chips';
import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner';
import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select';
import { TranslateModule } from '@ngx-translate/core';
import { InputModule } from 'src/app/modules/input/input.module';
import { SearchOrgAutocompleteComponent } from './search-org-autocomplete.component';
@NgModule({
declarations: [SearchOrgAutocompleteComponent],
imports: [
CommonModule,
MatAutocompleteModule,
MatChipsModule,
MatButtonModule,
InputModule,
MatIconModule,
ReactiveFormsModule,
MatProgressSpinnerModule,
FormsModule,
TranslateModule,
MatSelectModule,
],
exports: [SearchOrgAutocompleteComponent],
})
export class SearchOrgAutocompleteModule {}
@@ -6,20 +6,8 @@
>
<ng-container *ngIf="currentCreateStep === 1">
<h1>{{ 'PROJECT.GRANT.CREATE.SEL_ORG' | translate }}</h1>
<p>{{ 'PROJECT.GRANT.CREATE.SEL_ORG_DESC' | translate }}</p>
<form (ngSubmit)="searchOrg(domain.value)">
<cnsl-form-field class="org-domain">
<cnsl-label>{{ 'PROJECT.GRANT.CREATE.SEL_ORG_FORMFIELD' | translate }}</cnsl-label>
<input cnslInput #domain />
</cnsl-form-field>
<button [disabled]="domain.value.length === 0" color="primary" type="submit" class="domain-button" mat-raised-button>
{{ 'PROJECT.GRANT.CREATE.SEL_ORG_BUTTON' | translate }}
</button>
</form>
<span *ngIf="org"> {{ 'PROJECT.GRANT.CREATE.FOR_ORG' | translate }} {{ org.name }} </span>
<cnsl-search-org-autocomplete class="block" (selectionChanged)="selectOrg($event)"> </cnsl-search-org-autocomplete>
<span *ngIf="org"> {{ 'PROJECT.GRANT.CREATE.FOR_ORG' | translate }} {{ org.name }} - {{ org.primaryDomain }} </span>
</ng-container>
<ng-container *ngIf="currentCreateStep === 2">
@@ -37,7 +25,15 @@
<div class="btn-container">
<ng-container *ngIf="currentCreateStep === 1">
<button [disabled]="!org" (click)="next()" color="primary" mat-raised-button class="big-button" cdkFocusInitial>
<button
[disabled]="!org"
(click)="next()"
color="primary"
mat-raised-button
class="big-button"
cdkFocusInitial
data-e2e="project-grant-continue"
>
{{ 'ACTIONS.CONTINUE' | translate }}
</button>
</ng-container>
@@ -46,7 +42,15 @@
<button (click)="previous()" mat-stroked-button class="small-button">
{{ 'ACTIONS.BACK' | translate }}
</button>
<button color="primary" [disabled]="!org" (click)="addGrant()" mat-raised-button class="big-button" cdkFocusInitial>
<button
color="primary"
[disabled]="!org"
(click)="addGrant()"
mat-raised-button
class="big-button"
cdkFocusInitial
data-e2e="save-project-grant-button"
>
{{ 'ACTIONS.SAVE' | translate }}
</button>
</ng-container>
@@ -88,6 +88,10 @@ export class ProjectGrantCreateComponent implements OnInit, OnDestroy {
}
}
public selectOrg(org: Org.AsObject): void {
this.org = org;
}
public selectRoles(roles: string[]): void {
this.rolesKeyList = roles;
}
@@ -15,6 +15,7 @@ import { InputModule } from 'src/app/modules/input/input.module';
import { ProjectRolesTableModule } from 'src/app/modules/project-roles-table/project-roles-table.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { SearchOrgAutocompleteModule } from 'src/app/modules/search-org-autocomplete/search-org-autocomplete.module';
import { ProjectGrantCreateRoutingModule } from './project-grant-create-routing.module';
import { ProjectGrantCreateComponent } from './project-grant-create.component';
@@ -38,6 +39,7 @@ import { ProjectGrantCreateComponent } from './project-grant-create.component';
MatProgressSpinnerModule,
FormsModule,
TranslateModule,
SearchOrgAutocompleteModule,
],
})
export default class ProjectGrantCreateModule {}
@@ -17,6 +17,7 @@
class="cnsl-action-button"
mat-raised-button
[routerLink]="['/projects', projectId, 'projectgrants', 'create']"
data-e2e="create-project-grant-button"
>
<mat-icon class="icon">add</mat-icon>
<span>{{ 'ACTIONS.NEW' | translate }}</span>
@@ -12,15 +12,15 @@
<div class="newrole">
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'PROJECT.ROLE.KEY' | translate }}</cnsl-label>
<input cnslInput formControlName="key" />
<input cnslInput formControlName="key" data-e2e="role-key-input" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'PROJECT.ROLE.DISPLAY_NAME' | translate }}</cnsl-label>
<input cnslInput formControlName="displayName" />
<input cnslInput formControlName="displayName" data-e2e="role-display-name-input" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'PROJECT.ROLE.GROUP' | translate }}</cnsl-label>
<input cnslInput formControlName="group" />
<input cnslInput formControlName="group" data-e2e="role-group-input" />
</cnsl-form-field>
</div>
<button
@@ -36,7 +36,14 @@
</ng-container>
</div>
<button class="add-line-btn" color="primary" type="button" mat-stroked-button (click)="addEntry()">
<button
class="add-line-btn"
color="primary"
type="button"
data-e2e="new-project-role-button"
mat-stroked-button
(click)="addEntry()"
>
{{ 'PROJECT.ROLE.ADDNEWLINE' | translate }}
</button>