Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<!-- Copyright The Linux Foundation and each contributor to LFX. -->
<!-- SPDX-License-Identifier: MIT -->

@if (!loading() && loadError()) {
<lfx-card data-testid="committee-surveys-error-state">
<div class="py-8">
<div class="text-center flex flex-col gap-3">
<i class="fa-light fa-triangle-exclamation text-3xl text-red-400"></i>
<p class="text-sm font-medium text-gray-600">Failed to load {{ surveyLabelPlural.toLowerCase() }}</p>
<button
type="button"
class="text-blue-600 hover:text-blue-700 hover:underline text-xs bg-transparent border-none cursor-pointer"
(click)="refreshSurveys()"
data-testid="committee-surveys-retry">
Try again
</button>
</div>
</div>
</lfx-card>
} @else if (!loading() && surveys().length === 0) {
<lfx-card data-testid="committee-surveys-empty-state">
<div class="py-8">
<div class="text-center flex flex-col gap-3">
<i class="fa-light fa-clipboard-list text-3xl text-gray-300"></i>
<p class="text-sm font-medium text-gray-600">No {{ surveyLabelPlural }} yet</p>
<p class="text-xs text-gray-400">Surveys created for this group will appear here.</p>
</div>
</div>
</lfx-card>
} @else {
<lfx-surveys-table
[surveys]="surveys()"
[hasPMOAccess]="hasPMOAccess()"
[loading]="loading()"
(viewResults)="onViewResults($event)"
(rowClick)="onRowClick($event)"
(refresh)="refreshSurveys()"
data-testid="committee-surveys-table">
</lfx-surveys-table>
}

<lfx-survey-results-drawer
[(visible)]="resultsDrawerVisible"
[surveyId]="selectedSurveyId()"
[listSurvey]="selectedListSurvey()"
[hasPMOAccess]="hasPMOAccess()"
(duplicate)="onDuplicateSurvey()"
(closeSurvey)="onCloseSurvey()"
data-testid="committee-survey-results-drawer">
</lfx-survey-results-drawer>
Original file line number Diff line number Diff line change
@@ -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<string>();
public readonly hasPMOAccess = input<boolean>(false);

// -- Writable Signals --
private readonly refreshTrigger = signal(0);
protected readonly loading = signal<boolean>(true);
protected readonly loadError = signal<boolean>(false);
protected readonly resultsDrawerVisible = signal<boolean>(false);
protected readonly selectedSurveyId = signal<string | null>(null);
protected readonly selectedListSurvey = signal<Survey | null>(null);

// -- Computed Signals --
protected readonly surveys: Signal<Survey[]> = 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<Survey[]> {
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: [] }
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<!-- Copyright The Linux Foundation and each contributor to LFX. -->
<!-- SPDX-License-Identifier: MIT -->

@if (!loading() && loadError()) {
<lfx-card data-testid="committee-votes-error-state">
<div class="py-8">
<div class="text-center flex flex-col gap-3">
<i class="fa-light fa-triangle-exclamation text-3xl text-red-400"></i>
<p class="text-sm font-medium text-gray-600">Failed to load {{ voteLabelPlural.toLowerCase() }}</p>
<button
type="button"
class="text-blue-600 hover:text-blue-700 hover:underline text-xs bg-transparent border-none cursor-pointer"
(click)="refreshVotes()"
data-testid="committee-votes-retry">
Try again
</button>
</div>
</div>
</lfx-card>
} @else if (!loading() && votes().length === 0 && !filters().search && !filters().status) {
<lfx-card data-testid="committee-votes-empty-state">
<div class="py-8">
<div class="text-center flex flex-col gap-3">
<i class="fa-light fa-check-to-slot text-3xl text-gray-300"></i>
<p class="text-sm font-medium text-gray-600">No {{ voteLabelPlural }} yet</p>
<p class="text-xs text-gray-400">Votes created for this group will appear here.</p>
</div>
</div>
</lfx-card>
} @else {
<lfx-votes-table
[votes]="votes()"
[hasPMOAccess]="hasPMOAccess()"
[loading]="loading()"
[totalRecords]="totalRecords()"
[rowsPerPage]="rowsPerPage()"
[first]="currentFirst()"
[lazy]="true"
[hideGroupFilter]="true"
(viewVote)="onViewVote($event)"
(viewResults)="onViewVote($event)"
(refresh)="refreshVotes()"
(pageChange)="onPageChange($event)"
(filtersChange)="onFiltersChange($event)"
data-testid="committee-votes-table">
</lfx-votes-table>
}

<lfx-vote-results-drawer
[(visible)]="resultsDrawerVisible"
[voteId]="selectedVoteId()"
[listVote]="selectedListVote()"
data-testid="committee-vote-results-drawer">
</lfx-vote-results-drawer>
Original file line number Diff line number Diff line change
@@ -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<string>();
public readonly committeeName = input.required<string>();
public readonly hasPMOAccess = input<boolean>(false);

// -- Trigger signals --
private readonly fetchTrigger = signal(0);
private readonly refreshTrigger = signal(0);

// -- Writable Signals --
protected readonly loading = signal<boolean>(true);
protected readonly loadError = signal<boolean>(false);
protected readonly resultsDrawerVisible = signal<boolean>(false);
protected readonly selectedVoteId = signal<string | null>(null);
protected readonly rowsPerPage = signal<number>(10);
protected readonly currentFirst = signal<number>(0);
protected readonly totalRecords = signal<number>(0);

// -- Filter State --
protected readonly filters = signal<VoteFilterState>({ search: '', status: null, group: null });

// -- Page Tokens --
private pageTokens: string[] = [];

// -- Computed Signals --
protected readonly votes: Signal<Vote[]> = this.initVotes();
protected readonly selectedListVote: Signal<Vote | null> = 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<Vote[]> {
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();

Comment on lines +143 to +152
return this.voteService.getVotesByProjectPaginated(projectUid, rows, pageToken, searchName || undefined, queryFilters).pipe(
tap((response: PaginatedResponse<Vote>) => {
if (response.page_token) {
this.pageTokens[pageIndex] = response.page_token;
}
this.loading.set(false);
}),
map((response: PaginatedResponse<Vote>) => response.data),
catchError(() => {
this.loadError.set(true);
this.loading.set(false);
return of([]);
})
);
})
),
{ initialValue: [] }
);
}

private initSelectedListVote(): Signal<Vote | null> {
return computed(() => {
const id = this.selectedVoteId();
if (!id) return null;
return this.votes().find((v) => v.uid === id) || null;
});
}
}
Loading
Loading