Skip to content

Commit 7d5d8e8

Browse files
authored
feat(meetings): add v1 legacy meeting support for migration (#190)
* feat(meetings): add v1 legacy meeting support for migration - Add QueryServiceMeetingType enum for v1/v2 meeting types - Extend Meeting interface with V1 legacy fields - Add getEarlyJoinTimeMinutes() utility for V1/V2 compatibility - Enhanced buildJoinUrlWithParams() with options for name/organization - Update backend services to handle V1 meeting types - Add V1/V2 fallback signals in frontend components - Fix auth middleware token refresh for optional routes - Remove unused getPastMeetingById endpoint LFXV2-853 LFXV2-854 LFXV2-855 Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Asitha de Silva <[email protected]> * fix(meetings): improve v1 legacy meeting error handling and cleanup - Add ResourceNotFoundError when v1 meeting resources are empty - Use getEarlyJoinTimeMinutes() utility for early join time calculation - Remove unused meeting_id field from Meeting interface - Fix minor comment typo in public meeting controller LFXV2-853 🤖 Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Asitha de Silva <[email protected]> --------- Signed-off-by: Asitha de Silva <[email protected]>
1 parent f62101e commit 7d5d8e8

35 files changed

+1037
-399
lines changed

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

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,8 @@ <h4 class="line-clamp-2 text-sm leading-tight font-medium text-gray-900" data-te
9898
<lfx-button
9999
class="w-full"
100100
label="See Meeting Details"
101-
[routerLink]="['/meetings', meeting().uid]"
102-
[queryParams]="{ password: meeting().password || '' }"
101+
[routerLink]="meetingDetailRouterLink()"
102+
[queryParams]="meetingDetailQueryParams()"
103103
target="_blank"
104104
type="button"
105105
rel="noopener noreferrer"
@@ -130,11 +130,9 @@ <h4 class="line-clamp-2 text-sm leading-tight font-medium text-gray-900" data-te
130130
size="small"
131131
class="w-full"
132132
label="Join Meeting"
133-
[routerLink]="['/meetings', meeting().uid]"
134-
[queryParams]="{ password: meeting().password || '' }"
133+
[routerLink]="meetingDetailRouterLink()"
134+
[queryParams]="meetingDetailQueryParams()"
135135
target="_blank"
136-
type="button"
137-
type="button"
138136
rel="noopener noreferrer"
139137
icon="fa-light fa-video"
140138
styleClass="w-full bg-emerald-500 hover:bg-emerald-600 text-white h-8 text-sm font-semibold"

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

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -140,22 +140,53 @@ export class DashboardMeetingCardComponent {
140140
return this.meeting().transcript_enabled === true;
141141
});
142142

143+
// TODO(v1-migration): Simplify to use V2 fields only once all meetings are migrated to V2
143144
public readonly hasAiSummary: Signal<boolean> = computed(() => {
144-
return this.meeting().zoom_config?.ai_companion_enabled === true;
145+
const meeting = this.meeting();
146+
// V2: zoom_config.ai_companion_enabled, V1: zoom_ai_enabled
147+
return meeting.zoom_config?.ai_companion_enabled === true || meeting.zoom_ai_enabled === true;
145148
});
146149

150+
// TODO(v1-migration): Simplify to use V2 fields only once all meetings are migrated to V2
147151
public readonly meetingTitle: Signal<string> = computed(() => {
148152
const occurrence = this.occurrence();
149153
const meeting = this.meeting();
150154

151-
// Use occurrence title if available, otherwise use meeting title
152-
return occurrence?.title || meeting.title;
155+
// Priority: occurrence title > meeting title > meeting topic (v1)
156+
return occurrence?.title || meeting.title || meeting.topic || '';
153157
});
154158

155159
public readonly canJoinMeeting: Signal<boolean> = computed(() => {
156160
return canJoinMeeting(this.meeting(), this.occurrence());
157161
});
158162

163+
// TODO(v1-migration): Remove once all meetings are migrated to V2
164+
public readonly isLegacyMeeting: Signal<boolean> = computed(() => {
165+
return this.meeting().version === 'v1';
166+
});
167+
168+
// TODO(v1-migration): Simplify to use V2 uid only once all meetings are migrated to V2
169+
public readonly meetingDetailRouterLink: Signal<string[]> = computed(() => {
170+
const meeting = this.meeting();
171+
const identifier = this.isLegacyMeeting() && meeting.id ? meeting.id : meeting.uid;
172+
return ['/meetings', identifier];
173+
});
174+
175+
// TODO(v1-migration): Remove V1 parameter handling once all meetings are migrated to V2
176+
public readonly meetingDetailQueryParams: Signal<Record<string, string>> = computed(() => {
177+
const meeting = this.meeting();
178+
const params: Record<string, string> = {};
179+
180+
if (meeting.password) {
181+
params['password'] = meeting.password;
182+
}
183+
if (this.isLegacyMeeting()) {
184+
params['v1'] = 'true';
185+
}
186+
187+
return params;
188+
});
189+
159190
public constructor() {
160191
// Convert meeting input signal to observable and create reactive attachment stream
161192
const meeting$ = toObservable(this.meeting);
@@ -170,13 +201,20 @@ export class DashboardMeetingCardComponent {
170201

171202
this.attachments = toSignal(attachments$, { initialValue: [] });
172203

204+
// TODO(v1-migration): Remove V1 join URL handling once all meetings are migrated to V2
173205
// Convert user signal to observable and create reactive join URL stream
174206
const user$ = toObservable(this.userService.user);
175207
const authenticated$ = toObservable(this.userService.authenticated);
208+
const isLegacyMeeting$ = toObservable(this.isLegacyMeeting);
209+
210+
const joinUrl$ = combineLatest([meeting$, user$, authenticated$, isLegacyMeeting$]).pipe(
211+
switchMap(([meeting, user, authenticated, isLegacy]) => {
212+
// For v1 meetings, use the join_url directly from the meeting object
213+
if (isLegacy && meeting.join_url && this.canJoinMeeting()) {
214+
return of(meeting.join_url);
215+
}
176216

177-
const joinUrl$ = combineLatest([meeting$, user$, authenticated$]).pipe(
178-
switchMap(([meeting, user, authenticated]) => {
179-
// Only fetch join URL for meetings that can be joined with authenticated users
217+
// For v2 meetings, fetch join URL from API for authenticated users
180218
if (meeting.uid && authenticated && user?.email && this.canJoinMeeting()) {
181219
return this.meetingService.getPublicMeetingJoinUrl(meeting.uid, meeting.password, { email: user.email }).pipe(
182220
map((res) => buildJoinUrlWithParams(res.join_url, user)),

apps/lfx-one/src/app/modules/dashboards/components/pending-actions/pending-actions.component.html

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ <h2 class="py-1 flex items-center gap-2">
1616
<div class="flex flex-col gap-3" data-testid="dashboard-pending-actions-list">
1717
@for (item of pendingActions(); track $index) {
1818
<div
19-
class="p-4 border rounded-lg shadow-md"
19+
class="p-4 border rounded-lg shadow-md space-y-3"
2020
[ngClass]="{
2121
'bg-yellow-50 hover:border-yellow-300': item.color === 'amber',
2222
'bg-blue-200 hover:border-blue-300': item.color === 'blue',
@@ -25,7 +25,7 @@ <h2 class="py-1 flex items-center gap-2">
2525
}"
2626
[attr.data-testid]="'dashboard-pending-actions-item-' + item.type">
2727
<!-- Header with Type -->
28-
<div class="flex items-start justify-between mb-3">
28+
<div class="flex items-center gap-2 flex-wrap">
2929
<div
3030
class="flex items-center gap-2 px-2 py-1 rounded-md text-xs"
3131
[ngClass]="{
@@ -57,36 +57,45 @@ <h2 class="py-1 flex items-center gap-2">
5757
</div>
5858

5959
<!-- Action Text -->
60-
<div class="mb-4">
61-
<p class="text-base font-normal">
62-
{{ item.text }}
63-
</p>
60+
<div class="flex items-start justify-between gap-2">
61+
<div class="flex flex-col gap-1">
62+
<p class="text-sm leading-tight font-medium text-gray-900">
63+
{{ item.text }}
64+
</p>
65+
@if (item.date) {
66+
<div class="flex items-center gap-2 flex-wrap mt-0.5">
67+
<span class="text-xs text-gray-600 whitespace-nowrap">{{ item.date }}</span>
68+
</div>
69+
}
70+
</div>
6471
</div>
6572

6673
<!-- Action Button -->
67-
@if (item.buttonLink) {
68-
<lfx-button
69-
size="small"
70-
class="w-full"
71-
styleClass="w-full text-sm"
72-
severity="secondary"
73-
rel="noopener noreferrer"
74-
[link]="true"
75-
[href]="item.buttonLink"
76-
target="_blank"
77-
(onClick)="handleActionClick(item)"
78-
[label]="item.buttonText">
79-
</lfx-button>
80-
} @else {
81-
<lfx-button
82-
size="small"
83-
class="w-full text-sm"
84-
styleClass="w-full text-sm"
85-
severity="secondary"
86-
(onClick)="handleActionClick(item)"
87-
[label]="item.buttonText">
88-
</lfx-button>
89-
}
74+
<div class="flex gap-2">
75+
@if (item.buttonLink) {
76+
<lfx-button
77+
size="small"
78+
class="w-full"
79+
styleClass="w-full text-sm"
80+
severity="secondary"
81+
rel="noopener noreferrer"
82+
[link]="true"
83+
[href]="item.buttonLink"
84+
target="_blank"
85+
(onClick)="handleActionClick(item)"
86+
[label]="item.buttonText">
87+
</lfx-button>
88+
} @else {
89+
<lfx-button
90+
size="small"
91+
class="w-full text-sm"
92+
styleClass="w-full text-sm"
93+
severity="secondary"
94+
(onClick)="handleActionClick(item)"
95+
[label]="item.buttonText">
96+
</lfx-button>
97+
}
98+
</div>
9099
</div>
91100
}
92101
</div>

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

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
data-testid="share-meeting-button"
5151
tooltip="Share Meeting"></lfx-button>
5252
}
53-
@if (meeting().project_slug && meeting().organizer && !pastMeeting()) {
53+
@if (meeting().project_slug && meeting().organizer && !pastMeeting() && !isLegacyMeeting()) {
5454
<lfx-button
5555
icon="fa-light fa-edit"
5656
[text]="true"
@@ -73,7 +73,7 @@
7373
meeting().committees && meeting().committees!.length > 0 ? 'Manage ' + committeeLabel.plural : 'Connect ' + committeeLabel.plural
7474
"></lfx-button>
7575
}
76-
@if (meeting().organizer) {
76+
@if (meeting().organizer && !isLegacyMeeting()) {
7777
<lfx-button
7878
icon="fa-light fa-trash"
7979
[text]="true"
@@ -89,9 +89,9 @@
8989

9090
<!-- Meeting Title -->
9191
<div class="flex items-center gap-2 mb-2" data-testid="meeting-title-section">
92-
@if (occurrence()?.title || meeting().title) {
92+
@if (meetingTitle()) {
9393
<h3 class="text-base font-medium text-gray-900 leading-tight tracking-tight" data-testid="meeting-title">
94-
{{ occurrence()?.title || meeting().title }}
94+
{{ meetingTitle() }}
9595
</h3>
9696
}
9797
</div>
@@ -114,24 +114,24 @@ <h3 class="text-base font-medium text-gray-900 leading-tight tracking-tight" dat
114114
@if (meeting().transcript_enabled) {
115115
<lfx-tag value="Transcripts" severity="secondary" icon="fa-light fa-file-lines"></lfx-tag>
116116
}
117-
@if (meeting().zoom_config?.ai_companion_enabled) {
117+
@if (hasAiCompanion()) {
118118
<lfx-tag value="AI Summary" severity="success" icon="fa-light fa-sparkles"></lfx-tag>
119119
}
120120
</div>
121121

122122
<!-- Description Section -->
123-
@if (occurrence()?.description || meeting().description) {
123+
@if (meetingDescription()) {
124124
<div class="my-3" data-testid="meeting-description">
125125
<lfx-expandable-text [maxHeight]="85">
126126
<div class="text-gray-600 text-[12.25px] leading-relaxed tracking-tight">
127-
<div [innerHTML]="occurrence()?.description || meeting().description | linkify"></div>
127+
<div [innerHTML]="meetingDescription() | linkify"></div>
128128
</div>
129129
</lfx-expandable-text>
130130
</div>
131131
}
132132

133133
<!-- AI Summary Review Banner for Organizers -->
134-
@if (pastMeeting() && meeting().organizer && hasSummary() && !summaryApproved()) {
134+
@if (pastMeeting() && meeting().organizer && !isLegacyMeeting() && hasSummary() && !summaryApproved()) {
135135
<div class="flex items-center gap-2.5 px-3.5 py-3 bg-blue-50 border border-blue-200 rounded-md mb-3">
136136
<i class="fa-light fa-sparkles text-base text-blue-600 flex-shrink-0 mt-0.5"></i>
137137
<div class="flex-1 flex items-center justify-between gap-3">
@@ -227,7 +227,7 @@ <h3 class="text-base font-medium text-gray-900 leading-tight tracking-tight" dat
227227
}
228228
</div>
229229
}
230-
@if (pastMeeting() && meeting().organizer) {
230+
@if (pastMeeting() && meeting().organizer && !isLegacyMeeting()) {
231231
<div class="flex flex-col md:flex-row gap-2 mt-3.5">
232232
@if (hasRecording()) {
233233
<lfx-button
@@ -259,18 +259,25 @@ <h3 class="text-base font-medium text-gray-900 leading-tight tracking-tight" dat
259259

260260
<!-- People/Attendees Card Column -->
261261
<div class="flex flex-col">
262-
@if (meeting().organizer) {
262+
@if (meeting().organizer && !isLegacyMeeting()) {
263263
<!-- Show RSVP Details for organizers and past meetings -->
264264
<lfx-meeting-rsvp-details
265265
[meeting]="meeting()"
266266
[currentOccurrence]="currentOccurrence()"
267267
[pastMeeting]="pastMeeting()"
268268
[showAddButton]="!pastMeeting()"
269-
[additionalRegistrantsCount]="additionalRegistrantsCount()">
269+
[additionalRegistrantsCount]="additionalRegistrantsCount()"
270+
[disabled]="isLegacyMeeting()"
271+
disabledMessage="Meetings created outside of LFX One do not have RSVP functionality">
270272
</lfx-meeting-rsvp-details>
271273
} @else if (!pastMeeting()) {
272274
<!-- Show RSVP Selection for authenticated non-organizers (upcoming meetings only) -->
273-
<lfx-rsvp-button-group [meeting]="meeting()" [occurrenceId]="occurrence()?.occurrence_id"> </lfx-rsvp-button-group>
275+
<lfx-rsvp-button-group
276+
[meeting]="meeting()"
277+
[occurrenceId]="occurrence()?.occurrence_id"
278+
[disabled]="isLegacyMeeting()"
279+
disabledMessage="Meetings created outside of LFX One do not have RSVP functionality">
280+
</lfx-rsvp-button-group>
274281
} @else if (pastMeeting() && !meeting().organizer) {
275282
<!-- Show Recording and AI Summary buttons for past meetings (non-organizers) -->
276283
<div class="flex flex-col gap-2">
@@ -303,7 +310,7 @@ <h3 class="text-base font-medium text-gray-900 leading-tight tracking-tight" dat
303310

304311
<!-- Action Buttons for People Column -->
305312
<div class="flex gap-2" [ngClass]="{ 'mt-3.5': !pastMeeting() || meeting().organizer }">
306-
@if (meeting().organizer) {
313+
@if (meeting().organizer && !isLegacyMeeting()) {
307314
<lfx-button
308315
class="w-full"
309316
icon="fa-light fa-users"
@@ -324,22 +331,24 @@ <h3 class="text-base font-medium text-gray-900 leading-tight tracking-tight" dat
324331
severity="secondary"
325332
styleClass="w-full"
326333
data-testid="see-meeting-details-button"
327-
[href]="'/meetings/' + meeting().uid + '?password=' + meeting().password"
334+
[href]="meetingDetailUrl()"
328335
target="_blank">
329336
</lfx-button>
330337
}
331338
</div>
332339
</div>
333340
</div>
334341

335-
<!-- Meeting Registrants -->
336-
<lfx-meeting-registrants-display
337-
[meeting]="meeting()"
338-
[pastMeeting]="pastMeeting()"
339-
[visible]="showRegistrants()"
340-
[showAddRegistrant]="!pastMeeting()"
341-
(registrantsCountChange)="additionalRegistrantsCount.set($event)">
342-
</lfx-meeting-registrants-display>
342+
<!-- Meeting Registrants (v2 meetings only) -->
343+
@if (!isLegacyMeeting()) {
344+
<lfx-meeting-registrants-display
345+
[meeting]="meeting()"
346+
[pastMeeting]="pastMeeting()"
347+
[visible]="showRegistrants()"
348+
[showAddRegistrant]="!pastMeeting()"
349+
(registrantsCountChange)="additionalRegistrantsCount.set($event)">
350+
</lfx-meeting-registrants-display>
351+
}
343352

344353
<!-- Confirmation Dialog -->
345354
<p-confirmDialog></p-confirmDialog>

0 commit comments

Comments
 (0)