Skip to content

Commit 675a00b

Browse files
authored
feat: complete user permissions management system (#24)
* feat: implement complete user permissions management system - Convert getCommitteeNames function to committee-names.pipe for better performance - Add permissions matrix component explaining different permission levels and scopes - Implement edit user functionality with proper form initialization and validation - Add comprehensive remove user functionality that preserves user records - Fix backend API to handle permission updates with proper 404 handling - Move committee names pipe to shared pipes folder for reusability - Update permission service methods to match backend API contracts - Ensure proper error handling for all CRUD operations on user permissions Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Asitha de Silva <asithade@gmail.com> * refactor: update language Signed-off-by: Asitha de Silva <asithade@gmail.com> * refactor: address pr comments Signed-off-by: Asitha de Silva <asithade@gmail.com> * refactor: standardized data refresh (#25) Signed-off-by: Asitha de Silva <asithade@gmail.com> * fix: update auth0 algorithm to its default RS256 value Signed-off-by: Asitha de Silva <asithade@gmail.com> --------- Signed-off-by: Asitha de Silva <asithade@gmail.com>
1 parent 71d3d78 commit 675a00b

31 files changed

+1249
-316
lines changed

apps/lfx-pcc/src/app/app.component.scss

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,12 @@
3030
}
3131
}
3232

33-
.p-select-overlay {
34-
.p-select-list-container {
35-
.p-select-option {
33+
.p-select-overlay,
34+
.p-multiselect-overlay {
35+
.p-select-list-container,
36+
.p-multiselect-list-container {
37+
.p-select-option,
38+
.p-multiselect-option {
3639
@apply text-sm;
3740
}
3841
}

apps/lfx-pcc/src/app/modules/project/committees/committee-dashboard/committee-dashboard.component.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import { AnimateOnScrollModule } from 'primeng/animateonscroll';
1919
import { ConfirmationService, MenuItem } from 'primeng/api';
2020
import { ConfirmDialogModule } from 'primeng/confirmdialog';
2121
import { DialogService, DynamicDialogModule, DynamicDialogRef } from 'primeng/dynamicdialog';
22-
import { of } from 'rxjs';
23-
import { debounceTime, distinctUntilChanged, startWith, tap } from 'rxjs/operators';
22+
import { BehaviorSubject, of } from 'rxjs';
23+
import { debounceTime, distinctUntilChanged, startWith, switchMap, tap } from 'rxjs/operators';
2424

2525
import { CommitteeFormComponent } from '../components/committee-form/committee-form.component';
2626
import { UpcomingCommitteeMeetingComponent } from '../components/upcoming-committee-meeting/upcoming-committee-meeting.component';
@@ -55,22 +55,23 @@ export class CommitteeDashboardComponent {
5555
private readonly dialogService = inject(DialogService);
5656

5757
// Class variables with types
58-
private dialogRef: DynamicDialogRef | undefined;
5958
public project: typeof this.projectService.project;
6059
public selectedCommittee: WritableSignal<Committee | null>;
6160
public isDeleting: WritableSignal<boolean>;
6261
public first: WritableSignal<number>;
6362
public rows: number;
6463
public searchForm: FormGroup;
6564
public categoryFilter: WritableSignal<string | null>;
66-
private searchTerm: Signal<string>;
6765
public committeesLoading: WritableSignal<boolean>;
6866
public committees: Signal<Committee[]>;
6967
public categories: Signal<{ label: string; value: string | null }[]>;
7068
public filteredCommittees: Signal<Committee[]>;
7169
public totalRecords: Signal<number>;
7270
public menuItems: MenuItem[];
7371
public actionMenuItems: MenuItem[];
72+
public refresh: BehaviorSubject<void>;
73+
private searchTerm: Signal<string>;
74+
private dialogRef: DynamicDialogRef | undefined;
7475

7576
public constructor() {
7677
// Initialize all class variables
@@ -80,6 +81,7 @@ export class CommitteeDashboardComponent {
8081
this.first = signal<number>(0);
8182
this.rows = 10;
8283
this.committeesLoading = signal<boolean>(true);
84+
this.refresh = new BehaviorSubject<void>(undefined);
8385
this.committees = this.initializeCommittees();
8486
this.searchForm = this.initializeSearchForm();
8587
this.categoryFilter = signal<string | null>(null);
@@ -189,9 +191,7 @@ export class CommitteeDashboardComponent {
189191
}
190192

191193
private refreshCommittees(): void {
192-
this.router.navigate(['/project', this.project()?.slug], { skipLocationChange: true }).then(() => {
193-
this.router.navigate(['/project', this.project()?.slug, 'committees']);
194-
});
194+
this.refresh.next();
195195
}
196196

197197
private openEditDialog(): void {
@@ -227,7 +227,12 @@ export class CommitteeDashboardComponent {
227227

228228
private initializeCommittees(): Signal<Committee[]> {
229229
return toSignal(
230-
this.project() ? this.committeeService.getCommitteesByProject(this.project()!.id).pipe(tap(() => this.committeesLoading.set(false))) : of([]),
230+
this.project()
231+
? this.refresh.pipe(
232+
tap(() => this.committeesLoading.set(true)),
233+
switchMap(() => this.committeeService.getCommitteesByProject(this.project()!.id).pipe(tap(() => this.committeesLoading.set(false))))
234+
)
235+
: of([]),
231236
{ initialValue: [] }
232237
);
233238
}

apps/lfx-pcc/src/app/modules/project/committees/committee-view/committee-view.component.html

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,11 @@ <h1 class="text-3xl font-bold text-gray-900 mb-2">{{ committee()?.name }}</h1>
6262
<div class="lg:col-span-2 flex flex-col gap-6">
6363
<!-- Members Section -->
6464
@if (committee()?.id) {
65-
<lfx-committee-members [committee]="committee()" (refresh)="refreshMembers()"></lfx-committee-members>
65+
<lfx-committee-members
66+
[committee]="committee()"
67+
[members]="members()"
68+
[membersLoading]="membersLoading()"
69+
(refresh)="refreshMembers()"></lfx-committee-members>
6670
}
6771

6872
<!-- Subcommittees Section -->

apps/lfx-pcc/src/app/modules/project/committees/committee-view/committee-view.component.ts

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,20 @@
22
// SPDX-License-Identifier: MIT
33

44
import { CommonModule } from '@angular/common';
5-
import { Component, computed, inject, signal, Signal, WritableSignal } from '@angular/core';
5+
import { Component, computed, inject, Injector, signal, Signal, WritableSignal } from '@angular/core';
66
import { toSignal } from '@angular/core/rxjs-interop';
77
import { ActivatedRoute, Router } from '@angular/router';
88
import { ButtonComponent } from '@components/button/button.component';
99
import { CardComponent } from '@components/card/card.component';
1010
import { MenuComponent } from '@components/menu/menu.component';
1111
import { TableComponent } from '@components/table/table.component';
12-
import { Committee } from '@lfx-pcc/shared/interfaces';
12+
import { Committee, CommitteeMember } from '@lfx-pcc/shared/interfaces';
1313
import { CommitteeService } from '@services/committee.service';
1414
import { ProjectService } from '@services/project.service';
1515
import { ConfirmationService, MenuItem } from 'primeng/api';
1616
import { ConfirmDialogModule } from 'primeng/confirmdialog';
1717
import { DialogService, DynamicDialogModule, DynamicDialogRef } from 'primeng/dynamicdialog';
18-
import { of, switchMap } from 'rxjs';
18+
import { BehaviorSubject, combineLatest, of, switchMap } from 'rxjs';
1919

2020
import { CommitteeFormComponent } from '../components/committee-form/committee-form.component';
2121
import { CommitteeMembersComponent } from '../components/committee-members/committee-members.component';
@@ -46,25 +46,32 @@ export class CommitteeViewComponent {
4646
private readonly committeeService = inject(CommitteeService);
4747
private readonly confirmationService = inject(ConfirmationService);
4848
private readonly dialogService = inject(DialogService);
49+
private readonly injector = inject(Injector);
4950

5051
// Class variables with types
5152
private dialogRef: DynamicDialogRef | undefined;
5253
public project: typeof this.projectService.project;
5354
public committee: Signal<Committee | null>;
54-
public loading: Signal<boolean>;
55+
public members: WritableSignal<CommitteeMember[]>;
56+
public membersLoading: WritableSignal<boolean>;
57+
public loading: WritableSignal<boolean>;
5558
public error: WritableSignal<boolean>;
5659
public isDeleting: WritableSignal<boolean>;
5760
public actionMenuItems: MenuItem[];
5861
public formattedCreatedDate: Signal<string>;
5962
public formattedUpdatedDate: Signal<string>;
63+
public refresh: BehaviorSubject<void>;
6064

6165
public constructor() {
6266
// Initialize all class variables
6367
this.project = this.projectService.project;
6468
this.error = signal<boolean>(false);
6569
this.isDeleting = signal<boolean>(false);
70+
this.refresh = new BehaviorSubject<void>(undefined);
71+
this.members = signal<CommitteeMember[]>([]);
72+
this.membersLoading = signal<boolean>(true);
73+
this.loading = signal<boolean>(true);
6674
this.committee = this.initializeCommittee();
67-
this.loading = this.initializeLoading();
6875
this.actionMenuItems = this.initializeActionMenuItems();
6976
this.formattedCreatedDate = this.initializeFormattedCreatedDate();
7077
this.formattedUpdatedDate = this.initializeFormattedUpdatedDate();
@@ -83,13 +90,7 @@ export class CommitteeViewComponent {
8390
}
8491

8592
public refreshMembers(): void {
86-
this.router
87-
.navigate(['/project', this.project()?.slug, 'committees'], {
88-
skipLocationChange: true,
89-
})
90-
.then(() => {
91-
this.router.navigate(['/project', this.project()?.slug, 'committees', this.committee()?.id]);
92-
});
93+
this.refresh.next();
9394
}
9495

9596
// Action handlers
@@ -148,26 +149,28 @@ export class CommitteeViewComponent {
148149

149150
private initializeCommittee(): Signal<Committee | null> {
150151
return toSignal(
151-
this.route.paramMap.pipe(
152-
switchMap((params) => {
153-
const committeeId = params.get('id');
152+
combineLatest([this.route.paramMap, this.refresh]).pipe(
153+
switchMap(([params]) => {
154+
const committeeId = params?.get('id');
154155
if (!committeeId) {
155156
this.error.set(true);
156157
return of(null);
157158
}
158-
return this.committeeService.getCommittee(committeeId);
159+
160+
return combineLatest([this.committeeService.getCommittee(committeeId), this.committeeService.getCommitteeMembers(committeeId)]).pipe(
161+
switchMap(([committee, members]) => {
162+
this.members.set(members);
163+
this.loading.set(false);
164+
this.membersLoading.set(false);
165+
return of(committee);
166+
})
167+
);
159168
})
160169
),
161170
{ initialValue: null }
162171
);
163172
}
164173

165-
private initializeLoading(): Signal<boolean> {
166-
return computed(() => {
167-
return !this.error() && this.committee() === null;
168-
});
169-
}
170-
171174
private initializeActionMenuItems(): MenuItem[] {
172175
return [
173176
{

apps/lfx-pcc/src/app/modules/project/committees/components/committee-form/committee-form.component.html

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<!-- Copyright The Linux Foundation and each contributor to LFX. -->
22
<!-- SPDX-License-Identifier: MIT -->
33

4-
<form [formGroup]="form()" (ngSubmit)="handleSubmit()" class="space-y-6">
4+
<form [formGroup]="form()" (ngSubmit)="onSubmit()" class="space-y-6">
55
<!-- Basic Information Section -->
66
<div class="flex flex-col gap-3">
77
<h3 class="text-lg font-medium text-gray-900">Basic Information</h3>
@@ -115,13 +115,12 @@ <h3 class="text-lg font-medium text-gray-900">Additional Information</h3>
115115

116116
<!-- Form Actions -->
117117
<div class="flex justify-end gap-3 pt-6 border-t">
118-
<lfx-button label="Cancel" severity="secondary" [outlined]="true" (click)="handleCancel()" size="small" type="button"></lfx-button>
118+
<lfx-button label="Cancel" severity="secondary" [outlined]="true" (click)="onCancel()" size="small" type="button"></lfx-button>
119119
<lfx-button
120120
[label]="isEditing() ? 'Update Committee' : 'Create Committee'"
121121
[loading]="loading()"
122122
[disabled]="loading()"
123123
type="submit"
124-
size="small"
125-
(onClick)="handleSubmit()"></lfx-button>
124+
size="small"></lfx-button>
126125
</div>
127126
</form>

apps/lfx-pcc/src/app/modules/project/committees/components/committee-form/committee-form.component.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export class CommitteeFormComponent {
5151
}
5252

5353
// Form submission handler
54-
protected handleSubmit(): void {
54+
protected onSubmit(): void {
5555
this.markAllFieldsAsTouched();
5656

5757
if (this.form().valid) {
@@ -66,23 +66,23 @@ export class CommitteeFormComponent {
6666
this.committeeService.updateCommittee(committeeId, formValue).subscribe({
6767
next: () => {
6868
this.submitting.set(false);
69-
this.handleSuccess();
69+
this.onSuccess();
7070
},
7171
error: (error) => {
7272
this.submitting.set(false);
73-
this.handleError('Failed to update committee:', error);
73+
this.onError('Failed to update committee:', error);
7474
},
7575
});
7676
} else {
7777
// Create new committee
7878
this.committeeService.createCommittee(formValue).subscribe({
7979
next: () => {
8080
this.submitting.set(false);
81-
this.handleSuccess();
81+
this.onSuccess();
8282
},
8383
error: (error) => {
8484
this.submitting.set(false);
85-
this.handleError('Failed to create committee:', error);
85+
this.onError('Failed to create committee:', error);
8686
},
8787
});
8888
}
@@ -92,7 +92,7 @@ export class CommitteeFormComponent {
9292
}
9393

9494
// Cancel handler
95-
protected handleCancel(): void {
95+
protected onCancel(): void {
9696
if (this.config.data?.onCancel) {
9797
this.config.data.onCancel();
9898
} else {
@@ -127,7 +127,7 @@ export class CommitteeFormComponent {
127127
}
128128

129129
// Success handler
130-
private handleSuccess(): void {
130+
private onSuccess(): void {
131131
const isEditing = this.isEditing();
132132
const action = isEditing ? 'updated' : 'created';
133133

@@ -149,7 +149,7 @@ export class CommitteeFormComponent {
149149
}
150150

151151
// Error handler
152-
private handleError(message: string, error: any): void {
152+
private onError(message: string, error: any): void {
153153
console.error(message, error);
154154

155155
this.messageService.add({

apps/lfx-pcc/src/app/modules/project/committees/components/committee-members/committee-members.component.html

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -191,23 +191,5 @@ <h3 class="text-lg font-medium text-gray-900 mb-2">No Members Yet</h3>
191191
}
192192
</div>
193193
}
194-
} @else {
195-
@if (membersLoading()) {
196-
<div class="flex justify-center items-center h-64">
197-
<i class="fa-light fa-circle-notch fa-spin text-4xl text-gray-400"></i>
198-
</div>
199-
} @else {
200-
<div class="text-center py-8">
201-
<i class="fa-light fa-users text-4xl text-gray-400 mb-4"></i>
202-
@if (members().length > 0) {
203-
<h3 class="text-lg font-medium text-gray-900 mb-2">No Members Found</h3>
204-
<p class="text-gray-600 mb-4">Try adjusting your filters to find members.</p>
205-
} @else {
206-
<h3 class="text-lg font-medium text-gray-900 mb-2">No Members Yet</h3>
207-
<p class="text-gray-600 mb-4">This committee doesn't have any members yet.</p>
208-
<lfx-button label="Add First Member" icon="fa-light fa-user-plus" severity="secondary" (onClick)="openAddMemberDialog()"> </lfx-button>
209-
}
210-
</div>
211-
}
212194
}
213195
</div>

apps/lfx-pcc/src/app/modules/project/committees/components/committee-members/committee-members.component.ts

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// SPDX-License-Identifier: MIT
33

44
import { CommonModule } from '@angular/common';
5-
import { Component, computed, inject, Injector, input, OnInit, output, runInInjectionContext, signal, Signal, WritableSignal } from '@angular/core';
5+
import { Component, computed, inject, Injector, input, OnInit, output, signal, Signal, WritableSignal } from '@angular/core';
66
import { toSignal } from '@angular/core/rxjs-interop';
77
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
88
import { ButtonComponent } from '@components/button/button.component';
@@ -18,7 +18,7 @@ import { AnimateOnScrollModule } from 'primeng/animateonscroll';
1818
import { ConfirmationService, MenuItem, MessageService } from 'primeng/api';
1919
import { ConfirmDialogModule } from 'primeng/confirmdialog';
2020
import { DialogService, DynamicDialogModule, DynamicDialogRef } from 'primeng/dynamicdialog';
21-
import { debounceTime, distinctUntilChanged, finalize, startWith, take } from 'rxjs/operators';
21+
import { debounceTime, distinctUntilChanged, startWith, take } from 'rxjs/operators';
2222

2323
import { MemberCardComponent } from '../member-card/member-card.component';
2424
import { MemberFormComponent } from '../member-form/member-form.component';
@@ -54,13 +54,13 @@ export class CommitteeMembersComponent implements OnInit {
5454

5555
// Input signals
5656
public committee = input.required<Committee | null>();
57+
public members = input.required<CommitteeMember[]>();
58+
public membersLoading = input<boolean>(true);
5759

5860
public readonly refresh = output<void>();
5961

6062
// Class variables with types
6163
private dialogRef: DynamicDialogRef | undefined;
62-
public membersLoading: WritableSignal<boolean>;
63-
public members: Signal<CommitteeMember[]> = signal<CommitteeMember[]>([]);
6464
public selectedMember: WritableSignal<CommitteeMember | null>;
6565
public isDeleting: WritableSignal<boolean>;
6666
public memberActionMenuItems: MenuItem[] = [];
@@ -85,7 +85,6 @@ export class CommitteeMembersComponent implements OnInit {
8585
// Initialize all class variables
8686
this.selectedMember = signal<CommitteeMember | null>(null);
8787
this.isDeleting = signal<boolean>(false);
88-
this.membersLoading = signal<boolean>(true);
8988
// Initialize filter form
9089
this.filterForm = this.initializeFilterForm();
9190
this.searchTerm = this.initializeSearchTerm();
@@ -104,10 +103,7 @@ export class CommitteeMembersComponent implements OnInit {
104103
}
105104

106105
public ngOnInit(): void {
107-
runInInjectionContext(this.injector, () => {
108-
this.members = this.initializeMembers();
109-
this.memberActionMenuItems = this.initializeMemberActionMenuItems();
110-
});
106+
this.memberActionMenuItems = this.initializeMemberActionMenuItems();
111107
}
112108

113109
public onMemberMenuToggle(data: { event: Event; member: CommitteeMember; menu: MenuComponent }): void {
@@ -268,13 +264,6 @@ export class CommitteeMembersComponent implements OnInit {
268264
private initializeOrganizationFilter(): Signal<string | null> {
269265
return toSignal(this.filterForm.get('organization')!.valueChanges.pipe(startWith(null), distinctUntilChanged()), { initialValue: null });
270266
}
271-
private initializeMembers(): Signal<CommitteeMember[]> {
272-
const committee = this.committee();
273-
if (!committee || !committee.id) {
274-
return signal<CommitteeMember[]>([]);
275-
}
276-
return toSignal(this.committeeService.getCommitteeMembers(committee.id).pipe(finalize(() => this.membersLoading.set(false))), { initialValue: [] });
277-
}
278267

279268
private initializeMemberActionMenuItems(): MenuItem[] {
280269
return [

0 commit comments

Comments
 (0)