diff --git a/apps/lfx-one/src/app/modules/committees/components/committee-meetings/committee-meetings.component.html b/apps/lfx-one/src/app/modules/committees/components/committee-meetings/committee-meetings.component.html index 3ad4c7d7c..99552a7f6 100644 --- a/apps/lfx-one/src/app/modules/committees/components/committee-meetings/committee-meetings.component.html +++ b/apps/lfx-one/src/app/modules/committees/components/committee-meetings/committee-meetings.component.html @@ -108,7 +108,7 @@

Meetings

- + Meeting (default) diff --git a/apps/lfx-one/src/app/modules/committees/components/committee-meetings/committee-meetings.component.ts b/apps/lfx-one/src/app/modules/committees/components/committee-meetings/committee-meetings.component.ts index d16b80786..884f3ac77 100644 --- a/apps/lfx-one/src/app/modules/committees/components/committee-meetings/committee-meetings.component.ts +++ b/apps/lfx-one/src/app/modules/committees/components/committee-meetings/committee-meetings.component.ts @@ -149,14 +149,15 @@ export class CommitteeMeetingsComponent { dismissableMask: true, data: { feedUrl, - committeeName, + name: committeeName, }, }); } - /** Handles FullCalendar event click — navigates to meeting detail for meeting events. */ + /** Handles FullCalendar event click — navigates to meeting detail. Cancelled occurrences are inert. */ public onCalendarEventClick(arg: EventClickArg): void { - const props = arg.event.extendedProps as { type: string; meetingId?: string }; + const props = arg.event.extendedProps as { type: string; meetingId?: string; cancelled?: boolean }; + if (props.cancelled) return; if (props.type === 'meeting' && props.meetingId) { void this.router.navigate(['/meetings', props.meetingId]); } @@ -271,6 +272,9 @@ export class CommitteeMeetingsComponent { borderColor: c.border, textColor: '#ffffff', display: 'block', + // cursor-default on cancelled occurrences removes the pointer affordance; + // onCalendarEventClick also short-circuits when extendedProps.cancelled is true. + classNames: isCancelled ? ['cursor-default'] : [], extendedProps: { type: 'meeting', meetingId: meeting.id, cancelled: isCancelled }, }; }); diff --git a/apps/lfx-one/src/app/modules/committees/components/ical-subscribe-dialog/ical-subscribe-dialog.component.ts b/apps/lfx-one/src/app/modules/committees/components/ical-subscribe-dialog/ical-subscribe-dialog.component.ts index d21ef2a4c..59fadb6b7 100644 --- a/apps/lfx-one/src/app/modules/committees/components/ical-subscribe-dialog/ical-subscribe-dialog.component.ts +++ b/apps/lfx-one/src/app/modules/committees/components/ical-subscribe-dialog/ical-subscribe-dialog.component.ts @@ -27,14 +27,14 @@ export class IcalSubscribeDialogComponent { private readonly destroyRef = inject(DestroyRef); public readonly feedUrl = this.dialogConfig.data?.feedUrl ?? ''; - public readonly committeeName = this.dialogConfig.data?.committeeName ?? 'Committee'; + public readonly name = this.dialogConfig.data?.name ?? 'Calendar'; public readonly googleCalendarUrl = this.feedUrl ? `https://calendar.google.com/calendar/r?cid=${encodeURIComponent(toWebcal(this.feedUrl))}` : ''; public readonly outlookLiveUrl = this.feedUrl - ? `https://outlook.live.com/calendar/0/addfromweb?url=${encodeURIComponent(this.feedUrl)}&name=${encodeURIComponent(this.committeeName)}` + ? `https://outlook.live.com/calendar/0/addfromweb?url=${encodeURIComponent(this.feedUrl)}&name=${encodeURIComponent(this.name)}` : ''; public readonly outlook365Url = this.feedUrl - ? `https://outlook.office.com/calendar/0/addfromweb?url=${encodeURIComponent(this.feedUrl)}&name=${encodeURIComponent(this.committeeName)}` + ? `https://outlook.office.com/calendar/0/addfromweb?url=${encodeURIComponent(this.feedUrl)}&name=${encodeURIComponent(this.name)}` : ''; public readonly webcalUrl = this.feedUrl ? toWebcal(this.feedUrl) : ''; diff --git a/apps/lfx-one/src/app/modules/meetings/meetings-dashboard/meetings-dashboard.component.html b/apps/lfx-one/src/app/modules/meetings/meetings-dashboard/meetings-dashboard.component.html index 18b6180f3..79307c921 100644 --- a/apps/lfx-one/src/app/modules/meetings/meetings-dashboard/meetings-dashboard.component.html +++ b/apps/lfx-one/src/app/modules/meetings/meetings-dashboard/meetings-dashboard.component.html @@ -186,8 +186,63 @@

{{ activeLens() === 'me' ? 'My Meet

+ +
+
+ + +
+ @if (activeLens() === 'foundation' || activeLens() === 'project') { + + } +
+
- @if ((timeFilter() === 'upcoming' && meetingsLoading()) || (timeFilter() === 'past' && pastMeetingsLoading())) { + @if (isCalendarView()) { +
+
+ + + Meeting (default) + + + + Cancelled + +
+ @if ((timeFilter() === 'upcoming' && meetingsLoading()) || (timeFilter() === 'past' && pastMeetingsLoading())) { +
+ +
+ } @else { + + } +
+ } @else if ((timeFilter() === 'upcoming' && meetingsLoading()) || (timeFilter() === 'past' && pastMeetingsLoading())) {
@for (item of [0, 1, 2]; track item) {
diff --git a/apps/lfx-one/src/app/modules/meetings/meetings-dashboard/meetings-dashboard.component.ts b/apps/lfx-one/src/app/modules/meetings/meetings-dashboard/meetings-dashboard.component.ts index d96555a38..8344709c7 100644 --- a/apps/lfx-one/src/app/modules/meetings/meetings-dashboard/meetings-dashboard.component.ts +++ b/apps/lfx-one/src/app/modules/meetings/meetings-dashboard/meetings-dashboard.component.ts @@ -5,19 +5,24 @@ import { isPlatformBrowser } from '@angular/common'; import { Component, computed, inject, PLATFORM_ID, signal, Signal, WritableSignal } from '@angular/core'; import { toObservable, toSignal } from '@angular/core/rxjs-interop'; import { ActivatedRoute, Router } from '@angular/router'; +import { IcalSubscribeDialogComponent } from '@app/modules/committees/components/ical-subscribe-dialog/ical-subscribe-dialog.component'; import { MeetingCardComponent } from '@app/modules/meetings/components/meeting-card/meeting-card.component'; +import { FullCalendarComponent } from '@app/shared/components/fullcalendar/fullcalendar.component'; import { ButtonComponent } from '@components/button/button.component'; import { CardComponent } from '@components/card/card.component'; import { EmptyStateComponent } from '@components/empty-state/empty-state.component'; -import { MEETING_TYPE_CONFIGS } from '@lfx-one/shared/constants'; -import { Lens, Meeting, PageResult, PastMeeting, ProjectContext } from '@lfx-one/shared/interfaces'; -import { getCurrentOrNextOccurrence, hasMeetingEnded } from '@lfx-one/shared/utils'; +import { environment } from '@environments/environment'; +import { EventClickArg, EventInput } from '@fullcalendar/core'; +import { CANCELLED_COLOR, MEETING_TYPE_COLORS, MEETING_TYPE_CONFIGS } from '@lfx-one/shared/constants'; +import { Lens, Meeting, PageResult, PastMeeting, ProjectContext, ViewMode } from '@lfx-one/shared/interfaces'; +import { addMinutesToDate, getCurrentOrNextOccurrence, hasMeetingEnded } from '@lfx-one/shared/utils'; import { LensService } from '@services/lens.service'; import { MeetingService } from '@services/meeting.service'; import { PersonaService } from '@services/persona.service'; import { ProjectContextService } from '@services/project-context.service'; import { UserService } from '@services/user.service'; import { OnRenderDirective } from '@shared/directives/on-render.directive'; +import { DialogService } from 'primeng/dynamicdialog'; import { BehaviorSubject, catchError, @@ -42,7 +47,8 @@ import { MeetingsTopBarComponent } from './components/meetings-top-bar/meetings- @Component({ selector: 'lfx-meetings-dashboard', - imports: [MeetingCardComponent, MeetingsTopBarComponent, ButtonComponent, CardComponent, OnRenderDirective, EmptyStateComponent], + imports: [MeetingCardComponent, MeetingsTopBarComponent, ButtonComponent, CardComponent, OnRenderDirective, EmptyStateComponent, FullCalendarComponent], + providers: [DialogService], templateUrl: './meetings-dashboard.component.html', styleUrl: './meetings-dashboard.component.scss', }) @@ -55,9 +61,18 @@ export class MeetingsDashboardComponent { private readonly router = inject(Router); private readonly route = inject(ActivatedRoute); private readonly platformId = inject(PLATFORM_ID); + private readonly dialogService = inject(DialogService); public readonly activeLens: Signal = this.lensService.activeLens; + // View mode: list (default) or calendar. Calendar renders meetings as + // FullCalendar events; the existing filter pipeline (lens/foundation/project/ + // search/meetingType/timeFilter) applies to both views. + public viewMode = signal('list'); + public isListView = computed(() => this.viewMode() === 'list'); + public isCalendarView = computed(() => this.viewMode() === 'calendar'); + public calendarEvents: Signal; + public meetingsLoading: WritableSignal; public pastMeetingsLoading: WritableSignal; public upcomingMeetings: Signal; @@ -191,6 +206,22 @@ export class MeetingsDashboardComponent { // Sentinel is placed at 50% of the list to trigger auto-load as user scrolls this.autoLoadTriggerIndex = computed(() => Math.floor(this.filteredMeetings().length / 2)); + + // Calendar events: source from the dashboard's NON-paginated raw signals + // (rawUserMeetings + rawUserPastMeetings for the Me lens; + // rawFpUpcomingMeetings + rawFpPastMeetings for foundation/project/org). + // This avoids the list-view pagination gap where the calendar would + // silently miss meetings the user hasn't scrolled to yet. + // Search and meeting-type filters are applied; foundation/project and + // pendingRsvp filters intentionally don't narrow the calendar in this + // iteration (FP raw signals are already scoped to the active context). + this.calendarEvents = computed(() => { + const lens = this.activeLens(); + const meetings: (Meeting | PastMeeting)[] = + lens === 'me' ? [...this.rawUserMeetings(), ...this.rawUserPastMeetings()] : [...this.rawFpUpcomingMeetings(), ...this.rawFpPastMeetings()]; + const filtered = this.filterBySearchAndType(meetings, this.debouncedSearchQuery(), this.meetingTypeFilter()); + return filtered.flatMap((m) => this.meetingToEvents(m)); + }); } public refreshMeetings(): void { @@ -199,6 +230,50 @@ export class MeetingsDashboardComponent { this.refresh$.next(); } + /** FullCalendar event click → navigate to the meeting detail. Cancelled occurrences are inert. */ + public onCalendarEventClick(arg: EventClickArg): void { + const props = arg.event.extendedProps as { type: string; meetingId?: string; cancelled?: boolean }; + if (props.cancelled) return; + if (props.type === 'meeting' && props.meetingId) { + void this.router.navigate(['/meetings', props.meetingId]); + } + } + + /** + * Opens the iCal Subscribe modal for foundation / project lenses. + * + * Foundations and projects share the same upstream data model (a foundation + * IS a project at the data layer), so both lenses use the same backend route. + * The "me" lens is descoped — a personal feed requires a token-based public + * URL (calendar clients can't carry session cookies); tracked in LFXV2-1772. + * The "org" lens is tracked in LFXV2-1770. + */ + public onSubscribe(): void { + const lens = this.activeLens(); + const projectCtx = this.project(); + + if (lens !== 'foundation' && lens !== 'project') { + console.warn(`Subscribe is not supported on the "${lens}" lens; aborting dialog open`); + return; + } + if (!projectCtx?.uid) { + console.warn(`Subscribe clicked on ${lens} lens with no uid; aborting dialog open`); + return; + } + + const feedUrl = `${environment.urls.home}/public/api/projects/${encodeURIComponent(projectCtx.uid)}/calendar.ics`; + const name = projectCtx.name ?? (lens === 'foundation' ? 'Foundation' : 'Project'); + + this.dialogService.open(IcalSubscribeDialogComponent, { + header: `Subscribe — ${name}`, + width: '480px', + modal: true, + closable: true, + dismissableMask: true, + data: { feedUrl, name }, + }); + } + public onMeetingTypeChange(value: string | null): void { this.meetingTypeFilter.set(value); } @@ -711,4 +786,49 @@ export class MeetingsDashboardComponent { () => !!this.debouncedSearchQuery() || !!this.meetingTypeFilter() || !!this.foundationFilter() || !!this.projectFilter() || this.pendingRsvpOnly() ); } + + /** + * Convert one Meeting (or PastMeeting) into FullCalendar EventInput entries. + * Recurring meetings expand to one event per occurrence; non-recurring + * meetings render as a single event. Mirrors CommitteeMeetingsComponent.meetingToEvents. + */ + private meetingToEvents(meeting: Meeting | PastMeeting): EventInput[] { + const typeKey = (meeting.meeting_type ?? 'default').toLowerCase(); + const colors = MEETING_TYPE_COLORS[typeKey] ?? MEETING_TYPE_COLORS['default']; + + if (meeting.occurrences && meeting.occurrences.length > 0) { + return meeting.occurrences.map((occ) => { + const isCancelled = occ.status === 'cancel'; + const c = isCancelled ? CANCELLED_COLOR : colors; + return { + id: `${meeting.id}-${occ.occurrence_id}`, + title: occ.title || meeting.title, + start: occ.start_time, + end: addMinutesToDate(occ.start_time, occ.duration ?? meeting.duration).toISOString(), + backgroundColor: c.bg, + borderColor: c.border, + textColor: '#ffffff', + display: 'block', + // cursor-default on cancelled occurrences removes the pointer affordance; + // onCalendarEventClick also short-circuits when extendedProps.cancelled is true. + classNames: isCancelled ? ['cursor-default'] : [], + extendedProps: { type: 'meeting', meetingId: meeting.id, cancelled: isCancelled }, + }; + }); + } + + return [ + { + id: meeting.id, + title: meeting.title, + start: meeting.start_time, + end: addMinutesToDate(meeting.start_time, meeting.duration).toISOString(), + backgroundColor: colors.bg, + borderColor: colors.border, + textColor: '#ffffff', + display: 'block', + extendedProps: { type: 'meeting', meetingId: meeting.id }, + }, + ]; + } } diff --git a/apps/lfx-one/src/app/shared/components/fullcalendar/fullcalendar.component.scss b/apps/lfx-one/src/app/shared/components/fullcalendar/fullcalendar.component.scss index cebeea015..a7e29434e 100644 --- a/apps/lfx-one/src/app/shared/components/fullcalendar/fullcalendar.component.scss +++ b/apps/lfx-one/src/app/shared/components/fullcalendar/fullcalendar.component.scss @@ -21,8 +21,13 @@ } .fc-timegrid { + // Past/future dimming for the legacy "meeting-event" classed entries only. + // Generic events keep their inline backgroundColor (set via EventInput.backgroundColor), + // so meeting-type colors stay visible in Week view. The original blanket override + // forced bg-gray-100/bg-blue-50 on ALL timegrid events with !important, which combined + // with inline textColor:'#fff' rendered events as invisible white-on-near-white. .fc-timegrid-col-events { - .fc-timegrid-event { + .fc-timegrid-event.meeting-event { &:not(.fc-event-future) { @apply bg-gray-100 #{!important}; } @@ -86,6 +91,19 @@ @apply -translate-y-0.5 shadow-md; } + // Inert events (cancelled occurrences, vote/survey markers) opt out of + // the pointer affordance and the hover lift. Higher specificity than + // the .fc-event selector above, so Tailwind's .cursor-default class + // alone wouldn't win — the explicit `cursor: default !important` + // guarantees the override regardless of source order. + &.cursor-default { + cursor: default !important; + + &:hover { + @apply translate-y-0 shadow-none; + } + } + &.meeting-event { .fc-event-title { @apply font-medium leading-tight overflow-hidden line-clamp-2 text-gray-400; diff --git a/apps/lfx-one/src/app/shared/components/fullcalendar/fullcalendar.component.ts b/apps/lfx-one/src/app/shared/components/fullcalendar/fullcalendar.component.ts index dc88dd500..eaea84807 100644 --- a/apps/lfx-one/src/app/shared/components/fullcalendar/fullcalendar.component.ts +++ b/apps/lfx-one/src/app/shared/components/fullcalendar/fullcalendar.component.ts @@ -55,7 +55,9 @@ export class FullCalendarComponent { displayEventTime: true, eventOrder: 'start', nowIndicator: true, - scrollTime: new Date().getHours() + ':00:00', + // Week view scrolls to 6am on open — earlier hours rarely have meetings + // and scrolling to "now" overshoots past the morning when checking after lunch. + scrollTime: '06:00:00', }; } diff --git a/apps/lfx-one/src/server/controllers/committee.controller.ts b/apps/lfx-one/src/server/controllers/committee.controller.ts index d4c042967..429b75f86 100644 --- a/apps/lfx-one/src/server/controllers/committee.controller.ts +++ b/apps/lfx-one/src/server/controllers/committee.controller.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: MIT import { ALLOWED_FILE_TYPES } from '@lfx-one/shared/constants'; +import { MeetingVisibility } from '@lfx-one/shared/enums'; import { CommitteeCreateData, CommitteeUpdateData, @@ -1043,13 +1044,17 @@ export class CommitteeController { fetchAllMeetingPages((token) => this.meetingService.getMeetings(req, token ? { ...query, page_token: token } : query, 'v1_past_meeting', false)), ]); - const allMeetings = [...upcoming, ...past]; + // Public endpoint — filter out PRIVATE / restricted meetings so the feed + // never exposes private metadata to anyone holding the committee UID. + // Mirrors PublicMeetingController.getMeeting's visibility guard. + const allMeetings = [...upcoming, ...past].filter((m) => m.visibility === MeetingVisibility.PUBLIC && !m.restricted); const events = meetingsToVEvents(allMeetings); const ics = buildVCalendar(events); logger.success(req, 'get_committee_calendar', startTime, { committee_id: id, event_count: events.length, + filtered_out: upcoming.length + past.length - allMeetings.length, }); res.setHeader('Content-Type', 'text/calendar; charset=utf-8'); diff --git a/apps/lfx-one/src/server/controllers/project.controller.ts b/apps/lfx-one/src/server/controllers/project.controller.ts index ae4ef3004..87bac4ac2 100644 --- a/apps/lfx-one/src/server/controllers/project.controller.ts +++ b/apps/lfx-one/src/server/controllers/project.controller.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: MIT import { ALLOWED_FILE_TYPES } from '@lfx-one/shared/constants'; +import { MeetingVisibility } from '@lfx-one/shared/enums'; import { AddUserToProjectRequest, CreateProjectDocumentRequest, UpdateUserRoleRequest, UploadProjectDocumentRequest } from '@lfx-one/shared/interfaces'; import { isFileTypeAllowed, isUuid } from '@lfx-one/shared/utils'; import { NextFunction, Request, Response } from 'express'; @@ -11,10 +12,13 @@ import { ReadableStream as NodeReadableStream } from 'node:stream/web'; import { ServiceValidationError } from '../errors'; import { contentDispositionAttachment } from '../helpers/content-disposition.helper'; +import { buildVCalendar, fetchAllMeetingPages, meetingsToVEvents } from '../helpers/ics.helper'; import { getStringQueryParam } from '../helpers/validation.helper'; import { logger } from '../services/logger.service'; +import { MeetingService } from '../services/meeting.service'; import { ProjectService } from '../services/project.service'; import { getEffectiveEmail } from '../utils/auth-helper'; +import { generateM2MToken } from '../utils/m2m-token.util'; const FOLDER_UID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; @@ -23,6 +27,10 @@ const FOLDER_UID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0- */ export class ProjectController { private projectService: ProjectService = new ProjectService(); + // Injected here (rather than on ProjectService) to keep the calendar endpoint's + // meeting access independent of ProjectService — mirrors the CommitteeController + // pattern that avoids a circular dependency with MeetingService. + private meetingService: MeetingService = new MeetingService(); /** * GET /projects @@ -842,4 +850,63 @@ export class ProjectController { next(error); } } + + // ── Calendar ICS Endpoint ──────────────────────────────────────────────── + + /** + * GET /public/api/projects/:id/calendar.ics + * Returns an iCalendar (.ics) file containing the project's PUBLIC, non-restricted + * meetings. Also serves foundation-lens subscriptions since foundations and + * projects share the same data model (a foundation IS a project with no parent). + * MeetingService is injected at the controller to avoid the same circular + * dependency CommitteeController works around. + */ + public async getProjectCalendar(req: Request, res: Response, next: NextFunction): Promise { + const { id } = req.params; + const startTime = logger.startOperation(req, 'get_project_calendar', { project_id: id }); + + // Validate UID before using in query tags and Content-Disposition header + if (!id || !/^[A-Za-z0-9_-]+$/.test(id)) { + next(ServiceValidationError.forField('id', 'Invalid project ID', { operation: 'get_project_calendar' })); + return; + } + + try { + // When called from the public route there is no OIDC session, so use an + // M2M token. When called from the authenticated route the user's bearer + // token is already on req and no replacement is needed. + if (!req.bearerToken) { + req.bearerToken = await generateM2MToken(req); + } + + const query = { tags: `project_uid:${id}` }; + + // Paginate both upcoming and past meetings — first page only would silently + // drop meetings once a project exceeds the default page size. + const [upcoming, past] = await Promise.all([ + fetchAllMeetingPages((token) => this.meetingService.getMeetings(req, token ? { ...query, page_token: token } : query, 'v1_meeting', false)), + fetchAllMeetingPages((token) => this.meetingService.getMeetings(req, token ? { ...query, page_token: token } : query, 'v1_past_meeting', false)), + ]); + + // Public endpoint — filter out PRIVATE / restricted meetings so the feed + // never exposes private metadata to anyone holding the project UID. + // Mirrors PublicMeetingController.getMeeting's visibility guard. + const allMeetings = [...upcoming, ...past].filter((m) => m.visibility === MeetingVisibility.PUBLIC && !m.restricted); + const events = meetingsToVEvents(allMeetings); + const ics = buildVCalendar(events); + + logger.success(req, 'get_project_calendar', startTime, { + project_id: id, + event_count: events.length, + filtered_out: upcoming.length + past.length - allMeetings.length, + }); + + res.setHeader('Content-Type', 'text/calendar; charset=utf-8'); + res.setHeader('Content-Disposition', `attachment; filename="project-${id}.ics"`); + res.setHeader('Cache-Control', 'public, max-age=900'); // 15 minutes — reduces load from calendar clients polling every 15-60 minutes + res.send(ics); + } catch (error) { + next(error); + } + } } diff --git a/apps/lfx-one/src/server/routes/public-projects.route.ts b/apps/lfx-one/src/server/routes/public-projects.route.ts new file mode 100644 index 000000000..179878754 --- /dev/null +++ b/apps/lfx-one/src/server/routes/public-projects.route.ts @@ -0,0 +1,17 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +import { Router } from 'express'; + +import { ProjectController } from '../controllers/project.controller'; + +const router = Router(); +const projectController = new ProjectController(); + +// GET /public/api/projects/:id/calendar.ics +// Returns the iCalendar (.ics) feed for a project's (or foundation's) meetings. +// Public access — no authentication required so external calendar clients +// (Google Calendar, Outlook, Apple Calendar) can subscribe by URL. +router.get('/:id/calendar.ics', (req, res, next) => projectController.getProjectCalendar(req, res, next)); + +export default router; diff --git a/apps/lfx-one/src/server/server.ts b/apps/lfx-one/src/server/server.ts index f7a693454..ce0fc9a46 100644 --- a/apps/lfx-one/src/server/server.ts +++ b/apps/lfx-one/src/server/server.ts @@ -36,6 +36,7 @@ import profileRouter from './routes/profile.route'; import projectsRouter from './routes/projects.route'; import publicCommitteesRouter from './routes/public-committees.route'; import publicMeetingsRouter from './routes/public-meetings.route'; +import publicProjectsRouter from './routes/public-projects.route'; import rewardsRouter from './routes/rewards.route'; import searchRouter from './routes/search.route'; import surveysRouter from './routes/surveys.route'; @@ -172,6 +173,7 @@ app.use('/login', authRateLimiter); app.use('/public/api/meetings', publicMeetingsRouter); app.use('/public/api/committees', publicCommitteesRouter); +app.use('/public/api/projects', publicProjectsRouter); app.use('/api/projects', projectsRouter); app.use('/api/committees', committeesRouter); diff --git a/packages/shared/src/interfaces/committee.interface.ts b/packages/shared/src/interfaces/committee.interface.ts index d0e442a29..562626d7c 100644 --- a/packages/shared/src/interfaces/committee.interface.ts +++ b/packages/shared/src/interfaces/committee.interface.ts @@ -713,7 +713,7 @@ export interface DescriptionDialogData { export interface IcalSubscribeDialogData { feedUrl: string; - committeeName: string; + name: string; } export interface EditChairsDialogData {