diff --git a/apps/lfx-one/src/app/modules/committees/components/committee-surveys-list/committee-surveys-list.component.html b/apps/lfx-one/src/app/modules/committees/components/committee-surveys-list/committee-surveys-list.component.html new file mode 100644 index 000000000..3661dce41 --- /dev/null +++ b/apps/lfx-one/src/app/modules/committees/components/committee-surveys-list/committee-surveys-list.component.html @@ -0,0 +1,50 @@ + + + +@if (!loading() && loadError()) { + +
+
+ +

Failed to load {{ surveyLabelPlural.toLowerCase() }}

+ +
+
+
+} @else if (!loading() && surveys().length === 0) { + +
+
+ +

No {{ surveyLabelPlural }} yet

+

Surveys created for this group will appear here.

+
+
+
+} @else { + + +} + + + diff --git a/apps/lfx-one/src/app/modules/committees/components/committee-surveys-list/committee-surveys-list.component.ts b/apps/lfx-one/src/app/modules/committees/components/committee-surveys-list/committee-surveys-list.component.ts new file mode 100644 index 000000000..347c2ea52 --- /dev/null +++ b/apps/lfx-one/src/app/modules/committees/components/committee-surveys-list/committee-surveys-list.component.ts @@ -0,0 +1,103 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +import { Component, inject, input, signal, Signal } from '@angular/core'; +import { toObservable, toSignal } from '@angular/core/rxjs-interop'; +import { CardComponent } from '@components/card/card.component'; +import { SURVEY_LABEL } from '@lfx-one/shared/constants'; +import { Survey } from '@lfx-one/shared/interfaces'; +import { SurveyService } from '@services/survey.service'; +import { MessageService } from 'primeng/api'; +import { catchError, finalize, of, switchMap } from 'rxjs'; + +import { SurveyResultsDrawerComponent } from '@app/modules/surveys/components/survey-results-drawer/survey-results-drawer.component'; +import { SurveysTableComponent } from '@app/modules/surveys/components/surveys-table/surveys-table.component'; + +@Component({ + selector: 'lfx-committee-surveys-list', + imports: [CardComponent, SurveysTableComponent, SurveyResultsDrawerComponent], + templateUrl: './committee-surveys-list.component.html', +}) +export class CommitteeSurveysListComponent { + // -- Services -- + private readonly surveyService = inject(SurveyService); + private readonly messageService = inject(MessageService); + + // -- Constants -- + protected readonly surveyLabelPlural = SURVEY_LABEL.plural; + + // -- Inputs -- + public readonly committeeUid = input.required(); + public readonly hasPMOAccess = input(false); + + // -- Writable Signals -- + private readonly refreshTrigger = signal(0); + protected readonly loading = signal(true); + protected readonly loadError = signal(false); + protected readonly resultsDrawerVisible = signal(false); + protected readonly selectedSurveyId = signal(null); + protected readonly selectedListSurvey = signal(null); + + // -- Computed Signals -- + protected readonly surveys: Signal = this.initSurveys(); + + // -- Protected Methods -- + protected onViewResults(surveyId: string): void { + this.selectedSurveyId.set(surveyId); + this.selectedListSurvey.set(this.surveys().find((s) => s.uid === surveyId) ?? null); + this.resultsDrawerVisible.set(true); + } + + protected onRowClick(survey: Survey): void { + this.onViewResults(survey.uid); + } + + protected refreshSurveys(): void { + this.loading.set(true); + this.loadError.set(false); + this.refreshTrigger.update((v) => v + 1); + } + + protected onDuplicateSurvey(): void { + this.messageService.add({ severity: 'info', summary: 'Coming Soon', detail: 'Survey duplication is not yet available' }); + } + + protected onCloseSurvey(): void { + this.messageService.add({ severity: 'info', summary: 'Coming Soon', detail: 'Survey close is not yet available' }); + } + + // -- Private Initializers -- + private initSurveys(): Signal { + const committeeUid$ = toObservable(this.committeeUid); + + return toSignal( + committeeUid$.pipe( + switchMap((committeeUid) => { + if (!committeeUid) { + this.loading.set(false); + return of([]); + } + + this.loading.set(true); + this.loadError.set(false); + this.resultsDrawerVisible.set(false); + this.selectedSurveyId.set(null); + this.selectedListSurvey.set(null); + return toObservable(this.refreshTrigger).pipe( + switchMap(() => { + this.loading.set(true); + return this.surveyService.getSurveysByCommittee(committeeUid).pipe( + catchError(() => { + this.loadError.set(true); + return of([]); + }), + finalize(() => this.loading.set(false)) + ); + }) + ); + }) + ), + { initialValue: [] } + ); + } +} diff --git a/apps/lfx-one/src/app/modules/committees/components/committee-votes-list/committee-votes-list.component.html b/apps/lfx-one/src/app/modules/committees/components/committee-votes-list/committee-votes-list.component.html new file mode 100644 index 000000000..9e00572f8 --- /dev/null +++ b/apps/lfx-one/src/app/modules/committees/components/committee-votes-list/committee-votes-list.component.html @@ -0,0 +1,54 @@ + + + +@if (!loading() && loadError()) { + +
+
+ +

Failed to load {{ voteLabelPlural.toLowerCase() }}

+ +
+
+
+} @else if (!loading() && votes().length === 0 && !filters().search && !filters().status) { + +
+
+ +

No {{ voteLabelPlural }} yet

+

Votes created for this group will appear here.

+
+
+
+} @else { + + +} + + + diff --git a/apps/lfx-one/src/app/modules/committees/components/committee-votes-list/committee-votes-list.component.ts b/apps/lfx-one/src/app/modules/committees/components/committee-votes-list/committee-votes-list.component.ts new file mode 100644 index 000000000..e2a3ce38b --- /dev/null +++ b/apps/lfx-one/src/app/modules/committees/components/committee-votes-list/committee-votes-list.component.ts @@ -0,0 +1,180 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +import { Component, computed, inject, input, signal, Signal } from '@angular/core'; +import { takeUntilDestroyed, toObservable, toSignal } from '@angular/core/rxjs-interop'; +import { CardComponent } from '@components/card/card.component'; +import { VOTE_LABEL } from '@lfx-one/shared'; +import { PaginatedResponse, Vote, VoteFilterState } from '@lfx-one/shared/interfaces'; +import { VoteService } from '@services/vote.service'; +import { catchError, combineLatest, map, of, switchMap, tap } from 'rxjs'; + +import { VoteResultsDrawerComponent } from '@app/modules/votes/components/vote-results-drawer/vote-results-drawer.component'; +import { VotesTableComponent } from '@app/modules/votes/components/votes-table/votes-table.component'; + +@Component({ + selector: 'lfx-committee-votes-list', + imports: [CardComponent, VotesTableComponent, VoteResultsDrawerComponent], + templateUrl: './committee-votes-list.component.html', +}) +export class CommitteeVotesListComponent { + // -- Services -- + private readonly voteService = inject(VoteService); + + // -- Constants -- + protected readonly voteLabelPlural = VOTE_LABEL.plural; + + // -- Inputs -- + public readonly projectUid = input.required(); + public readonly committeeName = input.required(); + public readonly hasPMOAccess = input(false); + + // -- Trigger signals -- + private readonly fetchTrigger = signal(0); + private readonly refreshTrigger = signal(0); + + // -- Writable Signals -- + protected readonly loading = signal(true); + protected readonly loadError = signal(false); + protected readonly resultsDrawerVisible = signal(false); + protected readonly selectedVoteId = signal(null); + protected readonly rowsPerPage = signal(10); + protected readonly currentFirst = signal(0); + protected readonly totalRecords = signal(0); + + // -- Filter State -- + protected readonly filters = signal({ search: '', status: null, group: null }); + + // -- Page Tokens -- + private pageTokens: string[] = []; + + // -- Computed Signals -- + protected readonly votes: Signal = this.initVotes(); + protected readonly selectedListVote: Signal = this.initSelectedListVote(); + + public constructor() { + this.initTotalCountSubscription(); + } + + // -- Protected Methods -- + protected onViewVote(voteId: string): void { + this.selectedVoteId.set(voteId); + this.resultsDrawerVisible.set(true); + } + + protected refreshVotes(): void { + this.loading.set(true); + this.loadError.set(false); + this.pageTokens = []; + this.currentFirst.set(0); + this.fetchTrigger.update((v) => v + 1); + this.refreshTrigger.update((v) => v + 1); + } + + protected onPageChange(event: { first: number; rows: number }): void { + if (event.rows !== this.rowsPerPage()) { + this.pageTokens = []; + this.rowsPerPage.set(event.rows); + this.currentFirst.set(0); + this.fetchTrigger.update((v) => v + 1); + return; + } + + this.currentFirst.set(event.first); + this.fetchTrigger.update((v) => v + 1); + } + + protected onFiltersChange(state: VoteFilterState): void { + this.pageTokens = []; + this.currentFirst.set(0); + this.filters.set(state); + } + + // -- Private Helpers -- + private buildFilters(): string[] { + const queryFilters: string[] = [`committee_name:${this.committeeName()}`]; + const { status } = this.filters(); + if (status) { + queryFilters.push(`status:${status}`); + } + return queryFilters; + } + + // -- Private Initializers -- + private initTotalCountSubscription(): void { + const filters$ = toObservable(this.filters); + const projectUid$ = toObservable(this.projectUid); + const committeeName$ = toObservable(this.committeeName); + const refresh$ = toObservable(this.refreshTrigger); + + combineLatest([projectUid$, committeeName$, filters$, refresh$]) + .pipe( + switchMap(([projectUid]) => { + if (!projectUid) return of(0); + const searchName = this.filters().search; + const queryFilters = this.buildFilters(); + return this.voteService.getVotesCountByProject(projectUid, searchName || undefined, queryFilters).pipe(catchError(() => of(0))); + }), + tap((count) => this.totalRecords.set(count)), + takeUntilDestroyed() + ) + .subscribe(); + } + + private initVotes(): Signal { + const filters$ = toObservable(this.filters); + const projectUid$ = toObservable(this.projectUid); + const committeeName$ = toObservable(this.committeeName); + const fetch$ = toObservable(this.fetchTrigger); + + return toSignal( + combineLatest([projectUid$, committeeName$, filters$, fetch$]).pipe( + tap(() => this.loading.set(true)), + switchMap(([projectUid]) => { + if (!projectUid) { + this.loading.set(false); + return of([]); + } + + const rows = this.rowsPerPage(); + const first = this.currentFirst(); + const pageIndex = first / rows; + const pageToken = pageIndex > 0 ? this.pageTokens[pageIndex - 1] : undefined; + + if (pageIndex > 0 && !pageToken) { + this.currentFirst.set(0); + this.loading.set(false); + return of([]); + } + + const searchName = this.filters().search; + const queryFilters = this.buildFilters(); + + return this.voteService.getVotesByProjectPaginated(projectUid, rows, pageToken, searchName || undefined, queryFilters).pipe( + tap((response: PaginatedResponse) => { + if (response.page_token) { + this.pageTokens[pageIndex] = response.page_token; + } + this.loading.set(false); + }), + map((response: PaginatedResponse) => response.data), + catchError(() => { + this.loadError.set(true); + this.loading.set(false); + return of([]); + }) + ); + }) + ), + { initialValue: [] } + ); + } + + private initSelectedListVote(): Signal { + return computed(() => { + const id = this.selectedVoteId(); + if (!id) return null; + return this.votes().find((v) => v.uid === id) || null; + }); + } +} diff --git a/apps/lfx-one/src/app/modules/surveys/survey-manage/survey-manage.component.ts b/apps/lfx-one/src/app/modules/surveys/survey-manage/survey-manage.component.ts index c4bcdf9a5..24cc402bb 100644 --- a/apps/lfx-one/src/app/modules/surveys/survey-manage/survey-manage.component.ts +++ b/apps/lfx-one/src/app/modules/surveys/survey-manage/survey-manage.component.ts @@ -72,6 +72,10 @@ export class SurveyManageComponent { public currentStep: Signal = this.initCurrentStep(); public readonly submitButtonLabel: Signal = this.initSubmitButtonLabel(); + public constructor() { + this.preselectCommitteeFromQueryParams(); + } + public nextStep(): void { const next = this.currentStep() + 1; if (next <= this.totalSteps && this.canNavigateToStep(next)) { @@ -384,6 +388,17 @@ Thank you, } } + private preselectCommitteeFromQueryParams(): void { + const params = this.route.snapshot.queryParams; + const uid = params['committee_uid']; + const name = params['committee_name']; + if (uid && name) { + this.form() + .get('committees') + ?.setValue([{ uid, name, allowed_voting_statuses: [] }]); + } + } + private markAllFormControlsAsTouched(): void { markFormControlsAsTouched(this.form()); } diff --git a/apps/lfx-one/src/app/modules/votes/components/votes-table/votes-table.component.html b/apps/lfx-one/src/app/modules/votes/components/votes-table/votes-table.component.html index fa8a9f09f..cae0a1184 100644 --- a/apps/lfx-one/src/app/modules/votes/components/votes-table/votes-table.component.html +++ b/apps/lfx-one/src/app/modules/votes/components/votes-table/votes-table.component.html @@ -15,18 +15,20 @@ data-testid="votes-search-input"> -
- -
+ @if (!hideGroupFilter()) { +
+ +
+ }
(0); public readonly lazy = input(false); public readonly groupOptions = input<{ label: string; value: string | null }[]>([{ label: 'All Groups', value: null }]); + public readonly hideGroupFilter = input(false); // === Outputs === public readonly viewVote = output();