Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<div class="flex min-h-screen pt-4">
<!-- Sidebar - Desktop -->
<div class="hidden lg:block w-72 flex-shrink-0 fixed top-[6rem] left-0">
<lfx-sidebar [items]="sidebarItems" [footerItems]="sidebarFooterItems"></lfx-sidebar>
<lfx-sidebar [items]="sidebarItems" [footerItems]="sidebarFooterItems" [showProjectSelector]="true"></lfx-sidebar>
</div>

<!-- Sidebar - Mobile Overlay -->
Expand All @@ -23,7 +23,7 @@ <h2 class="text-lg font-semibold text-gray-900">Menu</h2>
</button>
</div>
<div class="overflow-y-auto h-[calc(100vh-4rem)]">
<lfx-sidebar [items]="sidebarItems" [footerItems]="sidebarFooterItems"></lfx-sidebar>
<lfx-sidebar [items]="sidebarItems" [footerItems]="sidebarFooterItems" [showProjectSelector]="true"></lfx-sidebar>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
<!-- SPDX-License-Identifier: MIT -->

<div class="container mx-auto px-4 sm:px-6 lg:px-8" data-testid="dashboard-container">
<!-- Foundation Project -->
<div class="mb-6 flex items-center gap-4" data-testid="foundation-project">
<h1 class="text-2xl font-serif font-semibold text-gray-900">{{ selectedFoundation()?.name }} Overview</h1>
</div>

<!-- Organization Selector -->
<div class="mb-6 flex items-center gap-4" data-testid="organization-selector">
<label for="organization-select" class="text-sm font-semibold text-gray-700">Organization:</label>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<string>(this.accountContextService.selectedAccount().accountId),
});

public readonly availableAccounts: Signal<Account[]> = computed(() => this.accountContextService.availableAccounts);
public readonly selectedFoundation = computed(() => this.projectContextService.selectedFoundation());

public constructor() {
this.form
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,16 @@ <h2 class="font-['Roboto_Slab'] font-semibold text-[16px]">{{ accountName() }}'s
</div>
</div>

@if (isLoading()) {
@if (!hasFoundationSelected()) {
<!-- No Foundation Selected State -->
<div class="flex items-center justify-center p-12">
<div class="text-center space-y-3">
<i class="fa-light fa-circle-exclamation text-4xl text-gray-400"></i>
<p class="text-sm text-gray-600 font-medium">Please select a foundation project to view organization data</p>
<p class="text-xs text-gray-500">Use the project selector in the sidebar to choose a foundation</p>
</div>
</div>
} @else if (isLoading()) {
<!-- Loading State -->
<div class="flex items-center justify-center p-12">
<div class="text-center space-y-3">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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<boolean>(() => !!this.projectContextService.selectedFoundation());
private readonly contributionsOverviewData = this.initializeContributionsOverviewData();
private readonly boardMemberDashboardData = this.initializeBoardMemberDashboardData();
private readonly eventsOverviewData = this.initializeEventsOverviewData();
Expand Down Expand Up @@ -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)));
})
),
{
Expand All @@ -166,12 +190,6 @@ export class OrganizationInvolvementComponent {
certifications: 0,
certifiedEmployees: 0,
},
boardMeetingAttendance: {
totalMeetings: 0,
attendedMeetings: 0,
notAttendedMeetings: 0,
attendancePercentage: 0,
},
accountId: '',
projectId: '',
},
Expand All @@ -196,7 +214,6 @@ export class OrganizationInvolvementComponent {
accountName: '',
},
accountId: '',
projectId: '',
},
}
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
<!-- SPDX-License-Identifier: MIT -->

<div class="container mx-auto px-4 sm:px-6 lg:px-8" data-testid="dashboard-container">
<!-- Foundation Project -->
<div class="mb-6 flex items-center gap-4" data-testid="foundation-project">
<h1 class="text-2xl font-serif font-semibold text-gray-900">{{ selectedFoundation()?.name }} Overview</h1>
</div>

<!-- Dashboard Sections -->
<div class="flex flex-col gap-6" data-testid="dashboard-sections-grid">
<!-- Recent Progress - Full Width -->
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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());
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
<!-- SPDX-License-Identifier: MIT -->

<div class="container mx-auto px-4 sm:px-6 lg:px-8" data-testid="dashboard-container">
<!-- Foundation Project -->
<div class="mb-6 flex items-center gap-4" data-testid="foundation-project">
<h1 class="text-2xl font-serif font-semibold text-gray-900">{{ selectedFoundation()?.name }} Overview</h1>
</div>

<!-- Filters -->
<div class="mb-6 flex flex-col sm:flex-row items-start sm:items-center gap-4" data-testid="maintainer-filters">
<div class="flex flex-col sm:flex-row items-start sm:items-center gap-4 w-full">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>(this.projectContextService.getProjectId() || ''),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,16 @@

<div class="p-8 mx-auto">
<div class="mb-8">
<h1 class="text-2xl font-serif font-semibold text-gray-900 mb-8">Meetings</h1>
<div class="flex justify-between items-center w-full gap-4 mb-8">
<span class="mb-0 text-2xl font-serif font-semibold text-gray-900">Meetings</span>
<lfx-button
icon="fa-light fa-plus"
severity="primary"
[attr.data-testid]="'meeting-create-button'"
label="Create Meeting"
[routerLink]="['/project', project()?.slug || '', 'meetings', 'create']">
</lfx-button>
</div>

<div class="sticky top-0 md:top-6 z-10 bg-white rounded-lg border border-slate-200 p-4 -mx-0 mb-8">
<div class="flex flex-col lg:flex-row gap-4 lg:items-center">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<boolean>;
public meetings: Signal<Meeting[]> = signal([]);
Expand All @@ -30,6 +33,7 @@ export class MeetingsDashboardComponent {
public searchQuery: WritableSignal<string>;
public timeFilter: WritableSignal<'upcoming' | 'past'>;
public topBarVisibilityFilter: WritableSignal<'mine' | 'public'>;
public project: Signal<ProjectContext | null>;

public constructor() {
this.meetingsLoading = signal<boolean>(true);
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@
<!-- SPDX-License-Identifier: MIT -->

<div class="space-y-6">
@if (projectOptions().length > 0) {
<div data-testid="meeting-project-selector">
<label for="project-select" class="text-sm font-semibold text-slate-900 mb-2 block">Select Project</label>
<p class="text-xs text-slate-500 mb-3">Choose which project this meeting is for. If not selected, the current project will be used.</p>
<lfx-select
class="w-full"
styleClass="w-full"
id="project-select"
[form]="form()"
control="selectedProjectUid"
[options]="projectOptions()"
placeholder="Select a project (optional)"
[showClear]="true"
data-testid="project-select-dropdown" />
</div>
}

<div class="text-center mb-8">
<h2 class="text-2xl font-semibold text-slate-900 mb-2">Meeting Type</h2>
<p class="text-slate-500 text-base">What kind of meeting are you organizing for your open source project?</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<FormGroup>();

// 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 },
Expand Down Expand Up @@ -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: [] }
);
}
}
Loading