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
1 change: 0 additions & 1 deletion apps/lfx-one/e2e/homepage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ test.describe('Homepage', () => {
// Check for metrics in project cards using data-testids
await expect(firstCard.getByTestId('metric-label').filter({ hasText: 'Meetings' })).toBeVisible();
await expect(firstCard.getByTestId('metric-label').filter({ hasText: 'Committees' })).toBeVisible();
await expect(firstCard.getByTestId('metric-label').filter({ hasText: 'Mailing Lists' })).toBeVisible();
});

test('should filter projects when searching', async ({ page }) => {
Expand Down
8 changes: 4 additions & 4 deletions apps/lfx-one/e2e/project-dashboard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ test.describe('Project Dashboard', () => {
await expect(page.getByTestId('menu-item').filter({ hasText: 'Dashboard' })).toBeVisible();
await expect(page.getByTestId('menu-item').filter({ hasText: 'Meetings' })).toBeVisible();
await expect(page.getByTestId('menu-item').filter({ hasText: 'Committees' })).toBeVisible();
await expect(page.getByTestId('menu-item').filter({ hasText: 'Mailing Lists' })).toBeVisible();
await expect(page.getByTestId('menu-item').filter({ hasText: 'Settings' })).toBeVisible();
// await expect(page.getByTestId('menu-item').filter({ hasText: 'Mailing Lists' })).toBeVisible();
// await expect(page.getByTestId('menu-item').filter({ hasText: 'Settings' })).toBeVisible();
});
});

Expand Down Expand Up @@ -423,10 +423,10 @@ test.describe('Without Write Permissions', () => {
await expect(page.getByTestId('menu-item').filter({ hasText: 'Dashboard' })).toBeVisible();
await expect(page.getByTestId('menu-item').filter({ hasText: 'Meetings' })).toBeVisible();
await expect(page.getByTestId('menu-item').filter({ hasText: 'Committees' })).toBeVisible();
await expect(page.getByTestId('menu-item').filter({ hasText: 'Mailing Lists' })).toBeVisible();
// await expect(page.getByTestId('menu-item').filter({ hasText: 'Mailing Lists' })).toBeVisible();

// Settings tab should also be accessible (though it may have limited functionality)
await expect(page.getByTestId('menu-item').filter({ hasText: 'Settings' })).toBeVisible();
// await expect(page.getByTestId('menu-item').filter({ hasText: 'Settings' })).toBeVisible();
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ <h1 class="text-2xl font-display font-semibold text-gray-900 mb-3">
</a>
</div>
</div>
<!-- TODO: Add settings menu item back in
<div class="items-center gap-3 hidden md:flex">
<a
routerLink="/project/{{ projectSlug() }}/settings"
Expand All @@ -79,6 +80,7 @@ <h1 class="text-2xl font-display font-semibold text-gray-900 mb-3">
<span>Settings</span>
</a>
</div>
-->
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,6 @@ export class ProjectLayoutComponent {
routerLink: `/project/${this.projectSlug()}/committees`,
routerLinkActiveOptions: { exact: false },
},
{
label: 'Mailing Lists',
icon: 'fa-light fa-envelope text-amber-500',
routerLink: `/project/${this.projectSlug()}/mailing-lists`,
routerLinkActiveOptions: { exact: false },
},
]);

public readonly metrics = computed(() => [
Expand Down
5 changes: 3 additions & 2 deletions apps/lfx-one/src/app/modules/pages/home/home.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ <h1 class="text-3xl font-display font-light text-gray-900 mb-3 xl:px-96 lg:px-64

<div class="container mx-auto py-24 px-8" data-testid="projects-section">
@if (projects().length > 0) {
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" data-testid="projects-grid">
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6" data-testid="projects-grid">
@for (project of filteredProjects(); track project.uid; let i = $index) {
<div
pAnimateOnScroll
Expand All @@ -55,7 +55,8 @@ <h1 class="text-3xl font-display font-light text-gray-900 mb-3 xl:px-96 lg:px-64
[description]="project.description ?? ''"
[logo]="project.logo_url ?? ''"
[url]="project.slug"
[metrics]="project.metrics"></lfx-project-card>
[metrics]="project.metrics"
class="h-full"></lfx-project-card>
</div>
}
</div>
Expand Down
5 changes: 0 additions & 5 deletions apps/lfx-one/src/app/modules/pages/home/home.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,6 @@ export class HomeComponent {
value: project.committees_count,
icon: 'fa-light fa-people-group text-green-500',
},
{
label: 'Mailing Lists',
value: project.mailing_list_count,
icon: 'fa-light fa-envelope text-amber-500',
},
];

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@
</lfx-card>

<!-- Original 3 cards row -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 items-stretch">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-6 items-stretch">
<div class="items-stretch">
@if (meetingsLoading()) {
<div>
Expand Down Expand Up @@ -267,39 +267,6 @@ <h3 class="text-lg font-medium text-gray-900 mb-2">No Committees</h3>
</lfx-card>
}
</div>
<div class="items-stretch">
<lfx-card header="Mailing Lists" styleClass="h-full" class="h-full">
<div class="flex flex-col gap-2">
<h3 class="text-sm font-display text-gray-500">Recently Joined Mailing Lists</h3>
<lfx-table [value]="mailingListTableData()" [scrollable]="false">
<ng-template #header let-columns>
<tr>
<th>
<span class="text-sm font-sans text-gray-500">Mailing List</span>
</th>
<th>
<div class="flex justify-end">
<span class="text-sm font-sans text-gray-500">Last Updated</span>
</div>
</th>
</tr>
</ng-template>
<ng-template #body let-row>
<tr>
<td class="w-1/2 max-w-0">
<a class="block truncate text-sm" [title]="row.title" [routerLink]="[row.url]" [relativeTo]="activatedRoute">{{ row.title }}</a>
</td>
<td class="w-1/2">
<div class="flex justify-end whitespace-nowrap">
<span class="text-sm font-sans"> {{ row.date | date: 'MMM d, yyyy @ h:mm a' }}</span>
</div>
</td>
</tr>
</ng-template>
</lfx-table>
</div>
</lfx-card>
</div>
</div>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,60 +5,58 @@
class="bg-white rounded-lg border border-gray-200 shadow-sm hover:shadow-md transition-shadow cursor-pointer h-full"
[routerLink]="['/project', url()]"
data-testid="project-card-container">
<div class="p-6">
<div class="p-6 h-full flex flex-col gap-6">
<!-- Logo and Title Section -->
<div class="flex flex-col gap-6 mb-6" data-testid="project-header">
<div class="flex flex-col gap-6" data-testid="project-header">
<div class="flex flex-col items-start gap-0">
<!-- Logo Container -->
<div class="flex items-start" data-testid="project-logo-container">
<img [src]="logo()" [alt]="name() + ' logo'" class="w-full h-16 object-contain" data-testid="project-logo" />
</div>

<!-- Title and Description -->
<div class="flex flex-col gap-2 mt-4" data-testid="project-info">
<h3 class="text-base font-display font-semibold text-gray-900" data-testid="project-title">
{{ name() }}
</h3>
<p class="text-sm text-gray-600 leading-relaxed line-clamp-2" data-testid="project-description">
{{ description() }}
</p>
<img [src]="logo()" [alt]="name() + ' logo'" class="w-full h-12 object-contain" data-testid="project-logo" />
</div>
</div>
</div>
<!-- Title and Description -->
<div class="flex flex-col justify-between gap-2 h-full" data-testid="project-info">
<h3 class="text-base font-display font-semibold text-gray-900" data-testid="project-title">
{{ name() }}
</h3>
<div class="flex flex-col gap-2">
<p class="text-sm text-gray-600 leading-relaxed line-clamp-2" data-testid="project-description">
{{ description() }}
</p>

<!-- Metrics Section -->
@if (metrics().length > 0) {
<div class="flex flex-col gap-3" data-testid="project-metrics">
@for (metric of metrics(); track metric.label) {
<div class="flex items-center justify-between py-0.5" data-testid="project-metric" [attr.data-metric-label]="metric.label">
<div class="flex items-center gap-2" data-testid="metric-label-container">
<i [class]="metric.icon" class="text-base w-5" data-testid="metric-icon"></i>
<span class="text-sm text-gray-600" data-testid="metric-label">{{ metric.label }}</span>
</div>
<div class="flex items-center gap-2" data-testid="metric-value-container">
@if (metric.badge) {
<span
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-sm font-semibold"
[class]="
metric.badge.severity === 'success'
? 'bg-emerald-500 text-white'
: metric.badge.severity === 'info'
? 'bg-blue-500 text-white'
: metric.badge.severity === 'warning'
? 'bg-amber-500 text-white'
: 'bg-red-500 text-white'
"
data-testid="metric-badge"
[attr.data-badge-severity]="metric.badge.severity">
{{ metric.badge.label }}
</span>
} @else {
<span class="text-sm text-gray-900 font-normal font-display" data-testid="metric-value">{{ metric.value || 0 }}</span>
}
</div>
<!-- Metrics Section -->
@if (metrics().length > 0) {
<div class="flex flex-col gap-3" data-testid="project-metrics">
@for (metric of metrics(); track metric.label) {
<div class="flex items-center justify-between py-0.5" data-testid="project-metric" [attr.data-metric-label]="metric.label">
<div class="flex items-center gap-2" data-testid="metric-label-container">
<i [class]="metric.icon" class="text-base w-5" data-testid="metric-icon"></i>
<span class="text-sm text-gray-600" data-testid="metric-label">{{ metric.label }}</span>
</div>
<div class="flex items-center gap-2" data-testid="metric-value-container">
@if (metric.badge) {
<span
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-sm font-semibold"
[ngClass]="{
'bg-emerald-500 text-white': metric.badge.severity === 'success',
'bg-blue-500 text-white': metric.badge.severity === 'info',
'bg-amber-500 text-white': metric.badge.severity === 'warning',
'bg-red-500 text-white': metric.badge.severity === 'danger',
}"
data-testid="metric-badge"
[attr.data-badge-severity]="metric.badge.severity">
{{ metric.badge.label }}
</span>
} @else {
<span class="text-sm text-gray-900 font-normal font-display" data-testid="metric-value">{{ metric.value || 0 }}</span>
}
</div>
</div>
}
</div>
}
</div>
}
</div>
</div>
</div>
22 changes: 22 additions & 0 deletions apps/lfx-one/src/server/controllers/project.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ import { NextFunction, Request, Response } from 'express';

import { ServiceValidationError } from '../errors';
import { Logger } from '../helpers/logger';
import { CommitteeService } from '../services/committee.service';
import { MeetingService } from '../services/meeting.service';
import { ProjectService } from '../services/project.service';

/**
* Controller for handling project HTTP requests
*/
export class ProjectController {
private projectService: ProjectService = new ProjectService();
private meetingService: MeetingService = new MeetingService();
private committeeService: CommitteeService = new CommitteeService();

/**
* GET /projects
Expand All @@ -25,6 +29,15 @@ export class ProjectController {
// Get the projects
const projects = await this.projectService.getProjects(req, req.query as Record<string, any>);

// Add metrics to all projects
// TODO: Remove this once we have a way to get the metrics from the microservice
await Promise.all(
projects.map(async (project) => {
project.meetings_count = (await this.meetingService.getMeetings(req, { tags: `project_uid:${project.uid}` }).catch(() => [])).length;
project.committees_count = (await this.committeeService.getCommittees(req, { tags: `project_uid:${project.uid}` }).catch(() => [])).length;
})
);

Logger.success(req, 'get_projects', startTime, {
project_count: projects.length,
});
Expand Down Expand Up @@ -68,6 +81,15 @@ export class ProjectController {
// Search for the projects
const results = await this.projectService.searchProjects(req, q);

// Add metrics to all projects
// TODO: Remove this once we have a way to get the metrics from the microservice
await Promise.all(
results.map(async (project) => {
project.meetings_count = (await this.meetingService.getMeetings(req, { tags: `project_uid:${project.uid}` }).catch(() => [])).length;
project.committees_count = (await this.committeeService.getCommittees(req, { tags: `project_uid:${project.uid}` }).catch(() => [])).length;
})
);

// Log the success
Logger.success(req, 'search_projects', startTime, {
result_count: results.length,
Expand Down