- {{ projectTitle() }} -
- - - @if (projectDescription(); as description) { -- {{ description }} -
- } -Loading {{ committeeLabel.toLowerCase() }} details...
- } - - @if (committees() && !committeesLoading()) { + } @else {{{ committeeLabel }}
+{{ committeeLabelPlural }}
@if (canCreateGroup()) {Basic Information
+Basic Information
- 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;
+
{{ 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: SignalLoading committee details...
+Loading group details...
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.
+