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
15 changes: 12 additions & 3 deletions apps/lfx-one/angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,16 @@
"ssr": {
"entry": "src/server/server.ts"
},
"allowedCommonJsDependencies": ["@linuxfoundation/lfx-ui-core"],
"allowedCommonJsDependencies": [
"@linuxfoundation/lfx-ui-core",
"combined-stream",
"mime-types",
"asynckit",
"hasown",
"es-set-tostringtag",
"safe-buffer",
"lodash.isempty"
],
"externalDependencies": ["snowflake-sdk"],
"define": {
"LAUNCHDARKLY_CLIENT_ID": "''"
Expand All @@ -68,7 +77,7 @@
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kB",
"maximumWarning": "6kB",
"maximumError": "8kB"
}
],
Expand All @@ -92,7 +101,7 @@
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kB",
"maximumWarning": "6kB",
"maximumError": "8kB"
}
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ <h1 class="text-2xl font-serif font-semibold text-gray-900">{{ selectedFoundatio
<!-- Middle Row - Two Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Pending Actions -->
<lfx-pending-actions class="h-full" />
<lfx-pending-actions class="h-full" [pendingActions]="boardMemberActions()" />

<!-- My Meetings -->
<lfx-my-meetings class="h-full" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@
// SPDX-License-Identifier: MIT

import { Component, computed, inject, Signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { takeUntilDestroyed, toObservable, toSignal } from '@angular/core/rxjs-interop';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { Account } from '@lfx-one/shared/interfaces';
import { Account, PendingActionItem } from '@lfx-one/shared/interfaces';
import { catchError, of, switchMap } from 'rxjs';

import { SelectComponent } from '../../../shared/components/select/select.component';
import { AccountContextService } from '../../../shared/services/account-context.service';
import { FeatureFlagService } from '../../../shared/services/feature-flag.service';
import { ProjectContextService } from '../../../shared/services/project-context.service';
import { ProjectService } from '../../../shared/services/project.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 @@ -24,6 +26,7 @@ import { PendingActionsComponent } from '../components/pending-actions/pending-a
export class BoardMemberDashboardComponent {
private readonly accountContextService = inject(AccountContextService);
private readonly projectContextService = inject(ProjectContextService);
private readonly projectService = inject(ProjectService);
private readonly featureFlagService = inject(FeatureFlagService);

public readonly form = new FormGroup({
Expand All @@ -32,11 +35,14 @@ export class BoardMemberDashboardComponent {

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

// Feature flags
protected readonly showOrganizationSelector = this.featureFlagService.getBooleanFlag('organization-selector', true);

public constructor() {
// Handle account selection changes
this.form
.get('selectedAccountId')
?.valueChanges.pipe(takeUntilDestroyed())
Expand All @@ -46,5 +52,34 @@ export class BoardMemberDashboardComponent {
this.accountContextService.setAccount(selectedAccount as Account);
}
});

// Initialize board member actions with reactive pattern
this.boardMemberActions = this.initializeBoardMemberActions();
}

private initializeBoardMemberActions(): Signal<PendingActionItem[]> {
// Convert project signal to observable to react to changes (handles both project and foundation)
const project$ = toObservable(this.selectedProject);

return toSignal(
project$.pipe(
switchMap((project) => {
// If no project/foundation selected, return empty array
if (!project?.slug) {
return of([]);
}

// Fetch survey actions from API
return this.projectService.getPendingActionSurveys(project.slug).pipe(
catchError((error) => {
console.error('Failed to fetch survey actions:', error);
// Return empty array on error
return of([]);
})
);
})
),
{ initialValue: [] }
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,85 +4,94 @@
<section class="flex flex-col flex-1" data-testid="dashboard-pending-actions-section">
<!-- Header -->
<div class="flex items-center justify-between mb-4">
<h2 class="font-display font-semibold text-gray-900">Pending Actions</h2>
<lfx-button
label="View All"
icon="fa-light fa-chevron-right"
iconPos="right"
(onClick)="handleViewAll()"
styleClass="!text-sm !font-normal"
[text]="true"
size="small"
data-testid="dashboard-pending-actions-view-all" />
<h2 class="font-display font-semibold text-gray-900 py-1.5">Pending Actions</h2>
</div>

<!-- Scrollable Content -->
<div class="flex flex-col flex-1 max-h-[28.125rem] overflow-y-auto">
<div class="flex flex-col gap-3" data-testid="dashboard-pending-actions-list">
@for (item of pendingActions(); track item.text) {
<div
class="p-4 border rounded-lg"
[ngClass]="{
'bg-amber-50 border-amber-200': item.color === 'amber',
'bg-blue-50 border-blue-200': item.color === 'blue',
'bg-green-50 border-green-200': item.color === 'green',
'bg-purple-50 border-purple-200': item.color === 'purple',
}"
[attr.data-testid]="'dashboard-pending-actions-item-' + item.type">
<!-- Header with Type and Badge -->
<div class="flex items-start justify-between mb-3">
<div class="flex items-center gap-2">
<div
[ngClass]="{
'text-amber-700': item.color === 'amber',
'text-blue-700': item.color === 'blue',
'text-green-700': item.color === 'green',
'text-purple-700': item.color === 'purple',
}">
<i [ngClass]="[item.icon, 'w-4', 'h-4']"></i>
@if (pendingActions().length > 0) {
<div class="flex flex-col gap-3" data-testid="dashboard-pending-actions-list">
@for (item of pendingActions(); track $index) {
<div
class="p-4 border rounded-lg"
[ngClass]="{
'bg-amber-50 border-amber-200': item.color === 'amber',
'bg-blue-50 border-blue-200': item.color === 'blue',
'bg-green-50 border-green-200': item.color === 'green',
'bg-purple-50 border-purple-200': item.color === 'purple',
}"
[attr.data-testid]="'dashboard-pending-actions-item-' + item.type">
<!-- Header with Type and Badge -->
<div class="flex items-start justify-between mb-3">
<div class="flex items-center gap-2">
<div
[ngClass]="{
'text-amber-700': item.color === 'amber',
'text-blue-700': item.color === 'blue',
'text-green-700': item.color === 'green',
'text-purple-700': item.color === 'purple',
}">
<i [ngClass]="[item.icon, 'w-4', 'h-4']"></i>
</div>
<span
class="text-xs font-medium"
[ngClass]="{
'text-amber-700': item.color === 'amber',
'text-blue-700': item.color === 'blue',
'text-green-700': item.color === 'green',
'text-purple-700': item.color === 'purple',
}">
{{ item.type }}
</span>
</div>
<span
class="text-xs font-medium"
class="inline-flex items-center justify-center rounded-md px-2 py-0.5 text-xs font-medium bg-white text-gray-700 border border-gray-200"
data-testid="dashboard-pending-actions-badge">
{{ item.badge }}
</span>
</div>

<!-- Action Text -->
<div class="mb-4">
<p
class="text-sm font-medium"
[ngClass]="{
'text-amber-700': item.color === 'amber',
'text-blue-700': item.color === 'blue',
'text-green-700': item.color === 'green',
'text-purple-700': item.color === 'purple',
'text-amber-900': item.color === 'amber',
'text-blue-900': item.color === 'blue',
'text-green-900': item.color === 'green',
'text-purple-900': item.color === 'purple',
}">
{{ item.type }}
</span>
{{ item.text }}
</p>
</div>
<span
class="inline-flex items-center justify-center rounded-md px-2 py-0.5 text-xs font-medium bg-white text-gray-700 border border-gray-200"
data-testid="dashboard-pending-actions-badge">
{{ item.badge }}
</span>
</div>

<!-- Action Text -->
<div class="mb-4">
<p
class="text-sm font-medium"
[ngClass]="{
'text-amber-900': item.color === 'amber',
'text-blue-900': item.color === 'blue',
'text-green-900': item.color === 'green',
'text-purple-900': item.color === 'purple',
}">
{{ item.text }}
</p>
<!-- Action Button -->
@if (item.buttonLink) {
<a
[href]="item.buttonLink"
target="_blank"
rel="noopener noreferrer"
class="w-full h-8 rounded-md px-3 text-sm font-medium border border-gray-300 bg-white text-gray-700 hover:bg-slate-100 transition-colors flex items-center justify-center"
data-testid="dashboard-pending-actions-button">
{{ item.buttonText }}
</a>
} @else {
<button
type="button"
(click)="handleActionClick(item)"
class="w-full h-8 rounded-md px-3 text-sm font-medium border border-gray-300 bg-white text-gray-700 hover:bg-slate-100 transition-colors"
data-testid="dashboard-pending-actions-button">
{{ item.buttonText }}
</button>
}
</div>

<!-- Action Button -->
<button
type="button"
(click)="handleActionClick(item)"
class="w-full h-8 rounded-md px-3 text-sm font-medium border border-gray-300 bg-white text-gray-700 hover:bg-slate-100 transition-colors"
data-testid="dashboard-pending-actions-button">
{{ item.buttonText }}
</button>
</div>
}
</div>
}
</div>
} @else {
<!-- Empty state - shows when no pending actions -->
<div class="text-xs text-gray-500 py-8 text-center border-2 border-dashed border-gray-300 rounded-lg" data-testid="dashboard-pending-actions-empty">
No pending actions
</div>
}
</div>
</section>
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,24 @@
// SPDX-License-Identifier: MIT

import { CommonModule } from '@angular/common';
import { Component, computed, inject, output } from '@angular/core';
import { PersonaService } from '@app/shared/services/persona.service';
import { ButtonComponent } from '@components/button/button.component';
import { BOARD_MEMBER_ACTION_ITEMS, CORE_DEVELOPER_ACTION_ITEMS, MAINTAINER_ACTION_ITEMS } from '@lfx-one/shared/constants';
import { Component, input, output } from '@angular/core';

import type { PendingActionItem } from '@lfx-one/shared/interfaces';

@Component({
selector: 'lfx-pending-actions',
standalone: true,
imports: [CommonModule, ButtonComponent],
imports: [CommonModule],
templateUrl: './pending-actions.component.html',
styleUrl: './pending-actions.component.scss',
})
export class PendingActionsComponent {
private readonly personaService = inject(PersonaService);

public readonly actionClick = output<PendingActionItem>();
public readonly viewAll = output<void>();

/**
* Computed signal that returns action items based on the current persona
* Required input signal for pending action items
*/
protected readonly pendingActions = computed<PendingActionItem[]>(() => {
const persona = this.personaService.currentPersona();
public readonly pendingActions = input.required<PendingActionItem[]>();

switch (persona) {
case 'maintainer':
return MAINTAINER_ACTION_ITEMS;
case 'board-member':
return BOARD_MEMBER_ACTION_ITEMS;
case 'core-developer':
default:
return CORE_DEVELOPER_ACTION_ITEMS;
}
});

public handleViewAll(): void {
this.viewAll.emit();
}
public readonly actionClick = output<PendingActionItem>();

protected handleActionClick(item: PendingActionItem): void {
this.actionClick.emit(item);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ <h1 class="text-2xl font-serif font-semibold text-gray-900">{{ selectedFoundatio
<!-- Middle Row - Two Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Pending Actions -->
<lfx-pending-actions class="h-full" />
<lfx-pending-actions class="h-full" [pendingActions]="coreDevActions()" />

<!-- My Meetings -->
<lfx-my-meetings class="h-full" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// Copyright The Linux Foundation and each contributor to LFX.
// SPDX-License-Identifier: MIT

import { Component, computed, inject } from '@angular/core';
import { Component, computed, inject, signal } from '@angular/core';
import { ProjectContextService } from '@app/shared/services/project-context.service';
import { CORE_DEVELOPER_ACTION_ITEMS } from '@lfx-one/shared/constants';

import { MyMeetingsComponent } from '../components/my-meetings/my-meetings.component';
import { MyProjectsComponent } from '../components/my-projects/my-projects.component';
Expand All @@ -20,4 +21,5 @@ export class CoreDeveloperDashboardComponent {
private readonly projectContextService = inject(ProjectContextService);

public readonly selectedFoundation = computed(() => this.projectContextService.selectedFoundation());
public readonly coreDevActions = signal(CORE_DEVELOPER_ACTION_ITEMS);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ <h1 class="text-2xl font-serif font-semibold text-gray-900">{{ selectedProject()
<!-- Middle Row - Two Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Pending Actions -->
<lfx-pending-actions class="h-full" />
<lfx-pending-actions class="h-full" [pendingActions]="maintainerActions()" />

<!-- My Meetings -->
<lfx-my-meetings class="h-full" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// Copyright The Linux Foundation and each contributor to LFX.
// SPDX-License-Identifier: MIT

import { Component, computed, inject } from '@angular/core';
import { Component, computed, inject, signal } from '@angular/core';
import { ProjectContextService } from '@app/shared/services/project-context.service';
import { MAINTAINER_ACTION_ITEMS } from '@lfx-one/shared/constants';

import { MyMeetingsComponent } from '../components/my-meetings/my-meetings.component';
import { MyProjectsComponent } from '../components/my-projects/my-projects.component';
Expand All @@ -20,4 +21,5 @@ export class MaintainerDashboardComponent {
private readonly projectContextService = inject(ProjectContextService);

public readonly selectedProject = computed(() => this.projectContextService.selectedFoundation() || this.projectContextService.selectedProject());
public readonly maintainerActions = signal(MAINTAINER_ACTION_ITEMS);
}
Loading