diff --git a/apps/lfx-one/src/app/modules/akrites/components/akrites-assign-steward-modal/akrites-assign-steward-modal.component.html b/apps/lfx-one/src/app/modules/akrites/components/akrites-assign-steward-modal/akrites-assign-steward-modal.component.html index 610977805..f4d0db26c 100644 --- a/apps/lfx-one/src/app/modules/akrites/components/akrites-assign-steward-modal/akrites-assign-steward-modal.component.html +++ b/apps/lfx-one/src/app/modules/akrites/components/akrites-assign-steward-modal/akrites-assign-steward-modal.component.html @@ -6,7 +6,7 @@ [modal]="true" [draggable]="false" [resizable]="false" - [style]="{ width: '32rem' }" + [style]="{ width: '34rem' }" [dismissableMask]="true" (onShow)="onShow()" header="Assign steward" @@ -19,6 +19,82 @@

} + +
+ Select steward + + +
+ + +
+ + +
+ @if (loadingStewards()) { +
+ + Loading stewards… +
+ } @else if (filteredStewards().length === 0) { +
+ @if (stewards().length === 0) { + No stewards available. + } @else { + No results for "{{ searchQuery() }}". + } +
+ } @else { + @for (steward of filteredStewards(); track steward.userId) { + + } + } +
+
+
Role @@ -42,46 +118,31 @@
- -
- - - @if (form.controls.userId.touched && form.controls.userId.errors?.['required']) { -

User ID is required.

- } -

- - A searchable user picker will be available once the steward roster endpoint is ready. -

-
- - + @@ -90,7 +151,7 @@ diff --git a/apps/lfx-one/src/app/modules/akrites/components/akrites-assign-steward-modal/akrites-assign-steward-modal.component.ts b/apps/lfx-one/src/app/modules/akrites/components/akrites-assign-steward-modal/akrites-assign-steward-modal.component.ts index db84e7171..e9daf80ad 100644 --- a/apps/lfx-one/src/app/modules/akrites/components/akrites-assign-steward-modal/akrites-assign-steward-modal.component.ts +++ b/apps/lfx-one/src/app/modules/akrites/components/akrites-assign-steward-modal/akrites-assign-steward-modal.component.ts @@ -1,21 +1,21 @@ // Copyright The Linux Foundation and each contributor to LFX. // SPDX-License-Identifier: MIT -import { Component, inject, input, model, output, signal } from '@angular/core'; -import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { Component, Signal, computed, inject, input, model, output, signal } from '@angular/core'; +import { take } from 'rxjs'; import { DialogModule } from 'primeng/dialog'; -import { AkritesAssignStewardRequest, AkritesRoleOption, AkritesStewardRole } from '@lfx-one/shared/interfaces'; +import { AkritesAssignStewardRequest, AkritesRoleOption, AkritesSearchStewardResult, AkritesStewardRole } from '@lfx-one/shared/interfaces'; +import { AkritesService } from '@app/shared/services/akrites.service'; import { ButtonComponent } from '@components/button/button.component'; -import { InputTextComponent } from '@components/input-text/input-text.component'; @Component({ selector: 'lfx-akrites-assign-steward-modal', - imports: [DialogModule, ReactiveFormsModule, ButtonComponent, InputTextComponent], + imports: [DialogModule, ButtonComponent], templateUrl: './akrites-assign-steward-modal.component.html', }) export class AkritesAssignStewardModalComponent { - private readonly formBuilder = inject(FormBuilder); + private readonly akritesService = inject(AkritesService); public readonly visible = model(false); public readonly packageName = input(null); @@ -24,37 +24,78 @@ export class AkritesAssignStewardModalComponent { public readonly confirm = output(); protected readonly selectedRole = signal('lead'); + protected readonly moveToAssessing = signal(false); + protected readonly searchQuery = signal(''); + protected readonly selectedSteward = signal(null); + protected readonly stewards = signal([]); + protected readonly loadingStewards = signal(false); protected readonly roleOptions: AkritesRoleOption[] = [ { value: 'lead', label: 'Lead steward', description: 'Primary owner — drives the security assessment and remediation.' }, { value: 'co_steward', label: 'Co-steward', description: 'Supporting role — assists the lead but shares responsibility.' }, ]; - protected readonly form = this.formBuilder.nonNullable.group({ - userId: ['', [Validators.required, Validators.minLength(3)]], - moveToAssessing: false, - }); + protected readonly filteredStewards: Signal = this.initFilteredStewards(); protected selectRole(role: AkritesStewardRole): void { this.selectedRole.set(role); } + protected selectSteward(steward: AkritesSearchStewardResult): void { + const current = this.selectedSteward(); + this.selectedSteward.set(current?.userId === steward.userId ? null : steward); + } + + protected onSearchInput(event: Event): void { + const query = (event.target as HTMLInputElement).value; + this.searchQuery.set(query); + const selected = this.selectedSteward(); + if (selected && !this.stewardMatchesQuery(selected, query)) { + this.selectedSteward.set(null); + } + } + protected onCancel(): void { this.visible.set(false); } protected onConfirm(): void { - if (this.form.invalid) return; - const { userId, moveToAssessing } = this.form.getRawValue(); + const steward = this.selectedSteward(); + if (!steward) return; this.confirm.emit({ - userId: userId.trim(), + userId: steward.userId, + username: steward.username, + displayName: steward.displayName, role: this.selectedRole(), - moveToAssessing: moveToAssessing || undefined, + moveToAssessing: this.moveToAssessing() || undefined, }); } protected onShow(): void { this.selectedRole.set('lead'); - this.form.reset({ userId: '', moveToAssessing: false }); + this.moveToAssessing.set(false); + this.searchQuery.set(''); + this.selectedSteward.set(null); + this.stewards.set([]); + this.loadingStewards.set(true); + this.akritesService + .searchStewards() + .pipe(take(1)) + .subscribe((members) => { + this.stewards.set(members); + this.loadingStewards.set(false); + }); + } + + private stewardMatchesQuery(steward: AkritesSearchStewardResult, query: string): boolean { + const q = query.toLowerCase().trim(); + if (!q) return true; + return ( + steward.displayName.toLowerCase().includes(q) || steward.username.toLowerCase().includes(q) || (steward.organization ?? '').toLowerCase().includes(q) + ); + } + + private initFilteredStewards(): Signal { + return computed(() => this.stewards().filter((s) => this.stewardMatchesQuery(s, this.searchQuery()))); } } diff --git a/apps/lfx-one/src/app/modules/akrites/components/akrites-package-drawer/akrites-package-drawer.component.html b/apps/lfx-one/src/app/modules/akrites/components/akrites-package-drawer/akrites-package-drawer.component.html index 54077cb8a..b87d3b47e 100644 --- a/apps/lfx-one/src/app/modules/akrites/components/akrites-package-drawer/akrites-package-drawer.component.html +++ b/apps/lfx-one/src/app/modules/akrites/components/akrites-package-drawer/akrites-package-drawer.component.html @@ -511,6 +511,17 @@

} + @if (canAssignSteward()) { + + } @if (canEscalate()) { + this.stewardshipStatus() === 'unassigned'); - // Allow assigning a steward on any package — if no stewardship row exists yet the - // confirm handler auto-opens the package first and then chains the assign call. + // Show "Assign steward" for unassigned/open and "Reassign" for assessing/escalated/inactive. protected readonly canAssignSteward = computed(() => { - const status = this.stewardshipStatus(); - return status !== 'inactive'; + const s = this.stewardshipStatus(); + return ['unassigned', 'open', 'assessing', 'escalated', 'inactive'].includes(s); + }); + protected readonly assignStewardLabel = computed(() => { + const s = this.stewardshipStatus(); + return ['assessing', 'escalated', 'inactive'].includes(s) ? 'Reassign' : 'Assign steward'; }); protected readonly canManageStatus = computed(() => this.stewardshipId() !== null); protected readonly canEscalate = computed(() => { diff --git a/apps/lfx-one/src/app/modules/akrites/components/akrites-triage-tab/akrites-triage-tab.component.html b/apps/lfx-one/src/app/modules/akrites/components/akrites-triage-tab/akrites-triage-tab.component.html index 0d0133232..90caf21a8 100644 --- a/apps/lfx-one/src/app/modules/akrites/components/akrites-triage-tab/akrites-triage-tab.component.html +++ b/apps/lfx-one/src/app/modules/akrites/components/akrites-triage-tab/akrites-triage-tab.component.html @@ -117,8 +117,8 @@ } - - @if (canWrite() && col.status !== 'unassigned') { + + @if (canWrite()) {