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 eb012b6b..1e045467 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 @@ -4,7 +4,7 @@
@@ -23,7 +23,7 @@

Menu

- +
diff --git a/apps/lfx-one/src/app/modules/dashboards/board-member/board-member-dashboard.component.html b/apps/lfx-one/src/app/modules/dashboards/board-member/board-member-dashboard.component.html index 5aa1f871..24510326 100644 --- a/apps/lfx-one/src/app/modules/dashboards/board-member/board-member-dashboard.component.html +++ b/apps/lfx-one/src/app/modules/dashboards/board-member/board-member-dashboard.component.html @@ -2,6 +2,11 @@
+ +
+

{{ selectedFoundation()?.name }} Overview

+
+
diff --git a/apps/lfx-one/src/app/modules/dashboards/board-member/board-member-dashboard.component.ts b/apps/lfx-one/src/app/modules/dashboards/board-member/board-member-dashboard.component.ts index 3d71f9fb..c3ad48e0 100644 --- a/apps/lfx-one/src/app/modules/dashboards/board-member/board-member-dashboard.component.ts +++ b/apps/lfx-one/src/app/modules/dashboards/board-member/board-member-dashboard.component.ts @@ -8,6 +8,7 @@ import { Account } from '@lfx-one/shared/interfaces'; import { SelectComponent } from '../../../shared/components/select/select.component'; import { AccountContextService } from '../../../shared/services/account-context.service'; +import { ProjectContextService } from '../../../shared/services/project-context.service'; import { FoundationHealthComponent } from '../components/foundation-health/foundation-health.component'; import { MyMeetingsComponent } from '../components/my-meetings/my-meetings.component'; import { OrganizationInvolvementComponent } from '../components/organization-involvement/organization-involvement.component'; @@ -21,12 +22,13 @@ import { PendingActionsComponent } from '../components/pending-actions/pending-a }) export class BoardMemberDashboardComponent { private readonly accountContextService = inject(AccountContextService); - + private readonly projectContextService = inject(ProjectContextService); public readonly form = new FormGroup({ selectedAccountId: new FormControl(this.accountContextService.selectedAccount().accountId), }); public readonly availableAccounts: Signal = computed(() => this.accountContextService.availableAccounts); + public readonly selectedFoundation = computed(() => this.projectContextService.selectedFoundation()); public constructor() { this.form diff --git a/apps/lfx-one/src/app/modules/dashboards/components/organization-involvement/organization-involvement.component.html b/apps/lfx-one/src/app/modules/dashboards/components/organization-involvement/organization-involvement.component.html index 5eb5ab4d..ad48e1b7 100644 --- a/apps/lfx-one/src/app/modules/dashboards/components/organization-involvement/organization-involvement.component.html +++ b/apps/lfx-one/src/app/modules/dashboards/components/organization-involvement/organization-involvement.component.html @@ -31,7 +31,16 @@

{{ accountName() }}'s

- @if (isLoading()) { + @if (!hasFoundationSelected()) { + +
+
+ +

Please select a foundation project to view organization data

+

Use the project selector in the sidebar to choose a foundation

+
+
+ } @else if (isLoading()) {
diff --git a/apps/lfx-one/src/app/modules/dashboards/components/organization-involvement/organization-involvement.component.ts b/apps/lfx-one/src/app/modules/dashboards/components/organization-involvement/organization-involvement.component.ts index 20249ae9..fe120bf5 100644 --- a/apps/lfx-one/src/app/modules/dashboards/components/organization-involvement/organization-involvement.component.ts +++ b/apps/lfx-one/src/app/modules/dashboards/components/organization-involvement/organization-involvement.component.ts @@ -6,13 +6,14 @@ import { Component, computed, ElementRef, inject, signal, ViewChild } from '@ang import { toObservable, toSignal } from '@angular/core/rxjs-interop'; import { AccountContextService } from '@app/shared/services/account-context.service'; import { AnalyticsService } from '@app/shared/services/analytics.service'; +import { ProjectContextService } from '@app/shared/services/project-context.service'; import { ChartComponent } from '@components/chart/chart.component'; import { FilterOption, FilterPillsComponent } from '@components/filter-pills/filter-pills.component'; import { BAR_CHART_OPTIONS, PRIMARY_INVOLVEMENT_METRICS, SPARKLINE_CHART_OPTIONS } from '@lfx-one/shared/constants'; import { OrganizationInvolvementMetricWithChart, PrimaryInvolvementMetric } from '@lfx-one/shared/interfaces'; import { hexToRgba } from '@lfx-one/shared/utils'; import { TooltipModule } from 'primeng/tooltip'; -import { finalize, map, switchMap } from 'rxjs'; +import { combineLatest, finalize, map, of, switchMap } from 'rxjs'; @Component({ selector: 'lfx-organization-involvement', @@ -26,11 +27,14 @@ export class OrganizationInvolvementComponent { private readonly analyticsService = inject(AnalyticsService); private readonly accountContextService = inject(AccountContextService); + private readonly projectContextService = inject(ProjectContextService); private readonly contributionsLoading = signal(true); private readonly dashboardLoading = signal(true); private readonly eventsLoading = signal(true); private readonly selectedAccountId$ = toObservable(this.accountContextService.selectedAccount).pipe(map((account) => account.accountId)); + private readonly selectedFoundationSlug$ = toObservable(this.projectContextService.selectedFoundation).pipe(map((foundation) => foundation?.slug || '')); + public readonly hasFoundationSelected = computed(() => !!this.projectContextService.selectedFoundation()); private readonly contributionsOverviewData = this.initializeContributionsOverviewData(); private readonly boardMemberDashboardData = this.initializeBoardMemberDashboardData(); private readonly eventsOverviewData = this.initializeEventsOverviewData(); @@ -148,10 +152,30 @@ export class OrganizationInvolvementComponent { private initializeBoardMemberDashboardData() { return toSignal( - this.selectedAccountId$.pipe( - switchMap((accountId) => { + combineLatest([this.selectedAccountId$, this.selectedFoundationSlug$]).pipe( + switchMap(([accountId, foundationSlug]) => { this.dashboardLoading.set(true); - return this.analyticsService.getBoardMemberDashboard(accountId).pipe(finalize(() => this.dashboardLoading.set(false))); + + // Return empty data if no foundation is selected + if (!foundationSlug) { + this.dashboardLoading.set(false); + return of({ + membershipTier: { + tier: '', + membershipStartDate: '', + membershipEndDate: '', + membershipStatus: '', + }, + certifiedEmployees: { + certifications: 0, + certifiedEmployees: 0, + }, + accountId: '', + projectId: '', + }); + } + + return this.analyticsService.getBoardMemberDashboard(accountId, foundationSlug).pipe(finalize(() => this.dashboardLoading.set(false))); }) ), { @@ -166,12 +190,6 @@ export class OrganizationInvolvementComponent { certifications: 0, certifiedEmployees: 0, }, - boardMeetingAttendance: { - totalMeetings: 0, - attendedMeetings: 0, - notAttendedMeetings: 0, - attendancePercentage: 0, - }, accountId: '', projectId: '', }, @@ -196,7 +214,6 @@ export class OrganizationInvolvementComponent { accountName: '', }, accountId: '', - projectId: '', }, } ); diff --git a/apps/lfx-one/src/app/modules/dashboards/core-developer/core-developer-dashboard.component.html b/apps/lfx-one/src/app/modules/dashboards/core-developer/core-developer-dashboard.component.html index 61900393..9cdbd5e5 100644 --- a/apps/lfx-one/src/app/modules/dashboards/core-developer/core-developer-dashboard.component.html +++ b/apps/lfx-one/src/app/modules/dashboards/core-developer/core-developer-dashboard.component.html @@ -2,6 +2,11 @@
+ +
+

{{ selectedFoundation()?.name }} Overview

+
+
diff --git a/apps/lfx-one/src/app/modules/dashboards/core-developer/core-developer-dashboard.component.ts b/apps/lfx-one/src/app/modules/dashboards/core-developer/core-developer-dashboard.component.ts index 15a1e590..11f8e497 100644 --- a/apps/lfx-one/src/app/modules/dashboards/core-developer/core-developer-dashboard.component.ts +++ b/apps/lfx-one/src/app/modules/dashboards/core-developer/core-developer-dashboard.component.ts @@ -1,7 +1,9 @@ // Copyright The Linux Foundation and each contributor to LFX. // SPDX-License-Identifier: MIT -import { Component } from '@angular/core'; +import { Component, computed, inject } from '@angular/core'; +import { ProjectContextService } from '@app/shared/services/project-context.service'; + import { MyMeetingsComponent } from '../components/my-meetings/my-meetings.component'; import { MyProjectsComponent } from '../components/my-projects/my-projects.component'; import { PendingActionsComponent } from '../components/pending-actions/pending-actions.component'; @@ -14,4 +16,8 @@ import { RecentProgressComponent } from '../components/recent-progress/recent-pr templateUrl: './core-developer-dashboard.component.html', styleUrl: './core-developer-dashboard.component.scss', }) -export class CoreDeveloperDashboardComponent {} +export class CoreDeveloperDashboardComponent { + private readonly projectContextService = inject(ProjectContextService); + + public readonly selectedFoundation = computed(() => this.projectContextService.selectedFoundation()); +} diff --git a/apps/lfx-one/src/app/modules/dashboards/maintainer/maintainer-dashboard.component.html b/apps/lfx-one/src/app/modules/dashboards/maintainer/maintainer-dashboard.component.html index d9e4b9b0..e909d941 100644 --- a/apps/lfx-one/src/app/modules/dashboards/maintainer/maintainer-dashboard.component.html +++ b/apps/lfx-one/src/app/modules/dashboards/maintainer/maintainer-dashboard.component.html @@ -2,6 +2,11 @@
+ +
+

{{ selectedFoundation()?.name }} Overview

+
+
diff --git a/apps/lfx-one/src/app/modules/dashboards/maintainer/maintainer-dashboard.component.ts b/apps/lfx-one/src/app/modules/dashboards/maintainer/maintainer-dashboard.component.ts index df1997d4..88bbe2c2 100644 --- a/apps/lfx-one/src/app/modules/dashboards/maintainer/maintainer-dashboard.component.ts +++ b/apps/lfx-one/src/app/modules/dashboards/maintainer/maintainer-dashboard.component.ts @@ -25,6 +25,7 @@ export class MaintainerDashboardComponent { private readonly analyticsService = inject(AnalyticsService); private readonly projectContextService = inject(ProjectContextService); + public readonly selectedFoundation = computed(() => this.projectContextService.selectedFoundation()); public readonly form = new FormGroup({ selectedProjectId: new FormControl(this.projectContextService.getProjectId() || ''), }); diff --git a/apps/lfx-one/src/app/modules/meetings/meetings-dashboard/meetings-dashboard.component.html b/apps/lfx-one/src/app/modules/meetings/meetings-dashboard/meetings-dashboard.component.html index e1b63416..896e933e 100644 --- a/apps/lfx-one/src/app/modules/meetings/meetings-dashboard/meetings-dashboard.component.html +++ b/apps/lfx-one/src/app/modules/meetings/meetings-dashboard/meetings-dashboard.component.html @@ -3,7 +3,16 @@
-

Meetings

+
+ Meetings + + +
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 376cdcfb..d5b4c519 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 @@ -5,7 +5,9 @@ import { CommonModule } from '@angular/common'; import { Component, computed, inject, signal, Signal, WritableSignal } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { MeetingCardComponent } from '@app/shared/components/meeting-card/meeting-card.component'; -import { Meeting } from '@lfx-one/shared/interfaces'; +import { ProjectContextService } from '@app/shared/services/project-context.service'; +import { ButtonComponent } from '@components/button/button.component'; +import { Meeting, ProjectContext } from '@lfx-one/shared/interfaces'; import { getCurrentOrNextOccurrence } from '@lfx-one/shared/utils'; import { MeetingService } from '@services/meeting.service'; import { BehaviorSubject, map, switchMap, tap } from 'rxjs'; @@ -15,12 +17,13 @@ import { MeetingsTopBarComponent } from './components/meetings-top-bar/meetings- @Component({ selector: 'lfx-meetings-dashboard', standalone: true, - imports: [CommonModule, MeetingCardComponent, MeetingsTopBarComponent], + imports: [CommonModule, MeetingCardComponent, MeetingsTopBarComponent, ButtonComponent], templateUrl: './meetings-dashboard.component.html', styleUrl: './meetings-dashboard.component.scss', }) export class MeetingsDashboardComponent { private readonly meetingService = inject(MeetingService); + private readonly projectContextService = inject(ProjectContextService); public meetingsLoading: WritableSignal; public meetings: Signal = signal([]); @@ -30,6 +33,7 @@ export class MeetingsDashboardComponent { public searchQuery: WritableSignal; public timeFilter: WritableSignal<'upcoming' | 'past'>; public topBarVisibilityFilter: WritableSignal<'mine' | 'public'>; + public project: Signal; public constructor() { this.meetingsLoading = signal(true); @@ -40,6 +44,7 @@ export class MeetingsDashboardComponent { this.timeFilter = signal<'upcoming' | 'past'>('upcoming'); this.topBarVisibilityFilter = signal<'mine' | 'public'>('mine'); this.filteredMeetings = this.initializeFilteredMeetings(); + this.project = computed(() => this.projectContextService.selectedFoundation()); } public onViewChange(view: 'list' | 'calendar'): void { 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 80819d15..fef640a6 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 @@ -315,7 +315,7 @@ export class MeetingManageComponent { } return { - project_uid: project.uid, + project_uid: formValue.selectedProjectUid || project.uid, title: formValue.title, description: formValue.description || '', start_time: startDateTime, @@ -664,6 +664,7 @@ export class MeetingManageComponent { return new FormGroup( { // Step 1: Meeting Type + selectedProjectUid: new FormControl(''), meeting_type: new FormControl('', [Validators.required]), visibility: new FormControl(MeetingVisibility.PRIVATE), restricted: new FormControl(false), diff --git a/apps/lfx-one/src/app/modules/project/meetings/components/meeting-type-selection/meeting-type-selection.component.html b/apps/lfx-one/src/app/modules/project/meetings/components/meeting-type-selection/meeting-type-selection.component.html index ad2ed0ef..20618bf2 100644 --- a/apps/lfx-one/src/app/modules/project/meetings/components/meeting-type-selection/meeting-type-selection.component.html +++ b/apps/lfx-one/src/app/modules/project/meetings/components/meeting-type-selection/meeting-type-selection.component.html @@ -2,6 +2,23 @@
+ @if (projectOptions().length > 0) { +
+ +

Choose which project this meeting is for. If not selected, the current project will be used.

+ +
+ } +

Meeting Type

What kind of meeting are you organizing for your open source project?

diff --git a/apps/lfx-one/src/app/modules/project/meetings/components/meeting-type-selection/meeting-type-selection.component.ts b/apps/lfx-one/src/app/modules/project/meetings/components/meeting-type-selection/meeting-type-selection.component.ts index f0d22191..934e868b 100644 --- a/apps/lfx-one/src/app/modules/project/meetings/components/meeting-type-selection/meeting-type-selection.component.ts +++ b/apps/lfx-one/src/app/modules/project/meetings/components/meeting-type-selection/meeting-type-selection.component.ts @@ -2,12 +2,18 @@ // SPDX-License-Identifier: MIT import { CommonModule } from '@angular/common'; -import { Component, input } from '@angular/core'; +import { HttpParams } from '@angular/common/http'; +import { Component, computed, inject, input } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; import { FormGroup, ReactiveFormsModule } from '@angular/forms'; import { MessageComponent } from '@components/message/message.component'; +import { SelectComponent } from '@components/select/select.component'; import { ToggleComponent } from '@components/toggle/toggle.component'; import { MeetingType } from '@lfx-one/shared/enums'; +import { Project } from '@lfx-one/shared/interfaces'; +import { ProjectService } from '@services/project.service'; import { TooltipModule } from 'primeng/tooltip'; +import { map, of } from 'rxjs'; interface MeetingTypeInfo { icon: string; @@ -19,13 +25,26 @@ interface MeetingTypeInfo { @Component({ selector: 'lfx-meeting-type-selection', standalone: true, - imports: [CommonModule, ReactiveFormsModule, MessageComponent, ToggleComponent, TooltipModule], + imports: [CommonModule, ReactiveFormsModule, MessageComponent, SelectComponent, ToggleComponent, TooltipModule], templateUrl: './meeting-type-selection.component.html', }) export class MeetingTypeSelectionComponent { + private readonly projectService = inject(ProjectService); + // Form group input from parent public readonly form = input.required(); + // Child projects signal + public childProjects = this.initializeChildProjects(); + + // Map projects to select options + public projectOptions = computed(() => { + return this.childProjects().map((project: Project) => ({ + label: project.name, + value: project.uid, + })); + }); + // Meeting type options using shared enum (excluding NONE for selection) public readonly meetingTypeOptions = [ { label: 'Board', value: MeetingType.BOARD }, @@ -92,4 +111,24 @@ export class MeetingTypeSelectionComponent { this.form().get('meeting_type')?.setValue(meetingType); this.form().get('meeting_type')?.markAsTouched(); } + + // Get child projects for the current project + private initializeChildProjects() { + const currentProject = this.projectService.project(); + + if (!currentProject) { + return toSignal(of([]), { initialValue: [] }); + } + + const params = new HttpParams().set('tags', `parent_uid:${currentProject.uid}`); + return toSignal( + this.projectService.getProjects(params).pipe( + map((projects: Project[]) => { + // Filter out the current project from the list + return projects.filter((project) => project.uid !== currentProject.uid); + }) + ), + { initialValue: [] } + ); + } } diff --git a/apps/lfx-one/src/app/shared/components/project-selector/project-selector.component.html b/apps/lfx-one/src/app/shared/components/project-selector/project-selector.component.html new file mode 100644 index 00000000..c4a259b8 --- /dev/null +++ b/apps/lfx-one/src/app/shared/components/project-selector/project-selector.component.html @@ -0,0 +1,73 @@ + + + +@if (projects().length > 1) { + +
+ +
+
+ @if (displayLogo()) { + + } +
+
+ + +
+

+ {{ displayName() }} +

+
+ + + +
+ + + +
+ @for (project of projects(); track project.uid) { +
+ +
+
+ @if (project.logo_url) { + + } +
+
+ + {{ project.name }} +
+ } @empty { +
No projects available
+ } +
+
+} @else { + +
+ +
+
+ @if (displayLogo()) { + + } +
+
+ + +
+

+ {{ displayName() }} +

+
+
+} diff --git a/apps/lfx-one/src/app/shared/components/project-selector/project-selector.component.scss b/apps/lfx-one/src/app/shared/components/project-selector/project-selector.component.scss new file mode 100644 index 00000000..f764a9f2 --- /dev/null +++ b/apps/lfx-one/src/app/shared/components/project-selector/project-selector.component.scss @@ -0,0 +1,25 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +:host { + display: block; + width: 100%; +} + +::ng-deep { + .project-selector-panel { + @apply mt-2 rounded-none border-l-0 border-r-0 border-t; + + &::before { + @apply hidden; + } + + &::after { + @apply hidden; + } + + .p-popover-content { + @apply rounded-none; + } + } +} diff --git a/apps/lfx-one/src/app/shared/components/project-selector/project-selector.component.ts b/apps/lfx-one/src/app/shared/components/project-selector/project-selector.component.ts new file mode 100644 index 00000000..83c7b040 --- /dev/null +++ b/apps/lfx-one/src/app/shared/components/project-selector/project-selector.component.ts @@ -0,0 +1,46 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +import { CommonModule } from '@angular/common'; +import { Component, computed, input, output } from '@angular/core'; +import { ButtonComponent } from '@components/button/button.component'; +import { Project } from '@lfx-one/shared/interfaces'; +import { Popover } from 'primeng/popover'; +import { PopoverModule } from 'primeng/popover'; + +@Component({ + selector: 'lfx-project-selector', + standalone: true, + imports: [CommonModule, PopoverModule, ButtonComponent], + templateUrl: './project-selector.component.html', + styleUrl: './project-selector.component.scss', +}) +export class ProjectSelectorComponent { + // Input properties + public readonly projects = input.required(); + public readonly selectedProject = input(null); + + // Output events + public readonly projectChange = output(); + + // Computed properties + protected readonly displayName = computed(() => { + const project = this.selectedProject(); + return project?.name || 'Select Project'; + }); + + protected readonly displayLogo = computed(() => { + const project = this.selectedProject(); + return project?.logo_url || ''; + }); + + // Event handlers + protected selectProject(project: Project, popover: Popover): void { + this.projectChange.emit(project); + popover.hide(); + } + + protected togglePanel(event: Event, popover: Popover): void { + popover.toggle(event); + } +} diff --git a/apps/lfx-one/src/app/shared/components/sidebar/sidebar.component.html b/apps/lfx-one/src/app/shared/components/sidebar/sidebar.component.html index 4ff8d1f9..4f65b8ce 100644 --- a/apps/lfx-one/src/app/shared/components/sidebar/sidebar.component.html +++ b/apps/lfx-one/src/app/shared/components/sidebar/sidebar.component.html @@ -10,6 +10,13 @@ rgba(0, 0, 0, 0.1) 0px 1px 2px -1px; " data-testid="sidebar"> + + @if (showProjectSelector()) { +
+ +
+ } +