From 4f240f60d7b882d649bd3add6e78aaad4ef33fd8 Mon Sep 17 00:00:00 2001 From: Rashad Date: Sat, 20 Jun 2026 12:25:46 +0300 Subject: [PATCH 1/5] feat(votes): add permission check on committee Create Vote CTA click MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace routerLink on both Create Vote buttons (table-actions slot and empty-state) with an onCreateVote() click handler that fetches fresh committee permissions before navigating. If the member's role was downgraded from Manager to Member since the page loaded, the stale canEdit() signal would still show the button; the click handler catches this and redirects to /project/overview?_notice=votes so AppComponent shows the "Access Denied" toast — consistent with the writerGuard denial flow used for meetings. Signed-off-by: Rashad --- .../committee-votes.component.html | 6 ++-- .../committee-votes.component.ts | 30 ++++++++++++++++++- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/apps/lfx-one/src/app/modules/committees/components/committee-votes/committee-votes.component.html b/apps/lfx-one/src/app/modules/committees/components/committee-votes/committee-votes.component.html index 77233b7c6..d26ab5be4 100644 --- a/apps/lfx-one/src/app/modules/committees/components/committee-votes/committee-votes.component.html +++ b/apps/lfx-one/src/app/modules/committees/components/committee-votes/committee-votes.component.html @@ -17,8 +17,7 @@ icon="fa-light fa-check-to-slot" severity="info" size="small" - [routerLink]="['/votes', 'create']" - [queryParams]="createVoteQueryParams()" + (click)="onCreateVote()" data-testid="committee-votes-create-btn"> } @@ -30,8 +29,7 @@ icon="fa-light fa-check-to-slot" severity="info" size="small" - [routerLink]="['/votes', 'create']" - [queryParams]="createVoteQueryParams()" + (click)="onCreateVote()" data-testid="committee-votes-empty-create-btn"> } diff --git a/apps/lfx-one/src/app/modules/committees/components/committee-votes/committee-votes.component.ts b/apps/lfx-one/src/app/modules/committees/components/committee-votes/committee-votes.component.ts index fa205dfb8..8444c5e53 100644 --- a/apps/lfx-one/src/app/modules/committees/components/committee-votes/committee-votes.component.ts +++ b/apps/lfx-one/src/app/modules/committees/components/committee-votes/committee-votes.component.ts @@ -3,15 +3,17 @@ import { ChangeDetectionStrategy, Component, computed, inject, input, model, signal, Signal } from '@angular/core'; import { toObservable, toSignal } from '@angular/core/rxjs-interop'; +import { Router } from '@angular/router'; import { ButtonComponent } from '@components/button/button.component'; import { CardComponent } from '@components/card/card.component'; import { Committee, Vote } from '@lfx-one/shared/interfaces'; import { buildCommitteeCreateQueryParams } from '@lfx-one/shared/utils'; import { VotesTableComponent } from '@app/modules/votes/components/votes-table/votes-table.component'; import { VoteResultsDrawerComponent } from '@app/modules/votes/components/vote-results-drawer/vote-results-drawer.component'; +import { CommitteeService } from '@services/committee.service'; import { VoteService } from '@services/vote.service'; import { MessageService } from 'primeng/api'; -import { catchError, filter, finalize, of, switchMap } from 'rxjs'; +import { catchError, filter, finalize, of, switchMap, take } from 'rxjs'; @Component({ selector: 'lfx-committee-votes', @@ -21,8 +23,10 @@ import { catchError, filter, finalize, of, switchMap } from 'rxjs'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class CommitteeVotesComponent { + private readonly committeeService = inject(CommitteeService); private readonly voteService = inject(VoteService); private readonly messageService = inject(MessageService); + private readonly router = inject(Router); // Inputs public committee = input.required(); @@ -38,6 +42,30 @@ export class CommitteeVotesComponent { public votes: Signal = this.initVotes(); public createVoteQueryParams: Signal> = this.initCreateVoteQueryParams(); + /** Checks committee write permission fresh before navigating to the create-vote route. + * Redirects to project overview with _notice=votes if permission has been revoked + * since the page loaded — consistent with the writerGuard denial flow. */ + public onCreateVote(): void { + const committee = this.committee(); + const denyParams: Record = { _notice: 'votes' }; + if (committee.project_slug) denyParams['project'] = committee.project_slug; + const deny = () => void this.router.navigate(['/project/overview'], { queryParams: denyParams }); + + this.committeeService + .getCommittee(committee.uid) + .pipe(take(1)) + .subscribe({ + next: (fresh) => { + if (fresh?.writer !== true) { + deny(); + return; + } + void this.router.navigate(['/votes', 'create'], { queryParams: this.createVoteQueryParams() }); + }, + error: () => deny(), + }); + } + /** Opens the vote results drawer for the selected vote. */ public viewVoteResults(voteUid: string): void { const vote = this.votes().find((v) => v.uid === voteUid) || null; From 1b0e90393fd956b2b62c42591f7d78c82295b05d Mon Sep 17 00:00:00 2001 From: Rashad Date: Sat, 20 Jun 2026 12:36:14 +0300 Subject: [PATCH 2/5] fix(review): address PR #997 Copilot feedback on lens-aware deny redirect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inject LensService and derive the overview path from the active lens so the access-denied redirect lands on /foundation/overview when the component is rendered under the foundation lens (/foundation/groups/...) rather than always redirecting to /project/overview — consistent with writerGuard's lens-aware denial behavior (per @copilot-pull-request-reviewer). Signed-off-by: Rashad --- .../components/committee-votes/committee-votes.component.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/lfx-one/src/app/modules/committees/components/committee-votes/committee-votes.component.ts b/apps/lfx-one/src/app/modules/committees/components/committee-votes/committee-votes.component.ts index 8444c5e53..76776a9ba 100644 --- a/apps/lfx-one/src/app/modules/committees/components/committee-votes/committee-votes.component.ts +++ b/apps/lfx-one/src/app/modules/committees/components/committee-votes/committee-votes.component.ts @@ -11,6 +11,7 @@ import { buildCommitteeCreateQueryParams } from '@lfx-one/shared/utils'; import { VotesTableComponent } from '@app/modules/votes/components/votes-table/votes-table.component'; import { VoteResultsDrawerComponent } from '@app/modules/votes/components/vote-results-drawer/vote-results-drawer.component'; import { CommitteeService } from '@services/committee.service'; +import { LensService } from '@services/lens.service'; import { VoteService } from '@services/vote.service'; import { MessageService } from 'primeng/api'; import { catchError, filter, finalize, of, switchMap, take } from 'rxjs'; @@ -24,6 +25,7 @@ import { catchError, filter, finalize, of, switchMap, take } from 'rxjs'; }) export class CommitteeVotesComponent { private readonly committeeService = inject(CommitteeService); + private readonly lensService = inject(LensService); private readonly voteService = inject(VoteService); private readonly messageService = inject(MessageService); private readonly router = inject(Router); @@ -47,9 +49,10 @@ export class CommitteeVotesComponent { * since the page loaded — consistent with the writerGuard denial flow. */ public onCreateVote(): void { const committee = this.committee(); + const overviewPath = this.lensService.activeLens() === 'foundation' ? '/foundation/overview' : '/project/overview'; const denyParams: Record = { _notice: 'votes' }; if (committee.project_slug) denyParams['project'] = committee.project_slug; - const deny = () => void this.router.navigate(['/project/overview'], { queryParams: denyParams }); + const deny = () => void this.router.navigate([overviewPath], { queryParams: denyParams }); this.committeeService .getCommittee(committee.uid) From e4839f6442917137f04eab81e7878435e9f18132 Mon Sep 17 00:00:00 2001 From: Rashad Date: Fri, 26 Jun 2026 18:15:02 +0300 Subject: [PATCH 3/5] feat(votes): pass committee queryParams on vote edit navigation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add editQueryParams input to lfx-votes-table and wire [queryParams] onto the edit routerLink so writerGuard receives project and committee_uid when a committee manager navigates to edit a draft vote — mirrors the meetings fix (PR #1025). - votes-table: add editQueryParams input (defaults to {}; non-breaking for all existing usages outside committee context) - votes-table: bind [queryParams]="editQueryParams()" on edit - committee-votes: add editVoteQueryParams computed signal via buildCommitteeCreateQueryParams; pass to [editQueryParams] binding Signed-off-by: Rashad --- .../components/committee-votes/committee-votes.component.html | 1 + .../components/committee-votes/committee-votes.component.ts | 1 + .../votes/components/votes-table/votes-table.component.html | 2 +- .../votes/components/votes-table/votes-table.component.ts | 1 + 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/lfx-one/src/app/modules/committees/components/committee-votes/committee-votes.component.html b/apps/lfx-one/src/app/modules/committees/components/committee-votes/committee-votes.component.html index d26ab5be4..b85ab5ff8 100644 --- a/apps/lfx-one/src/app/modules/committees/components/committee-votes/committee-votes.component.html +++ b/apps/lfx-one/src/app/modules/committees/components/committee-votes/committee-votes.component.html @@ -8,6 +8,7 @@ [hasPMOAccess]="canEdit()" [loading]="loading()" [totalRecords]="votes().length" + [editQueryParams]="editVoteQueryParams()" (viewResults)="viewVoteResults($event)" (viewVote)="viewVoteResults($event)"> @if (canEdit()) { diff --git a/apps/lfx-one/src/app/modules/committees/components/committee-votes/committee-votes.component.ts b/apps/lfx-one/src/app/modules/committees/components/committee-votes/committee-votes.component.ts index 76776a9ba..d7484f978 100644 --- a/apps/lfx-one/src/app/modules/committees/components/committee-votes/committee-votes.component.ts +++ b/apps/lfx-one/src/app/modules/committees/components/committee-votes/committee-votes.component.ts @@ -43,6 +43,7 @@ export class CommitteeVotesComponent { // Data public votes: Signal = this.initVotes(); public createVoteQueryParams: Signal> = this.initCreateVoteQueryParams(); + public editVoteQueryParams: Signal> = computed(() => buildCommitteeCreateQueryParams(this.committee())); /** Checks committee write permission fresh before navigating to the create-vote route. * Redirects to project overview with _notice=votes if permission has been revoked 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 f0f0b0247..9e800e77a 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 @@ -156,7 +156,7 @@
@if (hasPMOAccess()) { @if (vote.status === PollStatus.DISABLED) { - + (false); // Draft tab is only meaningful in management contexts (project/committee lens); hide it in the Me lens. public readonly showDraftTab = input(true); + public readonly editQueryParams = input>({}); // === Outputs === public readonly viewVote = output(); From 7268ddcbd4ef1d4cd5d662c01eedeaeff71f1769 Mon Sep 17 00:00:00 2001 From: Rashad Date: Fri, 26 Jun 2026 19:38:25 +0300 Subject: [PATCH 4/5] fix(review): address PR #997 review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address Copilot review comments: - committee-votes.component.ts: switch getCommittee() to fetchCommittee() in onCreateVote() to avoid mutating shared committee service state during a permission-only check (per T1) - committee-votes.component.ts: alias editVoteQueryParams to createVoteQueryParams instead of duplicating the computed (per T3) - votes-table.component.html: replace wrapper with direct [routerLink] and [queryParams] inputs on — ButtonComponent already supports these inputs; wrapper created invalid nested interactive elements - votes-table.component.html: add (onClick)=stopPropagation to prevent row-select from firing alongside edit route navigation in selectionMode table - votes-table.component.ts: remove now-unused RouterLink import Resolves 3 review threads (T1, T3, implicit wrapper issue). Signed-off-by: Rashad --- .../committee-votes/committee-votes.component.ts | 4 ++-- .../votes-table/votes-table.component.html | 13 ++++++++++--- .../components/votes-table/votes-table.component.ts | 2 -- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/apps/lfx-one/src/app/modules/committees/components/committee-votes/committee-votes.component.ts b/apps/lfx-one/src/app/modules/committees/components/committee-votes/committee-votes.component.ts index d7484f978..26917287d 100644 --- a/apps/lfx-one/src/app/modules/committees/components/committee-votes/committee-votes.component.ts +++ b/apps/lfx-one/src/app/modules/committees/components/committee-votes/committee-votes.component.ts @@ -43,7 +43,7 @@ export class CommitteeVotesComponent { // Data public votes: Signal = this.initVotes(); public createVoteQueryParams: Signal> = this.initCreateVoteQueryParams(); - public editVoteQueryParams: Signal> = computed(() => buildCommitteeCreateQueryParams(this.committee())); + public editVoteQueryParams: Signal> = this.createVoteQueryParams; /** Checks committee write permission fresh before navigating to the create-vote route. * Redirects to project overview with _notice=votes if permission has been revoked @@ -56,7 +56,7 @@ export class CommitteeVotesComponent { const deny = () => void this.router.navigate([overviewPath], { queryParams: denyParams }); this.committeeService - .getCommittee(committee.uid) + .fetchCommittee(committee.uid) .pipe(take(1)) .subscribe({ next: (fresh) => { 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 9e800e77a..e8c3604db 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 @@ -156,9 +156,16 @@
@if (hasPMOAccess()) { @if (vote.status === PollStatus.DISABLED) { - - - + + Date: Sat, 27 Jun 2026 21:03:41 +0300 Subject: [PATCH 5/5] fix(review): address PR #997 review feedback (round 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review comments from @dealako and @copilot: - committee-votes.component.ts: remove redundant .pipe(take(1)) from onCreateVote() — fetchCommittee() already applies take(1) internally (CommitteeService line 77), so the extra pipe was misleading - committee-votes.component.ts: drop now-unused `take` from RxJS imports - committee-votes.component.ts: change onCreateVote() from public to protected — it is only called from the template (Angular 20 convention) Resolves 5 review threads (4 from @dealako + @copilot on take(1)/import, 1 from @dealako on public/protected). Signed-off-by: Rashad --- .../components/committee-votes/committee-votes.component.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/lfx-one/src/app/modules/committees/components/committee-votes/committee-votes.component.ts b/apps/lfx-one/src/app/modules/committees/components/committee-votes/committee-votes.component.ts index 26917287d..3bb0173b2 100644 --- a/apps/lfx-one/src/app/modules/committees/components/committee-votes/committee-votes.component.ts +++ b/apps/lfx-one/src/app/modules/committees/components/committee-votes/committee-votes.component.ts @@ -14,7 +14,7 @@ import { CommitteeService } from '@services/committee.service'; import { LensService } from '@services/lens.service'; import { VoteService } from '@services/vote.service'; import { MessageService } from 'primeng/api'; -import { catchError, filter, finalize, of, switchMap, take } from 'rxjs'; +import { catchError, filter, finalize, of, switchMap } from 'rxjs'; @Component({ selector: 'lfx-committee-votes', @@ -48,7 +48,7 @@ export class CommitteeVotesComponent { /** Checks committee write permission fresh before navigating to the create-vote route. * Redirects to project overview with _notice=votes if permission has been revoked * since the page loaded — consistent with the writerGuard denial flow. */ - public onCreateVote(): void { + protected onCreateVote(): void { const committee = this.committee(); const overviewPath = this.lensService.activeLens() === 'foundation' ? '/foundation/overview' : '/project/overview'; const denyParams: Record = { _notice: 'votes' }; @@ -57,7 +57,6 @@ export class CommitteeVotesComponent { this.committeeService .fetchCommittee(committee.uid) - .pipe(take(1)) .subscribe({ next: (fresh) => { if (fresh?.writer !== true) {