Skip to content

Commit 30a4801

Browse files
committed
feat(meetings): lens-aware dashboard meeting layout
Rewrite my-meetings component with last/next meeting layout and lens-aware data switching (me=user, project=all, org=hidden). Backend: - Reverse-registrant lookup for upcoming meetings via ITX - Participant-based lookup for past meetings via composite IDs - Normalize email, restore board meeting type filter - Extract shared fetchByIdFilterAndLimit helper - New GET /api/user/past-meetings endpoint Frontend: - All 4 persona dashboards: lens-conditional visibility - Dashboard meeting card: recording button, same-tab nav - Pending actions: two-column grid layout - Maintainer header: lens-aware per PR #391 pattern - Dev toolbar: sync form on external persona changes - Extract initLensSwitchedData generic helper Signed-off-by: Asitha de Silva <asithade@gmail.com>
1 parent 1fc9597 commit 30a4801

19 files changed

Lines changed: 366 additions & 273 deletions

apps/lfx-one/src/app/layouts/dev-toolbar/dev-toolbar.component.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// SPDX-License-Identifier: MIT
33

44
import { Component, computed, inject, signal, Signal } from '@angular/core';
5-
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
5+
import { takeUntilDestroyed, toObservable, toSignal } from '@angular/core/rxjs-interop';
66
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
77
import { NavigationEnd, Router } from '@angular/router';
88
import { ButtonComponent } from '@components/button/button.component';
@@ -14,7 +14,7 @@ import { CookieRegistryService } from '@services/cookie-registry.service';
1414
import { FeatureFlagService } from '@services/feature-flag.service';
1515
import { PersonaService } from '@services/persona.service';
1616
import { ProjectContextService } from '@services/project-context.service';
17-
import { filter, map, startWith } from 'rxjs';
17+
import { filter, map, skip, startWith } from 'rxjs';
1818

1919
@Component({
2020
selector: 'lfx-dev-toolbar',
@@ -56,10 +56,7 @@ export class DevToolbarComponent {
5656
// Find the initial preset matching the current persona
5757
const currentPersona = this.personaService.currentPersona();
5858
const allPersonas = this.personaService.allPersonas();
59-
const initialPreset =
60-
DEV_PERSONA_PRESETS.find(
61-
(p) => p.primary === currentPersona && p.personas.length === allPersonas.length && p.personas.every((persona) => allPersonas.includes(persona))
62-
) ?? DEV_PERSONA_PRESETS[1];
59+
const initialPreset = this.findMatchingPreset(currentPersona, allPersonas);
6360
this.activePreset.set(initialPreset);
6461

6562
this.form = new FormGroup({
@@ -119,6 +116,17 @@ export class DevToolbarComponent {
119116
}
120117
});
121118

119+
// Sync form when persona changes externally (e.g., after SSR persona detection)
120+
toObservable(this.personaService.currentPersona)
121+
.pipe(skip(1), takeUntilDestroyed())
122+
.subscribe((persona) => {
123+
const matchingPreset = this.findMatchingPreset(persona, this.personaService.allPersonas());
124+
if (matchingPreset.value !== this.form.get('persona')?.value) {
125+
this.activePreset.set(matchingPreset);
126+
this.form.get('persona')?.setValue(matchingPreset.value, { emitEvent: false });
127+
}
128+
});
129+
122130
// Subscribe to project/foundation selection changes
123131
this.form
124132
.get('selectedProjectUid')
@@ -147,6 +155,16 @@ export class DevToolbarComponent {
147155
}
148156
}
149157

158+
private findMatchingPreset(persona: string, allPersonas: string[]): DevPersonaPreset {
159+
return (
160+
DEV_PERSONA_PRESETS.find(
161+
(p) => p.primary === persona && p.personas.length === allPersonas.length && p.personas.every((pp) => allPersonas.includes(pp))
162+
) ??
163+
DEV_PERSONA_PRESETS.find((p) => p.primary === persona) ??
164+
DEV_PERSONA_PRESETS[1]
165+
);
166+
}
167+
150168
private initIsOnBoardDashboard(): Signal<boolean> {
151169
return toSignal(
152170
this.router.events.pipe(

apps/lfx-one/src/app/modules/dashboards/board-member/board-member-dashboard.component.html

Lines changed: 51 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,10 @@ <h1>{{ selectedFoundation()?.name }} Overview</h1>
4848
</section>
4949
}
5050

51-
<!-- Middle Row - Two Cards -->
52-
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
53-
<!-- My Meetings -->
51+
<!-- Meetings (hidden on org lens) -->
52+
@if (showMeetings()) {
5453
@defer (on idle) {
55-
<lfx-my-meetings class="h-full" />
54+
<lfx-my-meetings />
5655
} @placeholder {
5756
<section class="flex flex-col flex-1" data-testid="dashboard-my-meetings-placeholder">
5857
<div class="flex items-center justify-between mb-4 h-8">
@@ -67,60 +66,62 @@ <h1>{{ selectedFoundation()?.name }} Overview</h1>
6766
</div>
6867
</section>
6968
}
69+
}
7070

71-
<!-- Pending Actions -->
72-
@defer (on idle) {
73-
<lfx-pending-actions class="h-full" [pendingActions]="boardMemberActions()" (actionClick)="handleActionClick()" />
71+
<!-- Pending Actions -->
72+
@defer (on idle) {
73+
<lfx-pending-actions [pendingActions]="boardMemberActions()" (actionClick)="handleActionClick()" />
74+
} @placeholder {
75+
<section class="flex flex-col flex-1" data-testid="dashboard-pending-actions-placeholder">
76+
<div class="flex items-center justify-between mb-4 px-2 h-8">
77+
<div class="flex items-center gap-2">
78+
<p-skeleton width="1.125rem" height="1.125rem" borderRadius="4px" />
79+
<p-skeleton width="9rem" height="1rem" />
80+
</div>
81+
</div>
82+
<div class="flex flex-col gap-3 px-2">
83+
<p-skeleton width="100%" height="140px" />
84+
</div>
85+
</section>
86+
}
87+
88+
<!-- Organization Involvement (hidden on me lens) -->
89+
@if (showOrgInvolvement()) {
90+
@defer (on viewport) {
91+
<lfx-organization-involvement />
7492
} @placeholder {
75-
<section class="flex flex-col flex-1" data-testid="dashboard-pending-actions-placeholder">
76-
<div class="flex items-center justify-between mb-4 px-2 h-8">
93+
<section data-testid="organization-involvement-placeholder">
94+
<div class="flex flex-wrap gap-3 items-end justify-between mb-4">
95+
<div class="flex flex-col gap-3">
96+
<p-skeleton width="14rem" height="1.5rem" />
97+
<p-skeleton width="16rem" height="1.5rem" />
98+
</div>
7799
<div class="flex items-center gap-2">
78-
<p-skeleton width="1.125rem" height="1.125rem" borderRadius="4px" />
79-
<p-skeleton width="9rem" height="1rem" />
100+
<p-skeleton width="2rem" height="2rem" borderRadius="4px" />
101+
<p-skeleton width="2rem" height="2rem" borderRadius="4px" />
80102
</div>
81103
</div>
82-
<div class="flex flex-col gap-3 px-2">
83-
<p-skeleton width="100%" height="140px" />
104+
<div class="flex gap-4 overflow-hidden">
105+
@for (i of [1, 2, 3]; track i) {
106+
<div class="p-4 bg-white border border-gray-200 rounded-lg flex-shrink-0 w-[calc(100vw-3rem)] md:w-80">
107+
<div class="flex flex-col gap-2">
108+
<div class="flex items-center gap-2">
109+
<p-skeleton width="1rem" height="1rem" borderRadius="4px" />
110+
<p-skeleton width="6rem" height="0.875rem" />
111+
</div>
112+
<div class="mt-3 w-full h-16">
113+
<p-skeleton width="100%" height="100%" borderRadius="8px" />
114+
</div>
115+
<div class="flex flex-col gap-1 mt-auto">
116+
<p-skeleton width="3rem" height="1.75rem" />
117+
<p-skeleton width="70%" height="0.75rem" />
118+
</div>
119+
</div>
120+
</div>
121+
}
84122
</div>
85123
</section>
86124
}
87-
</div>
88-
89-
<!-- Organization Involvement - Full Width (below fold) -->
90-
@defer (on viewport) {
91-
<lfx-organization-involvement />
92-
} @placeholder {
93-
<section data-testid="organization-involvement-placeholder">
94-
<div class="flex flex-wrap gap-3 items-end justify-between mb-4">
95-
<div class="flex flex-col gap-3">
96-
<p-skeleton width="14rem" height="1.5rem" />
97-
<p-skeleton width="16rem" height="1.5rem" />
98-
</div>
99-
<div class="flex items-center gap-2">
100-
<p-skeleton width="2rem" height="2rem" borderRadius="4px" />
101-
<p-skeleton width="2rem" height="2rem" borderRadius="4px" />
102-
</div>
103-
</div>
104-
<div class="flex gap-4 overflow-hidden">
105-
@for (i of [1, 2, 3]; track i) {
106-
<div class="p-4 bg-white border border-gray-200 rounded-lg flex-shrink-0 w-[calc(100vw-3rem)] md:w-80">
107-
<div class="flex flex-col gap-2">
108-
<div class="flex items-center gap-2">
109-
<p-skeleton width="1rem" height="1rem" borderRadius="4px" />
110-
<p-skeleton width="6rem" height="0.875rem" />
111-
</div>
112-
<div class="mt-3 w-full h-16">
113-
<p-skeleton width="100%" height="100%" borderRadius="8px" />
114-
</div>
115-
<div class="flex flex-col gap-1 mt-auto">
116-
<p-skeleton width="3rem" height="1.75rem" />
117-
<p-skeleton width="70%" height="0.75rem" />
118-
</div>
119-
</div>
120-
</div>
121-
}
122-
</div>
123-
</section>
124125
}
125126
</div>
126127
</div>

apps/lfx-one/src/app/modules/dashboards/board-member/board-member-dashboard.component.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Component, computed, inject, Signal } from '@angular/core';
55
import { takeUntilDestroyed, toObservable, toSignal } from '@angular/core/rxjs-interop';
66
import { PendingActionItem } from '@lfx-one/shared/interfaces';
77
import { HiddenActionsService } from '@services/hidden-actions.service';
8+
import { LensService } from '@services/lens.service';
89
import { ProjectContextService } from '@services/project-context.service';
910
import { ProjectService } from '@services/project.service';
1011
import { SkeletonModule } from 'primeng/skeleton';
@@ -25,6 +26,10 @@ export class BoardMemberDashboardComponent {
2526
private readonly projectContextService = inject(ProjectContextService);
2627
private readonly projectService = inject(ProjectService);
2728
private readonly hiddenActionsService = inject(HiddenActionsService);
29+
private readonly lensService = inject(LensService);
30+
31+
protected readonly showMeetings = computed(() => this.lensService.activeLens() !== 'org');
32+
protected readonly showOrgInvolvement = computed(() => this.lensService.activeLens() !== 'me');
2833

2934
public readonly selectedFoundation = computed(() => this.projectContextService.selectedFoundation());
3035
public readonly selectedProject = computed(() => this.projectContextService.selectedProject() || this.projectContextService.selectedFoundation());

apps/lfx-one/src/app/modules/dashboards/components/dashboard-meeting-card/dashboard-meeting-card.component.html

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,18 +131,47 @@
131131
<div class="flex gap-2">
132132
<!-- See Meeting button - full width when alone, flex-1 when both buttons present -->
133133
@if (showDetailsButton()) {
134+
@if (openDetailsInNewTab()) {
135+
<lfx-button
136+
class="w-full"
137+
severity="secondary"
138+
size="small"
139+
styleClass="w-full"
140+
label="See Meeting Details"
141+
[href]="meetingDetailUrl()"
142+
target="_blank"
143+
type="button"
144+
[outlined]="true"
145+
icon="fa-light fa-up-right-from-square text-xs"
146+
data-testid="dashboard-meeting-card-see-button" />
147+
} @else {
148+
<lfx-button
149+
class="w-full"
150+
severity="secondary"
151+
size="small"
152+
styleClass="w-full"
153+
label="See Meeting Details"
154+
[routerLink]="meetingDetailUrl()"
155+
type="button"
156+
[outlined]="true"
157+
data-testid="dashboard-meeting-card-see-button" />
158+
}
159+
}
160+
161+
<!-- See Recording button -->
162+
@if (recordingShareUrl()) {
134163
<lfx-button
135164
class="w-full"
136165
severity="secondary"
137166
size="small"
138167
styleClass="w-full"
139-
label="See Meeting Details"
140-
[href]="meetingDetailUrl()"
168+
label="See Recording"
169+
[href]="recordingShareUrl()!"
141170
target="_blank"
142171
type="button"
143172
[outlined]="true"
144-
icon="fa-light fa-up-right-from-square text-xs"
145-
data-testid="dashboard-meeting-card-see-button" />
173+
icon="fa-light fa-play text-xs"
174+
data-testid="dashboard-meeting-card-recording-button" />
146175
}
147176

148177
<!-- Join Meeting button - right side when present -->

apps/lfx-one/src/app/modules/dashboards/components/dashboard-meeting-card/dashboard-meeting-card.component.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
MEETING_TYPE_CONFIGS,
1616
MeetingOccurrence,
1717
MeetingTypeBadge,
18+
PastMeetingRecording,
1819
TagSeverity,
1920
} from '@lfx-one/shared';
2021
import { RecurrenceSummaryPipe } from '@pipes/recurrence-summary.pipe';
@@ -40,6 +41,8 @@ export class DashboardMeetingCardComponent {
4041
public readonly detailUrl = input<string | null>(null);
4142
/** Set to false to hide the "See Meeting Details" button (e.g. for past meetings where the detail page is inaccessible). */
4243
public readonly showDetailsButton = input<boolean>(true);
44+
/** Set to false to open the details link in the same tab instead of a new tab. */
45+
public readonly openDetailsInNewTab = input<boolean>(true);
4346

4447
public readonly attachments: Signal<MeetingAttachment[]> = this.initAttachments();
4548
public readonly joinUrl: Signal<string | null>;
@@ -60,6 +63,7 @@ export class DashboardMeetingCardComponent {
6063
public readonly meetingTitle: Signal<string> = this.initMeetingTitle();
6164
public readonly isRecurring: Signal<boolean> = this.initIsRecurring();
6265
public readonly meetingDetailUrl: Signal<string> = this.initMeetingDetailUrl();
66+
public readonly recordingShareUrl: Signal<string | null> = this.initRecordingShareUrl();
6367

6468
public constructor() {
6569
const meeting$ = toObservable(this.meeting);
@@ -274,6 +278,33 @@ export class DashboardMeetingCardComponent {
274278
});
275279
}
276280

281+
private initRecordingShareUrl(): Signal<string | null> {
282+
return toSignal(
283+
toObservable(this.meeting).pipe(
284+
switchMap((meeting) => {
285+
if (!meeting?.id || !meeting.recording_enabled) {
286+
return of(null);
287+
}
288+
// Skip for upcoming meetings — no recording exists yet
289+
if (new Date(meeting.start_time).getTime() > Date.now()) {
290+
return of(null);
291+
}
292+
return this.meetingService.getPastMeetingRecording(meeting.id).pipe(
293+
map((recording: PastMeetingRecording | null) => {
294+
if (!recording?.sessions?.length) {
295+
return null;
296+
}
297+
const largest = recording.sessions.reduce((a, b) => ((a.total_size || 0) >= (b.total_size || 0) ? a : b));
298+
return largest.share_url || null;
299+
}),
300+
catchError(() => of(null))
301+
);
302+
})
303+
),
304+
{ initialValue: null }
305+
);
306+
}
307+
277308
private initAttachments(): Signal<MeetingAttachment[]> {
278309
return toSignal(
279310
toObservable(this.meeting).pipe(

apps/lfx-one/src/app/modules/dashboards/components/my-meetings/my-meetings.component.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,13 @@ <h3 class="text-sm font-normal text-gray-500 flex items-center gap-1.5">
4141
</div>
4242
</lfx-card>
4343
} @else if (lastMeeting(); as meeting) {
44-
<lfx-dashboard-meeting-card [meeting]="meeting" [detailUrl]="'/meetings/' + meeting.meeting_id + '/details'" />
44+
<lfx-dashboard-meeting-card [meeting]="meeting" [detailUrl]="'/meetings/' + meeting.id + '/details'" [openDetailsInNewTab]="false" />
4545
} @else {
4646
<lfx-card>
4747
<div class="flex flex-col items-center py-6">
4848
<i class="fa-light fa-clock-rotate-left text-2xl text-gray-300 mb-2"></i>
4949
<p class="text-sm font-medium text-gray-500">No past meetings</p>
50-
<p class="text-xs text-gray-400 mt-1">Your past meetings will appear here.</p>
50+
<p class="text-xs text-gray-400 mt-1">{{ emptyPastText() }}</p>
5151
</div>
5252
</lfx-card>
5353
}
@@ -79,7 +79,7 @@ <h3 class="text-sm font-normal text-gray-500 flex items-center gap-1.5">
7979
<div class="flex flex-col items-center py-6">
8080
<i class="fa-light fa-calendar-check text-2xl text-gray-300 mb-2"></i>
8181
<p class="text-sm font-medium text-gray-500">No upcoming meetings</p>
82-
<p class="text-xs text-gray-400 mt-1">Your scheduled meetings will appear here.</p>
82+
<p class="text-xs text-gray-400 mt-1">{{ emptyUpcomingText() }}</p>
8383
</div>
8484
</lfx-card>
8585
}

0 commit comments

Comments
 (0)