diff --git a/apps/lfx-one/src/app/app.routes.ts b/apps/lfx-one/src/app/app.routes.ts index f5842df4..c31a4f69 100644 --- a/apps/lfx-one/src/app/app.routes.ts +++ b/apps/lfx-one/src/app/app.routes.ts @@ -27,6 +27,10 @@ export const routes: Routes = [ path: 'groups', loadChildren: () => import('./modules/committees/committees.routes').then((m) => m.COMMITTEE_ROUTES), }, + { + path: 'settings', + loadChildren: () => import('./modules/settings/settings.routes').then((m) => m.SETTINGS_ROUTES), + }, ], }, { diff --git a/apps/lfx-one/src/app/layouts/main-layout/main-layout.component.html b/apps/lfx-one/src/app/layouts/main-layout/main-layout.component.html index 69e4caba..c95c73db 100644 --- a/apps/lfx-one/src/app/layouts/main-layout/main-layout.component.html +++ b/apps/lfx-one/src/app/layouts/main-layout/main-layout.component.html @@ -30,9 +30,11 @@

Menu

} -
- +
+
+ +
- +
diff --git a/apps/lfx-one/src/app/layouts/main-layout/main-layout.component.ts b/apps/lfx-one/src/app/layouts/main-layout/main-layout.component.ts index 9ef075a5..8504c26a 100644 --- a/apps/lfx-one/src/app/layouts/main-layout/main-layout.component.ts +++ b/apps/lfx-one/src/app/layouts/main-layout/main-layout.component.ts @@ -11,6 +11,7 @@ import { SidebarComponent } from '@components/sidebar/sidebar.component'; import { COMMITTEE_LABEL } from '@lfx-one/shared/constants'; import { SidebarMenuItem } from '@lfx-one/shared/interfaces'; import { PersonaService } from '@services/persona.service'; +import { ProjectContextService } from '@services/project-context.service'; import { filter } from 'rxjs'; @Component({ @@ -26,12 +27,14 @@ export class MainLayoutComponent { private readonly appService = inject(AppService); private readonly featureFlagService = inject(FeatureFlagService); private readonly personaService = inject(PersonaService); + private readonly projectContextService = inject(ProjectContextService); // Expose mobile sidebar state from service + protected readonly selectedProject = this.projectContextService.selectedProject; protected readonly showMobileSidebar = this.appService.showMobileSidebar; // Feature flags - private readonly showProjectsInSidebar = this.featureFlagService.getBooleanFlag('sidebar-projects', true); + private readonly showProjectsInSidebar = this.featureFlagService.getBooleanFlag('sidebar-projects', false); // Base sidebar navigation items - matching React NavigationSidebar design private readonly baseSidebarItems: SidebarMenuItem[] = [ @@ -79,7 +82,6 @@ export class MainLayoutComponent { label: 'Settings', icon: 'fa-light fa-gear', routerLink: '/settings', - disabled: true, }, { label: 'Profile', diff --git a/apps/lfx-one/src/app/layouts/project-layout/project-layout.component.html b/apps/lfx-one/src/app/layouts/project-layout/project-layout.component.html index a53dbcf9..75ebb8f0 100644 --- a/apps/lfx-one/src/app/layouts/project-layout/project-layout.component.html +++ b/apps/lfx-one/src/app/layouts/project-layout/project-layout.component.html @@ -4,93 +4,6 @@ -
-
- - - -
-
-
- -

- {{ projectTitle() }} -

- - - @if (projectDescription(); as description) { -

- {{ description }} -

- } -
- - - @if (categoryLabel(); as category) { -
- -
- } -
-
- - -
-
- @for (menu of menuItems(); track menu.label) { - - @if (menu.icon) { - - } - {{ menu.label }} - - } - @if (hasWriterAccess()) { - - } -
- @if (hasWriterAccess()) { - - } -
-
-
@if (project()) { } diff --git a/apps/lfx-one/src/app/layouts/project-layout/project-layout.component.ts b/apps/lfx-one/src/app/layouts/project-layout/project-layout.component.ts index f944b67e..b2a172dd 100644 --- a/apps/lfx-one/src/app/layouts/project-layout/project-layout.component.ts +++ b/apps/lfx-one/src/app/layouts/project-layout/project-layout.component.ts @@ -5,7 +5,6 @@ import { CommonModule } from '@angular/common'; import { Component, computed, inject, input, Signal, signal } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { ActivatedRoute, RouterModule } from '@angular/router'; -import { BreadcrumbComponent } from '@components/breadcrumb/breadcrumb.component'; import { FilterButton, Project } from '@lfx-one/shared/interfaces'; import { ProjectService } from '@services/project.service'; import { HeaderComponent } from '@shared/components/header/header.component'; @@ -16,7 +15,7 @@ import { of, switchMap } from 'rxjs'; @Component({ selector: 'lfx-project-layout', standalone: true, - imports: [CommonModule, RouterModule, HeaderComponent, BreadcrumbComponent, ChipModule], + imports: [CommonModule, RouterModule, HeaderComponent, ChipModule], templateUrl: './project-layout.component.html', styleUrl: './project-layout.component.scss', }) diff --git a/apps/lfx-one/src/app/modules/committees/committee-dashboard/committee-dashboard.component.html b/apps/lfx-one/src/app/modules/committees/committee-dashboard/committee-dashboard.component.html index 17a4e634..07bbae92 100644 --- a/apps/lfx-one/src/app/modules/committees/committee-dashboard/committee-dashboard.component.html +++ b/apps/lfx-one/src/app/modules/committees/committee-dashboard/committee-dashboard.component.html @@ -9,13 +9,11 @@

Loading {{ committeeLabel.toLowerCase() }} details...

- } - - @if (committees() && !committeesLoading()) { + } @else {
-

{{ committeeLabel }}

+

{{ committeeLabelPlural }}

@if (canCreateGroup()) {
-

Basic Information

+

Basic Information

@@ -88,7 +88,7 @@

-

Settings

+

Settings

diff --git a/apps/lfx-one/src/app/modules/committees/components/committee-table/committee-table.component.html b/apps/lfx-one/src/app/modules/committees/components/committee-table/committee-table.component.html index 6abce6ba..f31abdaf 100644 --- a/apps/lfx-one/src/app/modules/committees/components/committee-table/committee-table.component.html +++ b/apps/lfx-one/src/app/modules/committees/components/committee-table/committee-table.component.html @@ -25,7 +25,7 @@ {{ committee.category || 'Other' }}
-

+

{{ committee.name }}

@if (!committee.public) { diff --git a/apps/lfx-one/src/app/modules/meetings/meetings-dashboard/meetings-dashboard.component.ts b/apps/lfx-one/src/app/modules/meetings/meetings-dashboard/meetings-dashboard.component.ts index 9bd68150..a2bc1bb7 100644 --- a/apps/lfx-one/src/app/modules/meetings/meetings-dashboard/meetings-dashboard.component.ts +++ b/apps/lfx-one/src/app/modules/meetings/meetings-dashboard/meetings-dashboard.component.ts @@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common'; import { Component, computed, inject, signal, Signal, WritableSignal } from '@angular/core'; -import { toSignal } from '@angular/core/rxjs-interop'; +import { toObservable, toSignal } from '@angular/core/rxjs-interop'; import { MeetingCardComponent } from '@app/shared/components/meeting-card/meeting-card.component'; import { ProjectContextService } from '@app/shared/services/project-context.service'; import { ButtonComponent } from '@components/button/button.component'; @@ -11,7 +11,7 @@ import { Meeting, ProjectContext } from '@lfx-one/shared/interfaces'; import { getCurrentOrNextOccurrence } from '@lfx-one/shared/utils'; import { MeetingService } from '@services/meeting.service'; import { PersonaService } from '@services/persona.service'; -import { BehaviorSubject, map, switchMap, tap } from 'rxjs'; +import { BehaviorSubject, catchError, map, merge, of, switchMap, tap } from 'rxjs'; import { MeetingsTopBarComponent } from './components/meetings-top-bar/meetings-top-bar.component'; @@ -40,18 +40,24 @@ export class MeetingsDashboardComponent { public isNonFoundationProjectSelected: Signal; public constructor() { + // Initialize project context first (needed for reactive data loading) + this.project = computed(() => this.projectContextService.selectedProject() || this.projectContextService.selectedFoundation()); + + // Initialize permission checks + this.isMaintainer = computed(() => this.personaService.currentPersona() === 'maintainer'); + this.isNonFoundationProjectSelected = computed(() => this.projectContextService.selectedProject() !== null); + + // Initialize state this.meetingsLoading = signal(true); this.refresh$ = new BehaviorSubject(undefined); - this.meetings = this.initializeMeetings(); this.currentView = signal<'list' | 'calendar'>('list'); this.searchQuery = signal(''); this.timeFilter = signal<'upcoming' | 'past'>('upcoming'); this.topBarVisibilityFilter = signal<'mine' | 'public'>('mine'); + + // Initialize data with reactive pattern + this.meetings = this.initializeMeetings(); this.filteredMeetings = this.initializeFilteredMeetings(); - this.project = computed(() => this.projectContextService.selectedProject() || this.projectContextService.selectedFoundation()); - this.isMaintainer = computed(() => this.personaService.currentPersona() === 'maintainer'); - // A non-foundation project is selected if selectedProject is not null - this.isNonFoundationProjectSelected = computed(() => this.projectContextService.selectedProject() !== null); } public onViewChange(view: 'list' | 'calendar'): void { @@ -64,10 +70,23 @@ export class MeetingsDashboardComponent { } private initializeMeetings(): Signal { + // Convert project signal to observable to react to project changes + const project$ = toObservable(this.project); + return toSignal( - this.refresh$.pipe( - switchMap(() => - this.meetingService.getMeetings().pipe( + merge( + project$, // Triggers on project context changes + this.refresh$ // Triggers on manual refresh + ).pipe( + tap(() => this.meetingsLoading.set(true)), + switchMap(() => { + const project = this.project(); + if (!project?.projectId) { + this.meetingsLoading.set(false); + return of([]); + } + + return this.meetingService.getMeetings().pipe( map((meetings) => { // Sort meetings by current or next occurrence start time (earliest first) return meetings.sort((a, b) => { @@ -87,13 +106,16 @@ export class MeetingsDashboardComponent { return new Date(occurrenceA.start_time).getTime() - new Date(occurrenceB.start_time).getTime(); }); }), + catchError((error) => { + console.error('Failed to load meetings:', error); + this.meetingsLoading.set(false); + return of([]); + }), tap(() => this.meetingsLoading.set(false)) - ) - ) + ); + }) ), - { - initialValue: [], - } + { initialValue: [] } ); } diff --git a/apps/lfx-one/src/app/modules/project/committees/committee-view/committee-view.component.html b/apps/lfx-one/src/app/modules/project/committees/committee-view/committee-view.component.html index b298d08a..cd149e7b 100644 --- a/apps/lfx-one/src/app/modules/project/committees/committee-view/committee-view.component.html +++ b/apps/lfx-one/src/app/modules/project/committees/committee-view/committee-view.component.html @@ -7,7 +7,7 @@
-

Loading committee details...

+

Loading group details...

} @@ -17,9 +17,9 @@
-

Committee Not Found

-

The committee you're looking for doesn't exist or has been removed.

- +

Group Not Found

+

The group you're looking for doesn't exist or has been removed.

+
} @@ -33,7 +33,7 @@

Committee Not Found

{{ committee()?.name }}

diff --git a/apps/lfx-one/src/app/modules/project/committees/committee-view/committee-view.component.ts b/apps/lfx-one/src/app/modules/project/committees/committee-view/committee-view.component.ts index a3b57e2f..74f3aae9 100644 --- a/apps/lfx-one/src/app/modules/project/committees/committee-view/committee-view.component.ts +++ b/apps/lfx-one/src/app/modules/project/committees/committee-view/committee-view.component.ts @@ -75,10 +75,7 @@ export class CommitteeViewComponent { } public goBack(): void { - const project = this.project(); - if (project) { - this.router.navigate(['/project', project.slug, 'committees']); - } + this.router.navigate(['/', 'groups']); } public toggleActionMenu(event: Event, menuComponent: MenuComponent): void { @@ -155,7 +152,7 @@ export class CommitteeViewComponent { summary: 'Error', detail: 'Failed to load committee', }); - this.router.navigate(['/project', this.project()!.slug, 'committees']); + this.router.navigate(['/', 'groups']); return throwError(() => new Error('Failed to load committee')); }) ); diff --git a/apps/lfx-one/src/app/modules/project/meetings/components/meeting-manage/meeting-manage.component.ts b/apps/lfx-one/src/app/modules/project/meetings/components/meeting-manage/meeting-manage.component.ts index 92630927..22b4655e 100644 --- a/apps/lfx-one/src/app/modules/project/meetings/components/meeting-manage/meeting-manage.component.ts +++ b/apps/lfx-one/src/app/modules/project/meetings/components/meeting-manage/meeting-manage.component.ts @@ -180,10 +180,7 @@ export class MeetingManageComponent { } public onCancel(): void { - const project = this.projectService.project(); - if (project?.slug) { - this.router.navigate(['/project', project.slug, 'meetings']); - } + this.router.navigate(['/', 'meetings']); } public onSubmit(): void { @@ -215,7 +212,7 @@ export class MeetingManageComponent { : this.meetingService.createMeeting(meetingData as CreateMeetingRequest); operation.subscribe({ - next: (meeting) => this.handleMeetingSuccess(meeting, project), + next: (meeting) => this.handleMeetingSuccess(meeting), error: (error) => this.handleMeetingError(error), }); } @@ -268,7 +265,7 @@ export class MeetingManageComponent { this.showRegistrantOperationToast(totalSuccess, totalFailed, totalOperations); if (!this.isEditMode()) { - this.router.navigate(['/project', this.projectService.project()?.slug, 'meetings']); + this.router.navigate(['/', 'meetings']); } else { this.registrantUpdatesRefresh$.next(); // Reset registrant updates only if there were some successes @@ -347,7 +344,7 @@ export class MeetingManageComponent { }; } - private handleMeetingSuccess(meeting: Meeting, project: any): void { + private handleMeetingSuccess(meeting: Meeting): void { this.meetingId.set(meeting.uid); // If we're in create mode and not on the last step, continue to next step @@ -387,7 +384,7 @@ export class MeetingManageComponent { .subscribe({ next: (result) => { // Process attachment operations after meeting save - this.handleAttachmentOperationsResults(result, project); + this.handleAttachmentOperationsResults(result); }, error: (attachmentError: any) => { console.error('Error processing attachments:', attachmentError); @@ -399,7 +396,7 @@ export class MeetingManageComponent { summary: this.isEditMode() ? 'Meeting Updated' : 'Meeting Created', detail: warningMessage, }); - this.router.navigate(['/project', project.slug, 'meetings']); + this.router.navigate(['/meetings']); }, }); } else { @@ -408,7 +405,7 @@ export class MeetingManageComponent { summary: 'Success', detail: `Meeting ${this.isEditMode() ? 'updated' : 'created'} successfully`, }); - this.router.navigate(['/project', project.slug, 'meetings']); + this.router.navigate(['/meetings']); } } @@ -422,14 +419,11 @@ export class MeetingManageComponent { this.submitting.set(false); } - private handleAttachmentOperationsResults( - result: { - deletions: { successes: number; failures: string[] }; - uploads: { successes: MeetingAttachment[]; failures: { fileName: string; error: any }[] }; - links: { successes: MeetingAttachment[]; failures: { linkName: string; error: any }[] }; - }, - project: any - ): void { + private handleAttachmentOperationsResults(result: { + deletions: { successes: number; failures: string[] }; + uploads: { successes: MeetingAttachment[]; failures: { fileName: string; error: any }[] }; + links: { successes: MeetingAttachment[]; failures: { linkName: string; error: any }[] }; + }): void { const totalDeleteSuccesses = result.deletions.successes; const totalDeleteFailures = result.deletions.failures.length; const totalUploadSuccesses = result.uploads.successes.length; @@ -512,7 +506,7 @@ export class MeetingManageComponent { this.pendingAttachmentDeletions.set([]); } - this.router.navigate(['/project', project.slug, 'meetings']); + this.router.navigate(['/meetings']); } private initializeMeeting() { diff --git a/apps/lfx-one/src/app/modules/project/project.routes.ts b/apps/lfx-one/src/app/modules/project/project.routes.ts index 04c7eaf8..641734ff 100644 --- a/apps/lfx-one/src/app/modules/project/project.routes.ts +++ b/apps/lfx-one/src/app/modules/project/project.routes.ts @@ -3,8 +3,6 @@ import { Routes } from '@angular/router'; -import { writerGuard } from '../../shared/guards/writer.guard'; - export const PROJECT_ROUTES: Routes = [ { path: '', @@ -20,15 +18,4 @@ export const PROJECT_ROUTES: Routes = [ loadChildren: () => import('./committees/committees.routes').then((m) => m.COMMITTEES_ROUTES), data: { preload: true, preloadDelay: 1500 }, // Medium usage, moderate delay }, - { - path: 'mailing-lists', - loadChildren: () => import('./mailing-lists/mailing-lists.routes').then((m) => m.MAILING_LISTS_ROUTES), - data: { preload: true, preloadDelay: 3000 }, // Lower priority, longer delay - }, - { - path: 'settings', - loadComponent: () => import('./settings/settings-dashboard/settings-dashboard.component').then((m) => m.SettingsDashboardComponent), - canActivate: [writerGuard], - data: { preload: false }, // Settings accessed less frequently, don't preload - }, ]; diff --git a/apps/lfx-one/src/app/modules/project/settings/components/permissions-matrix/permissions-matrix.component.html b/apps/lfx-one/src/app/modules/settings/components/permissions-matrix/permissions-matrix.component.html similarity index 100% rename from apps/lfx-one/src/app/modules/project/settings/components/permissions-matrix/permissions-matrix.component.html rename to apps/lfx-one/src/app/modules/settings/components/permissions-matrix/permissions-matrix.component.html diff --git a/apps/lfx-one/src/app/modules/project/settings/components/permissions-matrix/permissions-matrix.component.ts b/apps/lfx-one/src/app/modules/settings/components/permissions-matrix/permissions-matrix.component.ts similarity index 100% rename from apps/lfx-one/src/app/modules/project/settings/components/permissions-matrix/permissions-matrix.component.ts rename to apps/lfx-one/src/app/modules/settings/components/permissions-matrix/permissions-matrix.component.ts diff --git a/apps/lfx-one/src/app/modules/project/settings/components/user-form/user-form.component.html b/apps/lfx-one/src/app/modules/settings/components/user-form/user-form.component.html similarity index 100% rename from apps/lfx-one/src/app/modules/project/settings/components/user-form/user-form.component.html rename to apps/lfx-one/src/app/modules/settings/components/user-form/user-form.component.html diff --git a/apps/lfx-one/src/app/modules/project/settings/components/user-form/user-form.component.ts b/apps/lfx-one/src/app/modules/settings/components/user-form/user-form.component.ts similarity index 94% rename from apps/lfx-one/src/app/modules/project/settings/components/user-form/user-form.component.ts rename to apps/lfx-one/src/app/modules/settings/components/user-form/user-form.component.ts index a9813819..a74a854b 100644 --- a/apps/lfx-one/src/app/modules/project/settings/components/user-form/user-form.component.ts +++ b/apps/lfx-one/src/app/modules/settings/components/user-form/user-form.component.ts @@ -4,12 +4,12 @@ import { HttpErrorResponse } from '@angular/common/http'; import { Component, computed, inject, signal } from '@angular/core'; import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { ProjectContextService } from '@app/shared/services/project-context.service'; import { ButtonComponent } from '@components/button/button.component'; import { InputTextComponent } from '@components/input-text/input-text.component'; import { RadioButtonComponent } from '@components/radio-button/radio-button.component'; import { AddUserToProjectRequest, ProjectPermissionUser, UpdateUserRoleRequest } from '@lfx-one/shared'; import { PermissionsService } from '@services/permissions.service'; -import { ProjectService } from '@services/project.service'; import { ConfirmationService, MessageService } from 'primeng/api'; import { ConfirmDialogModule } from 'primeng/confirmdialog'; import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; @@ -25,7 +25,7 @@ import { take } from 'rxjs'; export class UserFormComponent { private readonly config = inject(DynamicDialogConfig); private readonly dialogRef = inject(DynamicDialogRef); - private readonly projectService = inject(ProjectService); + private readonly projectContextService = inject(ProjectContextService); private readonly messageService = inject(MessageService); private readonly permissionsService = inject(PermissionsService); private readonly confirmationService = inject(ConfirmationService); @@ -41,7 +41,7 @@ export class UserFormComponent { public isEditing = computed(() => this.config.data?.isEditing || false); public user = computed(() => (this.config.data?.user as ProjectPermissionUser) || null); - public project = this.projectService.project; + public project = computed(() => this.projectContextService.selectedProject() || this.projectContextService.selectedFoundation()); // Permission options - simplified to only View/Manage public permissionLevelOptions = [ @@ -67,7 +67,7 @@ export class UserFormComponent { return; } - const project = this.projectService.project(); + const project = this.project(); if (!project) { this.messageService.add({ severity: 'error', @@ -83,7 +83,7 @@ export class UserFormComponent { // For editing, update role only if (this.isEditing()) { this.permissionsService - .updateUserRole(project.uid, this.user()!.username, { + .updateUserRole(project.projectId, this.user()!.username, { role: formValue.role, } as UpdateUserRoleRequest) .pipe(take(1)) @@ -113,7 +113,7 @@ export class UserFormComponent { if (!this.showManualFields()) { // Try to add user with just email (backend will resolve to username) this.permissionsService - .addUserToProject(project.uid, { + .addUserToProject(project.projectId, { username: formValue.email, // Pass email as username, backend will resolve role: formValue.role, } as AddUserToProjectRequest) @@ -196,7 +196,7 @@ export class UserFormComponent { } private addUserWithManualData(formValue: any): void { - const project = this.projectService.project(); + const project = this.project(); if (!project) { this.messageService.add({ severity: 'error', @@ -223,7 +223,7 @@ export class UserFormComponent { // Since the user doesn't exist in the system, we need to send the full user data // The backend should handle this case by accepting UserInfo objects this.permissionsService - .addUserToProject(project.uid, userData as any) + .addUserToProject(project.projectId, userData as any) .pipe(take(1)) .subscribe({ next: () => { diff --git a/apps/lfx-one/src/app/modules/project/settings/components/user-permissions-table/user-permissions-table.component.html b/apps/lfx-one/src/app/modules/settings/components/user-permissions-table/user-permissions-table.component.html similarity index 100% rename from apps/lfx-one/src/app/modules/project/settings/components/user-permissions-table/user-permissions-table.component.html rename to apps/lfx-one/src/app/modules/settings/components/user-permissions-table/user-permissions-table.component.html diff --git a/apps/lfx-one/src/app/modules/project/settings/components/user-permissions-table/user-permissions-table.component.ts b/apps/lfx-one/src/app/modules/settings/components/user-permissions-table/user-permissions-table.component.ts similarity index 92% rename from apps/lfx-one/src/app/modules/project/settings/components/user-permissions-table/user-permissions-table.component.ts rename to apps/lfx-one/src/app/modules/settings/components/user-permissions-table/user-permissions-table.component.ts index f2154d0d..dcddc7c2 100644 --- a/apps/lfx-one/src/app/modules/project/settings/components/user-permissions-table/user-permissions-table.component.ts +++ b/apps/lfx-one/src/app/modules/settings/components/user-permissions-table/user-permissions-table.component.ts @@ -2,14 +2,14 @@ // SPDX-License-Identifier: MIT import { CommonModule } from '@angular/common'; -import { Component, inject, input, output, signal, WritableSignal } from '@angular/core'; +import { Component, computed, inject, input, output, signal, WritableSignal } from '@angular/core'; +import { ProjectContextService } from '@app/shared/services/project-context.service'; import { ButtonComponent } from '@components/button/button.component'; import { CardComponent } from '@components/card/card.component'; import { MenuComponent } from '@components/menu/menu.component'; import { TableComponent } from '@components/table/table.component'; import { ProjectPermissionUser } from '@lfx-one/shared/interfaces'; import { PermissionsService } from '@services/permissions.service'; -import { ProjectService } from '@services/project.service'; import { ConfirmationService, MenuItem, MessageService } from 'primeng/api'; import { ConfirmDialogModule } from 'primeng/confirmdialog'; import { DialogService } from 'primeng/dynamicdialog'; @@ -26,15 +26,15 @@ import { UserFormComponent } from '../user-form/user-form.component'; }) export class UserPermissionsTableComponent { private readonly permissionsService = inject(PermissionsService); - private readonly projectService = inject(ProjectService); private readonly confirmationService = inject(ConfirmationService); private readonly messageService = inject(MessageService); private readonly dialogService = inject(DialogService); + private readonly projectContextService = inject(ProjectContextService); // State signals public users = input.required(); public loading = input(); - public project = this.projectService.project; + public project = computed(() => this.projectContextService.selectedProject() || this.projectContextService.selectedFoundation()); public isRemoving: WritableSignal = signal(null); public selectedUser: WritableSignal = signal(null); public userActionMenuItems: MenuItem[] = this.initializeUserActionMenuItems(); @@ -108,7 +108,7 @@ export class UserPermissionsTableComponent { this.isRemoving.set(user.username); this.permissionsService - .removeUserFromProject(this.project()!.uid, user.username) + .removeUserFromProject(this.project()!.projectId, user.username) .pipe(take(1)) .subscribe({ next: () => { diff --git a/apps/lfx-one/src/app/modules/project/settings/settings-dashboard/settings-dashboard.component.html b/apps/lfx-one/src/app/modules/settings/settings-dashboard/settings-dashboard.component.html similarity index 100% rename from apps/lfx-one/src/app/modules/project/settings/settings-dashboard/settings-dashboard.component.html rename to apps/lfx-one/src/app/modules/settings/settings-dashboard/settings-dashboard.component.html diff --git a/apps/lfx-one/src/app/modules/project/settings/settings-dashboard/settings-dashboard.component.ts b/apps/lfx-one/src/app/modules/settings/settings-dashboard/settings-dashboard.component.ts similarity index 52% rename from apps/lfx-one/src/app/modules/project/settings/settings-dashboard/settings-dashboard.component.ts rename to apps/lfx-one/src/app/modules/settings/settings-dashboard/settings-dashboard.component.ts index 8fc8a76c..63eb1da6 100644 --- a/apps/lfx-one/src/app/modules/project/settings/settings-dashboard/settings-dashboard.component.ts +++ b/apps/lfx-one/src/app/modules/settings/settings-dashboard/settings-dashboard.component.ts @@ -1,16 +1,16 @@ // Copyright The Linux Foundation and each contributor to LFX. // SPDX-License-Identifier: MIT -import { Component, inject, signal, Signal, WritableSignal } from '@angular/core'; -import { toSignal } from '@angular/core/rxjs-interop'; +import { Component, computed, inject, signal, Signal, WritableSignal } from '@angular/core'; +import { toObservable, toSignal } from '@angular/core/rxjs-interop'; +import { ProjectContextService } from '@app/shared/services/project-context.service'; import { CardComponent } from '@components/card/card.component'; import { MenuComponent } from '@components/menu/menu.component'; import { ProjectPermissionUser } from '@lfx-one/shared'; import { PermissionsService } from '@services/permissions.service'; -import { ProjectService } from '@services/project.service'; import { MenuItem } from 'primeng/api'; import { DialogService } from 'primeng/dynamicdialog'; -import { BehaviorSubject, of, switchMap, take, tap } from 'rxjs'; +import { BehaviorSubject, catchError, merge, of, switchMap, take, tap } from 'rxjs'; import { PermissionsMatrixComponent } from '../components/permissions-matrix/permissions-matrix.component'; import { UserFormComponent } from '../components/user-form/user-form.component'; @@ -22,14 +22,15 @@ import { UserPermissionsTableComponent } from '../components/user-permissions-ta templateUrl: './settings-dashboard.component.html', }) export class SettingsDashboardComponent { - private readonly projectService = inject(ProjectService); - private readonly dialogService = inject(DialogService); + private readonly projectContextService = inject(ProjectContextService); private readonly permissionsService = inject(PermissionsService); + private readonly dialogService = inject(DialogService); public users: Signal; public loading: WritableSignal = signal(true); - public refresh: BehaviorSubject = new BehaviorSubject(undefined); - public project = this.projectService.project; + public refresh$: BehaviorSubject = new BehaviorSubject(undefined); + public project = computed(() => this.projectContextService.selectedProject() || this.projectContextService.selectedFoundation()); + protected readonly menuItems: MenuItem[] = [ { label: 'Add User', @@ -39,22 +40,43 @@ export class SettingsDashboardComponent { ]; public constructor() { - // Initialize userPermissions signal from service - this.users = toSignal( - this.project()?.uid - ? this.refresh.pipe( - tap(() => this.loading.set(true)), - switchMap(() => this.permissionsService.getProjectPermissions(this.project()?.uid as string).pipe(tap(() => this.loading.set(false)))) - ) - : of([]), - { - initialValue: [], - } - ); + // Initialize users signal with reactive project context + this.users = this.initializeUsers(); } public refreshUsers(): void { - this.refresh.next(); + this.refresh$.next(); + } + + private initializeUsers(): Signal { + // Convert project signal to observable to react to project changes + const project$ = toObservable(this.project); + + return toSignal( + merge( + project$, // Triggers on project context changes + this.refresh$ // Triggers on manual refresh + ).pipe( + tap(() => this.loading.set(true)), + switchMap(() => { + const project = this.project(); + if (!project?.projectId) { + this.loading.set(false); + return of([]); + } + + return this.permissionsService.getProjectPermissions(project.projectId).pipe( + catchError((error) => { + console.error('Failed to load permissions:', error); + this.loading.set(false); + return of([]); + }), + tap(() => this.loading.set(false)) + ); + }) + ), + { initialValue: [] } + ); } private onAddUser(): void { diff --git a/apps/lfx-one/src/app/modules/settings/settings.routes.ts b/apps/lfx-one/src/app/modules/settings/settings.routes.ts new file mode 100644 index 00000000..b4a7a986 --- /dev/null +++ b/apps/lfx-one/src/app/modules/settings/settings.routes.ts @@ -0,0 +1,15 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +import { Routes } from '@angular/router'; + +import { authGuard } from '../../shared/guards/auth.guard'; + +export const SETTINGS_ROUTES: Routes = [ + { + path: '', + loadComponent: () => import('./settings-dashboard/settings-dashboard.component').then((m) => m.SettingsDashboardComponent), + canActivate: [authGuard], + data: { preload: false }, // Settings accessed less frequently, don't preload + }, +]; diff --git a/apps/lfx-one/src/server/services/committee.service.ts b/apps/lfx-one/src/server/services/committee.service.ts index b8289d7d..179028c6 100644 --- a/apps/lfx-one/src/server/services/committee.service.ts +++ b/apps/lfx-one/src/server/services/committee.service.ts @@ -44,7 +44,18 @@ export class CommitteeService { const { resources } = await this.microserviceProxy.proxyRequest>(req, 'LFX_V2_SERVICE', '/query/resources', 'GET', params); - const committees = resources.map((resource) => resource.data); + let committees = resources.map((resource) => resource.data); + + // Get member count for each committee + committees = await Promise.all( + committees.map(async (committee) => { + const memberCount = await this.getCommitteeMembersCount(req, committee.uid); + return { + ...committee, + total_members: memberCount, + }; + }) + ); // Add writer access field to all committees return await this.accessCheckService.addAccessToResources(req, committees, 'committee'); @@ -222,6 +233,30 @@ export class CommitteeService { return resources.map((resource) => resource.data); } + /** + * Fetches count of all members for a specific committee + */ + public async getCommitteeMembersCount(req: Request, committeeId: string, query: Record = {}): Promise { + req.log.info( + { + operation: 'get_committee_members_count', + committee_id: committeeId, + query: query, + }, + 'Fetching committee members count' + ); + + const params = { + ...query, + type: 'committee_member', + tags: `committee_uid:${committeeId}`, + }; + + const { count } = await this.microserviceProxy.proxyRequest(req, 'LFX_V2_SERVICE', `/query/resources/count`, 'GET', params); + + return count; + } + /** * Fetches a single committee member by ID */ diff --git a/apps/lfx-one/src/styles.scss b/apps/lfx-one/src/styles.scss index d00cbc0d..63297a8d 100644 --- a/apps/lfx-one/src/styles.scss +++ b/apps/lfx-one/src/styles.scss @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT */ /* Google Fonts - Must be at the very top */ -@import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&family=Roboto+Slab:wght@100..900&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&family=Open+Sans:ital,wght@0,300..800;1,300..800&family=Roboto+Slab:wght@100..900&display=swap'); @layer tailwind-base, primeng, tailwind-utilities; @@ -19,6 +19,9 @@ /* Open Sans as the primary sans-serif font */ --font-sans: 'Open Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; + /* Inter as additional sans-serif font */ + --font-inter: 'Inter', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; + /* Roboto Slab for headings and display text */ --font-display: 'Roboto Slab', ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif; diff --git a/apps/lfx-one/tailwind.config.js b/apps/lfx-one/tailwind.config.js index 2eb13dec..9f85bd82 100644 --- a/apps/lfx-one/tailwind.config.js +++ b/apps/lfx-one/tailwind.config.js @@ -66,6 +66,7 @@ export default { fontSize: lfxFontSizes, fontFamily: { sans: ['Open Sans', 'sans-serif'], + inter: ['Inter', 'sans-serif'], display: ['Roboto Slab', 'serif'], serif: ['Roboto Slab', 'serif'], }, diff --git a/packages/shared/src/constants/committees.constants.ts b/packages/shared/src/constants/committees.constants.ts index 79ddaad9..6558d8f3 100644 --- a/packages/shared/src/constants/committees.constants.ts +++ b/packages/shared/src/constants/committees.constants.ts @@ -160,7 +160,7 @@ export function getCommitteeTypeColor(category: string | undefined): string { if (lowerCategory.includes('technical oversight')) return 'bg-teal-100 text-teal-800'; if (lowerCategory.includes('marketing oversight')) return 'bg-pink-100 text-pink-800'; if (lowerCategory.includes('marketing committee')) return 'bg-pink-100 text-pink-800'; - if (lowerCategory.includes('finance')) return 'bg-emerald-100 text-emerald-800'; + if (lowerCategory.includes('finance')) return 'bg-amber-100 text-amber-800'; // Fallback to exact match or default return COMMITTEE_TYPE_COLORS[category as keyof typeof COMMITTEE_TYPE_COLORS] || DEFAULT_COMMITTEE_TYPE_COLOR;