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: 1 addition & 0 deletions apps/lfx-pcc/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ QUERY_SERVICE_TOKEN=your-jwt-token-here
# Get these from your Supabase project settings
SUPABASE_URL=https://your-project.supabase.co
POSTGRES_API_KEY=your-supabase-anon-key
SUPABASE_STORAGE_BUCKET=meeting-attachments

# E2E Test Configuration (Optional)
# Test user credentials for automated testing
Expand Down
14 changes: 14 additions & 0 deletions apps/lfx-pcc/src/app/app.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,18 @@
.pill {
@apply inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-full transition-colors text-gray-600 border border-gray-200 hover:bg-gray-50;
}

.p-fileupload {
.p-fileupload-content {
@apply mx-3 mb-3 border border-dashed border-blue-300 rounded-md text-gray-500;

.p-fileupload-file-list {
@apply hidden;
}

p-progressbar {
@apply hidden;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
<!-- SPDX-License-Identifier: MIT -->

<form [formGroup]="form()" (ngSubmit)="onSubmit()" class="space-y-6">
@if (!isEditing()) {
<div class="bg-blue-50 shadow-md p-3 rounded-md text-sm">
<p class="text-sm">
Committees are a great way to connect community members who have shared interests. There are common committees like governing boards and technical
oversight committees, but you can also create committees for special interest groups (SIGs) or working groups to tackle specific concerns for your
project community.
</p>
</div>
}
<!-- Basic Information Section -->
<div class="flex flex-col gap-3">
<h3 class="text-lg font-medium text-gray-900">Basic Information</h3>
Expand Down Expand Up @@ -45,19 +54,49 @@ <h3 class="text-lg font-medium text-gray-900">Basic Information</h3>
<h3 class="text-lg font-medium text-gray-900">Settings</h3>

<div class="flex flex-col gap-2">
<lfx-toggle size="small" [form]="form()" control="business_email_required" label="Business Email Required" id="business-email-toggle"></lfx-toggle>
<lfx-toggle
size="small"
[form]="form()"
control="business_email_required"
label="Business Email Required"
id="business-email-toggle"
tooltip="If enabled, public emails like Gmail or Yahoo will require confirmation before being saved to limit their presence in the committee"
tooltipPosition="right"></lfx-toggle>

<lfx-toggle size="small" [form]="form()" control="enable_voting" label="Enable Voting" id="voting-toggle"></lfx-toggle>
<lfx-toggle
size="small"
[form]="form()"
control="enable_voting"
label="Enable Voting"
id="voting-toggle"
tooltip="Voting status for members can be enabled for the committee"
tooltipPosition="right"></lfx-toggle>

<lfx-toggle size="small" [form]="form()" control="is_audit_enabled" label="Enable Audit" id="audit-toggle"></lfx-toggle>

<lfx-toggle
size="small"
[form]="form()"
control="joinable"
label="Joinable"
id="joinable-toggle"
tooltip="When enabled, users are able to join the committee through LFX Projects"
tooltipPosition="right"></lfx-toggle>
</div>
</div>

<!-- Public Settings Section -->
<div class="flex flex-col gap-3">
<h3 class="text-lg font-medium text-gray-900">Public Settings</h3>

<lfx-toggle size="small" [form]="form()" control="public_enabled" label="Make Committee Public" id="public-toggle"></lfx-toggle>
<lfx-toggle
size="small"
[form]="form()"
control="public_enabled"
label="Make Committee Public"
id="public-toggle"
tooltip="When enabled, the committee will be visible to the public on LFX tools like the public meeting calendar"
tooltipPosition="right"></lfx-toggle>

<!-- Conditional Public Name Field -->
<div *ngIf="form().get('public_enabled')?.value === true">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ export class CommitteeFormComponent {
sso_group_name: new FormControl(committee?.sso_group_name || ''),
committee_website: new FormControl(committee?.committee_website || '', [Validators.pattern(/^https?:\/\/.+\..+/)]),
project_uid: new FormControl(committee?.project_uid || ''),
joinable: new FormControl(committee?.joinable || false),
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,30 +103,56 @@ <h3 class="text-xl font-display font-semibold text-gray-900 mb-3">
}
</div>

@if (meeting().agenda) {
<div class="pb-3">
<!-- Meeting Attachments Template -->
@if (attachments().length > 0 || importantLinks().length > 0) {
<div class="bg-gray-50 p-4 border border-gray-100 mb-4 rounded-md shadow-sm">
<h4 class="text-sm font-semibold text-gray-700 mb-2 flex items-center gap-1">
<i class="fa-light fa-link text-blue-500"></i>
@if (attachments().length > 0) {
<span>Attachments</span>
@if (importantLinks().length > 0) {
<span>&</span>
}
}
@if (importantLinks().length > 0) {
<span>Important Links</span>
}
</h4>

<!-- Important Links -->
@if (importantLinks().length > 0) {
<div class="mb-4">
<h4 class="text-sm font-semibold text-gray-700 mb-2 flex items-center gap-2">
<i class="fa-light fa-link text-blue-500"></i>
Important Links
</h4>
<div class="flex flex-wrap gap-2">
@for (link of importantLinks(); track link.url) {
<a
[href]="link.url"
target="_blank"
rel="noopener noreferrer"
class="cursor-pointer inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-blue-600 bg-blue-50 hover:bg-blue-100 rounded-md transition-colors"
[title]="link.url">
<i class="fa-light fa-external-link"></i>
{{ link.domain }}
</a>
}
</div>
<div>
<div class="flex flex-wrap gap-2">
@for (attachment of attachments(); track attachment.id) {
<a
[href]="attachment.file_url"
target="_blank"
rel="noopener noreferrer"
class="cursor-pointer inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-gray-600 bg-blue-50 hover:bg-blue-100 rounded-md transition-colors"
[title]="attachment.file_name + ' (' + (attachment.file_size || 0 | fileSize) + ')'">
<i [class]="attachment.mime_type || '' | fileTypeIcon"></i>
<span class="truncate max-w-[120px]">{{ attachment.file_name }}</span>
<span class="text-gray-400 ml-1">({{ attachment.file_size || 0 | fileSize }})</span>
</a>
}

@for (link of importantLinks(); track link.url) {
<a
[href]="link.url"
target="_blank"
rel="noopener noreferrer"
class="cursor-pointer inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-blue-600 bg-blue-50 hover:bg-blue-100 rounded-md transition-colors"
[title]="link.url">
<i class="fa-light fa-link"></i>
{{ link.domain }}
</a>
}
</div>
}
</div>
</div>
}

@if (meeting().agenda) {
<div class="pb-3">
<lfx-expandable-text [maxHeight]="100">
<div class="flex flex-col gap-4">
<div [innerHTML]="meeting().agenda | linkify"></div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@
// SPDX-License-Identifier: MIT

import { CommonModule } from '@angular/common';
import { Component, computed, effect, inject, Injector, input, output, runInInjectionContext, signal, Signal, WritableSignal } from '@angular/core';
import { Component, computed, effect, inject, Injector, input, OnInit, output, runInInjectionContext, signal, Signal, WritableSignal } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { RouterLink } from '@angular/router';
import { FileSizePipe } from '@app/shared/pipes/file-size.pipe';
import { FileTypeIconPipe } from '@app/shared/pipes/file-type-icon.pipe';
import { LinkifyPipe } from '@app/shared/pipes/linkify.pipe';
import { AvatarComponent } from '@components/avatar/avatar.component';
import { BadgeComponent } from '@components/badge/badge.component';
import { ButtonComponent } from '@components/button/button.component';
import { ExpandableTextComponent } from '@components/expandable-text/expandable-text.component';
import { MenuComponent } from '@components/menu/menu.component';
import { extractUrlsWithDomains, Meeting, MeetingParticipant } from '@lfx-pcc/shared';
import { extractUrlsWithDomains, Meeting, MeetingAttachment, MeetingParticipant } from '@lfx-pcc/shared';
import { MeetingTimePipe } from '@pipes/meeting-time.pipe';
import { CommitteeService } from '@services/committee.service';
import { MeetingService } from '@services/meeting.service';
Expand Down Expand Up @@ -45,12 +47,14 @@ import { RecurringEditOption, RecurringMeetingEditOptionsComponent } from '../re
ConfirmDialogModule,
ExpandableTextComponent,
LinkifyPipe,
FileTypeIconPipe,
FileSizePipe,
],
providers: [ConfirmationService],
templateUrl: './meeting-card.component.html',
styleUrl: './meeting-card.component.scss',
})
export class MeetingCardComponent {
export class MeetingCardComponent implements OnInit {
private readonly projectService = inject(ProjectService);
private readonly meetingService = inject(MeetingService);
private readonly committeeService = inject(CommitteeService);
Expand All @@ -70,19 +74,26 @@ export class MeetingCardComponent {
public participantsLabel: Signal<string> = this.initParticipantsLabel();
public additionalParticipantsCount: WritableSignal<number> = signal(0);
public actionMenuItems: Signal<MenuItem[]> = this.initializeActionMenuItems();
public attachments: Signal<MeetingAttachment[]> = signal([]);

public readonly meetingDeleted = output<void>();
public readonly project = this.projectService.project;

// Extract important links from agenda
public readonly importantLinks = this.initImportantLinks();

// Meeting attachments

public constructor() {
effect(() => {
this.meeting.set(this.meetingInput());
});
}

public ngOnInit(): void {
this.attachments = this.initAttachments();
}

public onParticipantsToggle(event: Event): void {
event.stopPropagation();

Expand All @@ -95,8 +106,6 @@ export class MeetingCardComponent {
// Show/hide inline participants display
this.participantsLoading.set(true);

// Show/hide inline participants display
this.participantsLoading.set(true);
if (!this.showParticipants()) {
this.initParticipantsList();
}
Expand Down Expand Up @@ -406,4 +415,10 @@ export class MeetingCardComponent {
return extractUrlsWithDomains(agenda);
});
}

private initAttachments(): Signal<MeetingAttachment[]> {
return runInInjectionContext(this.injector, () => {
return toSignal(this.meetingService.getMeetingAttachments(this.meetingInput().id).pipe(catchError(() => of([]))), { initialValue: [] });
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,71 @@ <h3 class="text-lg font-medium text-gray-900">Meeting Settings</h3>
</div>
}

@if (!isEditing()) {
<div class="flex flex-col gap-2">
<div class="flex items-center gap-2">
<label for="demo[]" class="block text-sm font-medium text-gray-700">Upload Meeting Attachments</label>
<i
class="fa-light fa-info-circle text-gray-400 cursor-help"
pTooltip="Upload any meeting materials that will be shared with participants"
tooltipPosition="right"
tooltipStyleClass="text-xs max-w-xs"></i>
</div>
<lfx-file-upload
name="attachments[]"
[customUpload]="true"
(onSelect)="onFileSelect($event)"
[multiple]="true"
accept="image/*, application/pdf, application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document"
[maxFileSize]="10000000"
[showUploadButton]="false"
[showCancelButton]="true"
chooseStyleClass="p-button-sm"
cancelStyleClass="p-button-sm"
mode="advanced"
chooseLabel="Choose Files"
cancelLabel="Clear">
<ng-template #content>
<div class="text-sm flex items-center justify-center">Drag and drop files to here to upload.</div>
</ng-template>
</lfx-file-upload>

<!-- Show pending attachments -->
@if (pendingAttachments().length > 0) {
<div class="mt-4 space-y-2">
<h4 class="text-sm font-medium text-gray-900">Selected Files:</h4>
@for (attachment of pendingAttachments(); track attachment.id) {
<div class="flex items-center justify-between p-2 bg-gray-50 rounded-lg">
<div class="flex items-center space-x-2">
@if (attachment.uploading) {
<i class="pi pi-spin pi-spinner text-blue-500"></i>
} @else if (attachment.uploadError) {
<i class="pi pi-exclamation-triangle text-red-500"></i>
} @else {
<i class="pi pi-check-circle text-green-500"></i>
}
<span class="text-sm text-gray-700">{{ attachment.fileName }}</span>
<span class="text-xs text-gray-500">({{ attachment.fileSize / 1024 / 1024 | number: '1.1-1' }}MB)</span>
</div>
<div class="flex items-center space-x-2">
@if (attachment.uploading) {
<span class="text-xs text-blue-600">Uploading...</span>
} @else if (attachment.uploadError) {
<span class="text-xs text-red-600">{{ attachment.uploadError }}</span>
} @else {
<span class="text-xs text-green-600">Ready</span>
}
<button type="button" (click)="removePendingAttachment(attachment.id)" class="text-red-500 hover:text-red-700">
<i class="pi pi-times text-xs"></i>
</button>
</div>
</div>
}
</div>
}
</div>
}

<!-- Form Actions -->
<div class="flex justify-end gap-3 pt-6 border-t">
<lfx-button label="Cancel" severity="secondary" [outlined]="true" (click)="onCancel()" size="small" type="button"></lfx-button>
Expand Down
Loading