Skip to content

Commit 52ded8f

Browse files
authored
feat(meetings): improve file upload and v1/v2 migration refactor (#204)
* feat(meetings): improve file upload validation and file types - Add user-friendly error messages for unsupported file types - Add getMimeTypeDisplayName() to convert MIME types to extensions - Add getAcceptedFileTypesDisplay() for categorized file types - Add support for SVG, CSV, RTF, and legacy markdown MIME types - Add legacy MIME types for images and Office documents - Add isFileTypeAllowed() with extension-based fallback validation - Fix validation for files with empty or generic MIME types LFXV2-894 Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Asitha de Silva <[email protected]> * fix(meetings): improve file type validation with extension fallback - Add optional filename parameter to getMimeTypeDisplayName for fallback - Fix extension detection bug for files without extensions - Derive MAX_FILE_SIZE_MB from MAX_FILE_SIZE_BYTES to prevent drift - Increase max file size from 10MB to 100MB - Remove redundant empty string check in MIME type validation LFXV2-894 Signed-off-by: Asitha de Silva <[email protected]> * refactor(meetings): move v1/v2 migration to server-side transformation LFXV2-899 - Add server-side transformation utilities (transformV1MeetingToV2, transformV1SummaryToV2) - Remove v1 fallback logic from UI components (meeting-card, dashboard-meeting-card, meeting-join) - Remove unnecessary computed signals (meetingIdentifier, formatV1SummaryContent) - Remove v1 legacy fields from Meeting and PastMeetingSummary interfaces - Server now detects v1 status internally using UUID format check - Fix meeting sorting when occurrences array is empty 🤖 Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Asitha de Silva <[email protected]> * refactor(meetings): remove unnecessary summaryUid computed signal Remove initSummaryUid wrapper and use this.summary()?.uid directly in the two places it was used (openSummaryModal guard and data). LFXV2-899 🤖 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 85298c3 commit 52ded8f

File tree

18 files changed

+780
-394
lines changed

18 files changed

+780
-394
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080
<div class="flex items-center gap-1 flex-shrink-0 mt-0.5">
8181
@for (attachment of attachments(); track attachment.uid; let index = $index) {
8282
<a
83-
[href]="attachment.type === 'link' ? attachment.link : '/api/meetings/' + meetingIdentifier() + '/attachments/' + attachment.uid"
83+
[href]="attachment.type === 'link' ? attachment.link : '/api/meetings/' + meeting().uid + '/attachments/' + attachment.uid"
8484
target="_blank"
8585
rel="noopener noreferrer"
8686
[download]="attachment.type === 'file' ? attachment.name : null"

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

Lines changed: 8 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -51,32 +51,24 @@ export class DashboardMeetingCardComponent {
5151
public readonly hasTranscripts: Signal<boolean> = this.initHasTranscripts();
5252
public readonly canJoinMeeting: Signal<boolean> = this.initCanJoinMeeting();
5353

54-
// TODO(v1-migration): Simplify to use V2 fields only once all meetings are migrated to V2
5554
public readonly hasAiSummary: Signal<boolean> = this.initHasAiSummary();
5655
public readonly meetingTitle: Signal<string> = this.initMeetingTitle();
57-
58-
// TODO(v1-migration): Remove once all meetings are migrated to V2
5956
public readonly isLegacyMeeting: Signal<boolean> = this.initIsLegacyMeeting();
60-
public readonly meetingIdentifier: Signal<string> = this.initMeetingIdentifier();
6157
public readonly meetingDetailUrl: Signal<string> = this.initMeetingDetailUrl();
6258

6359
public constructor() {
64-
// Convert meeting input signal to observable and create reactive attachment stream
6560
const meeting$ = toObservable(this.meeting);
66-
const meetingIdentifier$ = toObservable(this.meetingIdentifier);
67-
const attachments$ = meetingIdentifier$.pipe(
68-
switchMap((identifier) => {
69-
if (identifier) {
70-
return this.meetingService.getMeetingAttachments(identifier).pipe(catchError(() => of([])));
61+
const attachments$ = meeting$.pipe(
62+
switchMap((meeting) => {
63+
if (meeting?.uid) {
64+
return this.meetingService.getMeetingAttachments(meeting.uid).pipe(catchError(() => of([])));
7165
}
7266
return of([]);
7367
})
7468
);
7569

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

78-
// TODO(v1-migration): Remove V1 join URL handling once all meetings are migrated to V2
79-
// Convert user signal to observable and create reactive join URL stream
8072
const user$ = toObservable(this.userService.user);
8173
const authenticated$ = toObservable(this.userService.authenticated);
8274
const isLegacyMeeting$ = toObservable(this.isLegacyMeeting);
@@ -223,46 +215,25 @@ export class DashboardMeetingCardComponent {
223215
});
224216
}
225217

226-
// TODO(v1-migration): Simplify to use V2 fields only once all meetings are migrated to V2
227218
private initHasAiSummary(): Signal<boolean> {
228-
return computed(() => {
229-
const meeting = this.meeting();
230-
// V2: zoom_config.ai_companion_enabled, V1: zoom_ai_enabled
231-
return meeting.zoom_config?.ai_companion_enabled === true || meeting.zoom_ai_enabled === true;
232-
});
219+
return computed(() => this.meeting().zoom_config?.ai_companion_enabled || false);
233220
}
234221

235-
// TODO(v1-migration): Simplify to use V2 fields only once all meetings are migrated to V2
236222
private initMeetingTitle(): Signal<string> {
237223
return computed(() => {
238224
const occurrence = this.occurrence();
239225
const meeting = this.meeting();
240-
241-
// Priority: occurrence title > meeting title > meeting topic (v1)
242-
return occurrence?.title || meeting.title || meeting.topic || '';
226+
return occurrence?.title || meeting.title || '';
243227
});
244228
}
245229

246-
// TODO(v1-migration): Remove once all meetings are migrated to V2
247230
private initIsLegacyMeeting(): Signal<boolean> {
248-
return computed(() => {
249-
return this.meeting().version === 'v1';
250-
});
251-
}
252-
253-
// TODO(v1-migration): Simplify to use V2 uid only once all meetings are migrated to V2
254-
private initMeetingIdentifier(): Signal<string> {
255-
return computed(() => {
256-
const meeting = this.meeting();
257-
return this.isLegacyMeeting() && meeting.id ? (meeting.id as string) : meeting.uid;
258-
});
231+
return computed(() => this.meeting().version === 'v1');
259232
}
260233

261-
// TODO(v1-migration): Remove V1 parameter handling once all meetings are migrated to V2
262234
private initMeetingDetailUrl(): Signal<string> {
263235
return computed(() => {
264236
const meeting = this.meeting();
265-
const identifier = this.meetingIdentifier();
266237
const params = new URLSearchParams();
267238

268239
if (meeting.password) {
@@ -274,7 +245,7 @@ export class DashboardMeetingCardComponent {
274245
}
275246

276247
const queryString = params.toString();
277-
return queryString ? `/meetings/${identifier}?${queryString}` : `/meetings/${identifier}`;
248+
return queryString ? `/meetings/${meeting.uid}?${queryString}` : `/meetings/${meeting.uid}`;
278249
});
279250
}
280251
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ <h3 class="text-base font-medium text-gray-900 leading-tight tracking-tight" dat
158158
<i class="fa-light fa-paperclip text-xs"></i>
159159
<span>Resources</span>
160160
</div>
161-
@if (!pastMeeting() && meeting().organizer) {
161+
@if (!pastMeeting() && meeting().organizer && !isLegacyMeeting()) {
162162
<lfx-button
163163
icon="fa-light fa-upload text-xs"
164164
label="Add"
@@ -177,7 +177,7 @@ <h3 class="text-base font-medium text-gray-900 leading-tight tracking-tight" dat
177177
<div class="grid grid-cols-2 gap-1">
178178
@for (attachment of attachments(); track attachment.uid) {
179179
<a
180-
[href]="attachment.type === 'link' ? attachment.link : '/api/meetings/' + meetingIdentifier() + '/attachments/' + attachment.uid"
180+
[href]="attachment.type === 'link' ? attachment.link : '/api/meetings/' + meeting().uid + '/attachments/' + attachment.uid"
181181
target="_blank"
182182
rel="noopener noreferrer"
183183
class="inline-flex !text-black font-semibold items-center gap-1.5 px-2 py-1.5 bg-gray-200/80 hover:bg-gray-300/80 rounded text-[12px] tracking-wide transition-colors h-[26px]"
@@ -217,7 +217,7 @@ <h3 class="text-base font-medium text-gray-900 leading-tight tracking-tight" dat
217217
size="small"
218218
class="w-full"
219219
label="Join Meeting"
220-
[routerLink]="['/meetings', meetingIdentifier()]"
220+
[routerLink]="['/meetings', meeting().uid]"
221221
[queryParams]="joinQueryParams()"
222222
rel="noopener noreferrer"
223223
icon="fa-light fa-video"
@@ -265,7 +265,7 @@ <h3 class="text-base font-medium text-gray-900 leading-tight tracking-tight" dat
265265
[meeting]="meeting()"
266266
[currentOccurrence]="currentOccurrence()"
267267
[pastMeeting]="pastMeeting()"
268-
[showAddButton]="!pastMeeting()"
268+
[showAddButton]="!pastMeeting() && !isLegacyMeeting()"
269269
[additionalRegistrantsCount]="additionalRegistrantsCount()"
270270
[disabled]="isLegacyMeeting()"
271271
disabledMessage="RSVP functionality will soon be available for all upcoming LFX meetings visible to you">

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

Lines changed: 14 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,6 @@ export class MeetingCardComponent implements OnInit {
107107
// Computed values for template
108108
public readonly meetingRegistrantCount: Signal<number> = this.initMeetingRegistrantCount();
109109
public readonly summaryContent: Signal<string | null> = this.initSummaryContent();
110-
public readonly summaryUid: Signal<string | null> = this.initSummaryUid();
111110
public readonly summaryApproved: Signal<boolean> = this.initSummaryApproved();
112111
public readonly hasSummary: Signal<boolean> = this.initHasSummary();
113112
public readonly attendancePercentage: Signal<number> = this.initAttendancePercentage();
@@ -132,7 +131,6 @@ export class MeetingCardComponent implements OnInit {
132131
public readonly joinUrl: Signal<string | null>;
133132
public readonly authenticated: Signal<boolean> = this.userService.authenticated;
134133

135-
// TODO(v1-migration): Remove V1/V2 fallback fields and legacy meeting handling once all meetings are migrated to V2
136134
public readonly isLegacyMeeting: Signal<boolean> = this.initIsLegacyMeeting();
137135
public readonly meetingDetailUrl: Signal<string> = this.initMeetingDetailUrl();
138136

@@ -142,11 +140,9 @@ export class MeetingCardComponent implements OnInit {
142140
() => !this.isInvited() && !this.meeting().restricted && this.meeting().visibility === 'public'
143141
);
144142

145-
// V1/V2 fallback fields
146143
public readonly meetingTitle: Signal<string> = this.initMeetingTitle();
147144
public readonly meetingDescription: Signal<string> = this.initMeetingDescription();
148145
public readonly hasAiCompanion: Signal<boolean> = this.initHasAiCompanion();
149-
public readonly meetingIdentifier: Signal<string> = this.initMeetingIdentifier();
150146
public readonly joinQueryParams: Signal<Record<string, string>> = this.initJoinQueryParams();
151147

152148
public readonly meetingDeleted = output<void>();
@@ -155,7 +151,7 @@ export class MeetingCardComponent implements OnInit {
155151

156152
public constructor() {
157153
effect(() => {
158-
if (!this.meeting()?.uid && !this.meeting()?.id) {
154+
if (!this.meeting()?.uid) {
159155
this.meeting.set(this.meetingInput());
160156
}
161157
// Priority: explicit occurrenceInput > current occurrence for upcoming > null for past without input
@@ -243,8 +239,7 @@ export class MeetingCardComponent implements OnInit {
243239

244240
public copyMeetingLink(): void {
245241
const meeting = this.meeting();
246-
const identifier = this.meetingIdentifier();
247-
const meetingUrl: URL = new URL(environment.urls.home + '/meetings/' + identifier);
242+
const meetingUrl: URL = new URL(environment.urls.home + '/meetings/' + meeting.uid);
248243

249244
if (meeting.password) {
250245
meetingUrl.searchParams.set('password', meeting.password);
@@ -306,7 +301,7 @@ export class MeetingCardComponent implements OnInit {
306301
}
307302

308303
public openSummaryModal(): void {
309-
if (!this.summaryContent() || !this.summaryUid()) {
304+
if (!this.summaryContent() || !this.summary()?.uid) {
310305
return;
311306
}
312307

@@ -318,8 +313,8 @@ export class MeetingCardComponent implements OnInit {
318313
dismissableMask: false,
319314
data: {
320315
summaryContent: this.summaryContent(),
321-
summaryUid: this.summaryUid(),
322-
pastMeetingUid: this.meeting().uid || this.meeting().id,
316+
summaryUid: this.summary()?.uid,
317+
pastMeetingUid: this.meeting().uid,
323318
meetingTitle: this.meetingTitle(),
324319
approved: this.summaryApproved(),
325320
},
@@ -484,7 +479,7 @@ export class MeetingCardComponent implements OnInit {
484479

485480
private initAttachments(): Signal<MeetingAttachment[]> {
486481
return runInInjectionContext(this.injector, () => {
487-
return toSignal(this.meetingService.getMeetingAttachments(this.meetingIdentifier()).pipe(catchError(() => of([]))), {
482+
return toSignal(this.meetingService.getMeetingAttachments(this.meeting().uid).pipe(catchError(() => of([]))), {
488483
initialValue: [],
489484
});
490485
});
@@ -493,7 +488,7 @@ export class MeetingCardComponent implements OnInit {
493488
private initRecording(): void {
494489
runInInjectionContext(this.injector, () => {
495490
toSignal(
496-
this.meetingService.getPastMeetingRecording(this.meetingIdentifier(), this.isLegacyMeeting()).pipe(
491+
this.meetingService.getPastMeetingRecording(this.meeting().uid).pipe(
497492
catchError(() => of(null)),
498493
tap((recording) => this.recording.set(recording))
499494
),
@@ -505,7 +500,7 @@ export class MeetingCardComponent implements OnInit {
505500
private initSummary(): void {
506501
runInInjectionContext(this.injector, () => {
507502
toSignal(
508-
this.meetingService.getPastMeetingSummary(this.meetingIdentifier(), this.isLegacyMeeting()).pipe(
503+
this.meetingService.getPastMeetingSummary(this.meeting().uid).pipe(
509504
catchError(() => of(null)),
510505
tap((summary) => this.summary.set(summary))
511506
),
@@ -685,65 +680,11 @@ export class MeetingCardComponent implements OnInit {
685680
return computed(() => this.recordingShareUrl() !== null);
686681
}
687682

688-
// TODO(v1-migration): Simplify to use V2 format only once all meetings are migrated to V2
689683
private initSummaryContent(): Signal<string | null> {
690684
return computed(() => {
691685
const summary = this.summary();
692-
if (!summary) return null;
693-
694-
// V2 format: use summary_data content
695-
if (summary.summary_data) {
696-
return summary.summary_data.edited_content || summary.summary_data.content;
697-
}
698-
699-
// V1 format: construct content from v1 fields
700-
if (summary.summary_overview || summary.summary_details || summary.next_steps) {
701-
return this.formatV1SummaryContent(summary);
702-
}
703-
704-
return null;
705-
});
706-
}
707-
708-
// TODO(v1-migration): Remove formatV1SummaryContent once all meetings are migrated to V2
709-
private formatV1SummaryContent(summary: PastMeetingSummary): string {
710-
const parts: string[] = [];
711-
712-
// Use edited versions if available, otherwise use original
713-
const overview = summary.edited_summary_overview || summary.summary_overview;
714-
const details = summary.edited_summary_details || summary.summary_details;
715-
const nextSteps = summary.edited_next_steps || summary.next_steps;
716-
717-
// Add overview
718-
if (overview) {
719-
parts.push(`## Overview\n${overview}`);
720-
}
721-
722-
// Add details
723-
if (details && details.length > 0) {
724-
parts.push('## Key Topics');
725-
details.forEach((detail) => {
726-
parts.push(`### ${detail.label}\n${detail.summary}`);
727-
});
728-
}
729-
730-
// Add next steps
731-
if (nextSteps && nextSteps.length > 0) {
732-
parts.push('## Next Steps');
733-
nextSteps.forEach((step) => {
734-
parts.push(`- ${step}`);
735-
});
736-
}
737-
738-
return parts.join('\n\n');
739-
}
740-
741-
// TODO(v1-migration): Simplify to use V2 uid only once all meetings are migrated to V2
742-
private initSummaryUid(): Signal<string | null> {
743-
return computed(() => {
744-
const summary = this.summary();
745-
// V2 uses 'uid', V1 uses 'id'
746-
return summary?.uid || summary?.id || null;
686+
if (!summary?.summary_data) return null;
687+
return summary.summary_data.edited_content || summary.summary_data.content;
747688
});
748689
}
749690

@@ -755,16 +696,13 @@ export class MeetingCardComponent implements OnInit {
755696
return computed(() => this.summaryContent() !== null);
756697
}
757698

758-
// TODO(v1-migration): Remove once all meetings are migrated to V2
759699
private initIsLegacyMeeting(): Signal<boolean> {
760700
return computed(() => this.meetingInput().version === 'v1');
761701
}
762702

763-
// TODO(v1-migration): Remove V1 parameter handling once all meetings are migrated to V2
764703
private initMeetingDetailUrl(): Signal<string> {
765704
return computed(() => {
766705
const meeting = this.meetingInput();
767-
const identifier = this.meetingIdentifier();
768706
const params = new URLSearchParams();
769707

770708
if (meeting.password) {
@@ -776,51 +714,32 @@ export class MeetingCardComponent implements OnInit {
776714
}
777715

778716
const queryString = params.toString();
779-
return queryString ? `/meetings/${identifier}?${queryString}` : `/meetings/${identifier}`;
717+
return queryString ? `/meetings/${meeting.uid}?${queryString}` : `/meetings/${meeting.uid}`;
780718
});
781719
}
782720

783-
// TODO(v1-migration): Simplify to use V2 fields only once all meetings are migrated to V2
784721
private initMeetingTitle(): Signal<string> {
785722
return computed(() => {
786723
const occurrence = this.occurrence();
787724
const meeting = this.meeting();
788-
789-
// Priority: occurrence title > meeting title > meeting topic (v1)
790-
return occurrence?.title || meeting.title || meeting.topic || '';
725+
return occurrence?.title || meeting.title || '';
791726
});
792727
}
793728

794-
// TODO(v1-migration): Simplify to use V2 fields only once all meetings are migrated to V2
795729
private initMeetingDescription(): Signal<string> {
796730
return computed(() => {
797731
const occurrence = this.occurrence();
798732
const meeting = this.meeting();
799-
800-
// Priority: occurrence description > meeting description > meeting agenda (v1)
801-
return occurrence?.description || meeting.description || meeting.agenda || '';
733+
return occurrence?.description || meeting.description || '';
802734
});
803735
}
804736

805-
// TODO(v1-migration): Simplify to use V2 fields only once all meetings are migrated to V2
806737
private initHasAiCompanion(): Signal<boolean> {
807738
return computed(() => {
808-
const meeting = this.meeting();
809-
810-
// V2: zoom_config.ai_companion_enabled, V1: zoom_ai_enabled
811-
return meeting.zoom_config?.ai_companion_enabled || meeting.zoom_ai_enabled || false;
812-
});
813-
}
814-
815-
// TODO(v1-migration): Simplify to use V2 uid only once all meetings are migrated to V2
816-
private initMeetingIdentifier(): Signal<string> {
817-
return computed(() => {
818-
const meeting = this.meetingInput();
819-
return this.isLegacyMeeting() && meeting.id ? (meeting.id as string) : meeting.uid;
739+
return this.meeting().zoom_config?.ai_companion_enabled || false;
820740
});
821741
}
822742

823-
// TODO(v1-migration): Remove V1 parameter handling once all meetings are migrated to V2
824743
private initJoinQueryParams(): Signal<Record<string, string>> {
825744
return computed(() => {
826745
const meeting = this.meetingInput();

0 commit comments

Comments
 (0)