Skip to content

Commit 3cddbae

Browse files
authored
feat(my-activity): implement my activity module with votes and surveys (#225)
* feat(my-activity): implement my activity module with votes and surveys LFXV2-973 Implement the My Activity module for board-member persona to view and manage their votes and surveys participation. Module Structure: - New my-activity module with routes - Dashboard component with tab navigation (Votes/Surveys) - Activity top bar component with select button tabs Votes Table: - Table displaying user's assigned votes/polls - Combined status column (Open, Submitted, Closed) - Custom sorting: Open → Submitted → Closed (secondary: due date) - Relative due dates for open votes, actual dates for completed - Committee filter and search functionality - Action buttons (Vote Now / View Results) Surveys Table: - Table displaying user's assigned surveys - Combined status column (Open, Submitted, Closed) - Submitted On column showing response date - Custom sorting: Open → Submitted → Closed (secondary: due date) - Relative due dates for open surveys - Committee filter and search functionality - Action buttons (Take Survey / View Results) New Pipes (16 total): - can-vote, can-take-survey - Permission check pipes - combined-vote-status-label, combined-vote-status-severity - combined-survey-status-label, combined-survey-status-severity - poll-status-label, poll-status-severity - survey-status-label, survey-status-severity - survey-response-label, survey-response-severity - vote-response-label, vote-response-severity - vote-action-text, survey-action-text - relative-due-date, is-due-within-month Navigation Updates: - Added My Activity to sidebar navigation - Visible only for board-member persona Shared Package Updates: - Poll interfaces, enums, and constants - Survey interfaces, enums, and constants - My Activity interfaces and constants - CommitteeReference interface for lightweight committee data Signed-off-by: Asitha de Silva <asithade@gmail.com> * refactor(my-activity): extract combined status logic to shared utilities LFXV2-973 Extract duplicated getCombinedStatus logic into shared utility functions to ensure consistency and reduce maintenance burden. Changes: - Create poll.utils.ts with getCombinedVoteStatus and CombinedVoteStatus type - Create survey.utils.ts with getCombinedSurveyStatus and CombinedSurveyStatus type - Update combined-vote-status-label.pipe.ts to use shared utility - Update combined-vote-status-severity.pipe.ts to use shared utility - Update combined-survey-status-label.pipe.ts to use shared utility - Update combined-survey-status-severity.pipe.ts to use shared utility - Update votes-table.component.ts to use shared utility - Update surveys-table.component.ts to use shared utility Signed-off-by: Asitha de Silva <asithade@gmail.com> --------- Signed-off-by: Asitha de Silva <asithade@gmail.com>
1 parent e1cd74e commit 3cddbae

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1600
-16
lines changed

apps/lfx-one/src/app/app.routes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ export const routes: Routes = [
3131
path: 'mailing-lists',
3232
loadChildren: () => import('./modules/mailing-lists/mailing-lists.routes').then((m) => m.MAILING_LIST_ROUTES),
3333
},
34+
{
35+
path: 'my-activity',
36+
loadChildren: () => import('./modules/my-activity/my-activity.routes').then((m) => m.MY_ACTIVITY_ROUTES),
37+
},
3438
{
3539
path: 'settings',
3640
loadChildren: () => import('./modules/settings/settings.routes').then((m) => m.SETTINGS_ROUTES),

apps/lfx-one/src/app/layouts/main-layout/main-layout.component.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
66
import { NavigationEnd, Router, RouterModule } from '@angular/router';
77
import { SidebarComponent } from '@components/sidebar/sidebar.component';
88
import { environment } from '@environments/environment';
9-
import { COMMITTEE_LABEL, MAILING_LIST_LABEL } from '@lfx-one/shared/constants';
9+
import { COMMITTEE_LABEL, MAILING_LIST_LABEL, MY_ACTIVITY_LABEL } from '@lfx-one/shared/constants';
1010
import { SidebarMenuItem } from '@lfx-one/shared/interfaces';
1111
import { AppService } from '@services/app.service';
1212
import { FeatureFlagService } from '@services/feature-flag.service';
@@ -56,26 +56,38 @@ export class MainLayoutComponent {
5656
icon: 'fa-light fa-envelope',
5757
routerLink: '/mailing-lists',
5858
},
59+
{
60+
label: MY_ACTIVITY_LABEL.singular,
61+
icon: 'fa-light fa-clipboard-list',
62+
routerLink: '/my-activity',
63+
},
5964
{
6065
label: 'Projects',
6166
icon: 'fa-light fa-folder-open',
6267
routerLink: '/projects',
6368
},
6469
];
6570

66-
// Computed sidebar items based on feature flags
71+
// Computed sidebar items based on feature flags and persona
6772
protected readonly sidebarItems = computed(() => {
6873
let items = [...this.baseSidebarItems];
74+
const isBoardMember = this.personaService.currentPersona() === 'board-member';
6975

7076
// Filter out Projects if feature flag is disabled
7177
if (!this.showProjectsInSidebar()) {
7278
items = items.filter((item) => item.label !== 'Projects');
7379
}
7480

75-
if (this.personaService.currentPersona() === 'board-member') {
81+
// Hide Committees for board-member persona
82+
if (isBoardMember) {
7683
items = items.filter((item) => item.label !== COMMITTEE_LABEL.plural);
7784
}
7885

86+
// My Activity is only visible for board-member persona
87+
if (!isBoardMember) {
88+
items = items.filter((item) => item.label !== MY_ACTIVITY_LABEL.singular);
89+
}
90+
7991
return items;
8092
});
8193

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<!-- Copyright The Linux Foundation and each contributor to LFX. -->
2+
<!-- SPDX-License-Identifier: MIT -->
3+
4+
<div class="flex items-center justify-between mb-6" data-testid="my-activity-top-bar">
5+
<lfx-select-button
6+
[form]="tabForm()"
7+
control="tab"
8+
[options]="tabOptions()"
9+
optionLabel="label"
10+
optionValue="value"
11+
size="small"
12+
class="w-full"
13+
styleClass="w-full"
14+
(onChange)="onTabChange($event)"
15+
data-testid="my-activity-tab-selector">
16+
</lfx-select-button>
17+
</div>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright The Linux Foundation and each contributor to LFX.
2+
// SPDX-License-Identifier: MIT
3+
4+
import { Component, input, output } from '@angular/core';
5+
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
6+
import { SelectButtonComponent } from '@components/select-button/select-button.component';
7+
import { MyActivityTab } from '@lfx-one/shared/interfaces';
8+
9+
interface TabOption {
10+
label: string;
11+
value: MyActivityTab;
12+
}
13+
14+
@Component({
15+
selector: 'lfx-activity-top-bar',
16+
imports: [ReactiveFormsModule, SelectButtonComponent],
17+
templateUrl: './activity-top-bar.component.html',
18+
})
19+
export class ActivityTopBarComponent {
20+
public tabForm = input.required<FormGroup>();
21+
public tabOptions = input.required<TabOption[]>();
22+
23+
public readonly tabChange = output<MyActivityTab>();
24+
25+
protected onTabChange(event: { value: MyActivityTab }): void {
26+
this.tabChange.emit(event.value);
27+
}
28+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
<!-- Copyright The Linux Foundation and each contributor to LFX. -->
2+
<!-- SPDX-License-Identifier: MIT -->
3+
4+
<lfx-card>
5+
<div class="pb-2 mb-2">
6+
<div class="flex flex-col md:flex-row md:items-center gap-3 md:gap-4">
7+
<div class="flex-1 min-w-0">
8+
<lfx-input-text
9+
[form]="searchForm"
10+
control="search"
11+
placeholder="Search surveys..."
12+
icon="fa-light fa-search"
13+
styleClass="w-full"
14+
size="small"
15+
data-testid="my-activity-surveys-search-input"></lfx-input-text>
16+
</div>
17+
18+
<div class="w-full sm:w-auto sm:shrink-0">
19+
<lfx-select
20+
[form]="searchForm"
21+
control="status"
22+
size="small"
23+
[options]="statusOptions()"
24+
placeholder="Status"
25+
[showClear]="true"
26+
(valueChange)="onStatusChange($event)"
27+
data-testid="my-activity-surveys-status-filter"></lfx-select>
28+
</div>
29+
30+
<div class="w-full sm:w-auto sm:shrink-0">
31+
<lfx-select
32+
[form]="searchForm"
33+
control="committee"
34+
size="small"
35+
[options]="committeeOptions()"
36+
placeholder="Committee"
37+
[showClear]="true"
38+
(valueChange)="onCommitteeChange($event)"
39+
data-testid="my-activity-surveys-committee-filter"></lfx-select>
40+
</div>
41+
</div>
42+
</div>
43+
44+
<lfx-table
45+
[value]="filteredSurveys()"
46+
[paginator]="filteredSurveys().length > 10"
47+
[rows]="10"
48+
[rowsPerPageOptions]="[10, 25, 50]"
49+
[customSort]="true"
50+
data-testid="my-activity-surveys-table">
51+
<ng-template #header>
52+
<tr>
53+
<th>Survey</th>
54+
<th>Group</th>
55+
<th>Submitted On</th>
56+
<th>Status</th>
57+
<th>Due</th>
58+
<th></th>
59+
</tr>
60+
</ng-template>
61+
62+
<ng-template #body let-survey>
63+
<tr [attr.data-testid]="'my-activity-survey-row-' + survey.survey_id">
64+
<td>
65+
<a href="#" class="text-blue-600 hover:text-blue-700 hover:underline font-medium" [attr.data-testid]="'my-activity-survey-name-' + survey.survey_id">
66+
{{ survey.survey_title }}
67+
</a>
68+
</td>
69+
70+
<td>
71+
<div class="flex flex-wrap gap-1">
72+
@for (committee of survey.committees; track committee.uid) {
73+
<span class="bg-blue-100 text-blue-500 text-xs px-2 py-1 rounded">
74+
{{ committee.name || committee.uid }}
75+
</span>
76+
}
77+
</div>
78+
</td>
79+
80+
<td>
81+
@if (survey.response_datetime) {
82+
<span>{{ survey.response_datetime | date: 'MMM d, y' }}</span>
83+
} @else {
84+
<span class="text-gray-400"></span>
85+
}
86+
</td>
87+
88+
<td>
89+
<lfx-tag [value]="survey | combinedSurveyStatusLabel" [severity]="survey | combinedSurveyStatusSeverity"></lfx-tag>
90+
</td>
91+
92+
<td>
93+
@if (survey | canTakeSurvey) {
94+
<span
95+
[class.text-green-500]="survey.survey_cutoff_date | isDueWithinMonth"
96+
[pTooltip]="(survey.survey_cutoff_date | isDueWithinMonth) ? (survey.survey_cutoff_date | date: 'MMMM d, y') || '' : ''"
97+
tooltipPosition="top">
98+
{{ survey.survey_cutoff_date | relativeDueDate }}
99+
</span>
100+
} @else {
101+
<span>{{ survey.survey_cutoff_date | date: 'MMM d, y' }}</span>
102+
}
103+
</td>
104+
105+
<td>
106+
<div class="flex items-center justify-end">
107+
<lfx-button
108+
[label]="survey | surveyActionText"
109+
[severity]="(survey | canTakeSurvey) ? 'primary' : 'secondary'"
110+
size="small"
111+
[outlined]="true"
112+
[attr.data-testid]="'my-activity-survey-action-' + survey.survey_id">
113+
</lfx-button>
114+
</div>
115+
</td>
116+
</tr>
117+
</ng-template>
118+
119+
<ng-template #emptymessage>
120+
<tr>
121+
<td colspan="6" class="text-center py-8">
122+
<i class="fa-light fa-clipboard-list text-3xl text-gray-400 mb-2"></i>
123+
<p class="text-sm text-gray-500">No surveys found</p>
124+
</td>
125+
</tr>
126+
</ng-template>
127+
</lfx-table>
128+
</lfx-card>

0 commit comments

Comments
 (0)