Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/backend/src/controllers/calendar.controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -624,12 +624,13 @@ export default class CalendarController {
static async getIcsFeed(req: Request, res: Response, next: NextFunction) {
try {
const { token } = req.params as Record<string, string>;
const { org, calendars } = req.query as Record<string, string | undefined>;
const { org, calendars, events } = req.query as Record<string, string | undefined>;
const organizationId = org ?? '';
const calendarIds = calendars ? calendars.split(',').filter(Boolean) : [];
const eventIds = events ? events.split(',').filter(Boolean) : [];

const events = await CalendarService.getIcsFeedEvents(token, organizationId, calendarIds);
const icsContent = generateIcsFeed(events);
const event = await CalendarService.getIcsFeedEvents(token, organizationId, calendarIds, eventIds);
const icsContent = generateIcsFeed(event);

res.setHeader('Content-Type', 'text/calendar; charset=utf-8');
res.setHeader('Content-Disposition', 'attachment; filename="finishline.ics"');
Expand Down
19 changes: 16 additions & 3 deletions src/backend/src/services/calendar.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2824,7 +2824,7 @@ export default class CalendarService {
return token;
}

static async getIcsFeedEvents(icsToken: string, organizationId: string, calendarIds: string[]) {
static async getIcsFeedEvents(icsToken: string, organizationId: string, calendarIds: string[], eventIds: string[] = []) {
const user = await prisma.user.findUnique({
where: { icsToken },
include: {
Expand All @@ -2836,6 +2836,18 @@ export default class CalendarService {

if (!user) throw new NotFoundException('User', 'icsToken');

// specific events case
if (eventIds.length > 0) {
const events = await prisma.event.findMany({
where: {
dateDeleted: null,
eventId: { in: eventIds }
},
...getEventQueryArgs(organizationId)
});
return events.map(eventTransformer);
}

const userTeamIds = [
...user.teamsAsMember.map((t) => t.teamId),
...user.teamsAsLead.map((t) => t.teamId),
Expand All @@ -2845,7 +2857,7 @@ export default class CalendarService {
const calendarFilter =
calendarIds.length > 0
? [{ eventType: { calendars: { some: { calendarId: { in: calendarIds }, organizationId } } } }]
: [];
: [{ eventType: { calendars: { some: { organizationId } } } }];

const events = await prisma.event.findMany({
where: {
Expand All @@ -2856,7 +2868,8 @@ export default class CalendarService {
{ requiredMembers: { some: { userId: user.userId } } },
{ optionalMembers: { some: { userId: user.userId } } },
...(userTeamIds.length > 0 ? [{ teams: { some: { teamId: { in: userTeamIds } } } }] : []),
...calendarFilter
...calendarFilter,
...[]
]
},
...getEventQueryArgs(organizationId)
Expand Down
45 changes: 20 additions & 25 deletions src/frontend/src/pages/CalendarPage/CalendarDayCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ import EventPartialInfoView from './EventPartialInfoView';
import EditEventModal from './Components/EditEventModal';
import DeleteSeriesConfirmationModal from './Components/DeleteSeriesConfirmationModal';
import NERDeleteModal from '../../components/NERDeleteModal';
import { useDeleteEvent, useDeleteScheduleSlot } from '../../hooks/calendar.hooks';
import { useDeleteEvent, useDeleteScheduleSlot, useGetIcsToken } from '../../hooks/calendar.hooks';
import { useToast } from '../../hooks/toasts.hooks';
import { getMutedColor } from '../../utils/calendar.utils';
import { TaskClickContent } from './TaskClickPopup';
import { apiUrls } from '../../utils/urls';

export const getTeamTypeIcon = (teamTypeName: string, isLarge?: boolean) => {
const teamIcons: Map<string, JSX.Element> = new Map([
Expand Down Expand Up @@ -85,6 +86,8 @@ const CalendarDayCard: React.FC<CalendarDayCardProps> = ({
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showSeriesDeleteModal, setShowSeriesDeleteModal] = useState(false);
const [selectedEvent, setSelectedEvent] = useState<EventInstance | null>(null);
const { data: tokenData } = useGetIcsToken();
const [, setCopied] = useState(false);
const toast = useToast();

// Ref and state for dynamic event count calculation
Expand Down Expand Up @@ -160,6 +163,16 @@ const CalendarDayCard: React.FC<CalendarDayCardProps> = ({
}
};

const handleExport = (event: EventInstance) => {
setSelectedEvent(event);
if (!tokenData) return;
const feedUrl = apiUrls.icsFeed(tokenData.icsToken, tokenData.organizationId, [], [event.eventId]);
navigator.clipboard.writeText(feedUrl);
setCopied(true);
toast.success('Copied calendar with event to clipboard!');
setTimeout(() => setCopied(false), 2000);
};

const allItems = [...events, ...tasks];
const totalItems = allItems.length;

Expand All @@ -168,10 +181,6 @@ const CalendarDayCard: React.FC<CalendarDayCardProps> = ({
const [tooltipHovered, setTooltipHovered] = useState(false);
const tooltipKey = `task-${task.taskId}`;
const isLocked = lockedTooltipEventId === tooltipKey;
const tooltipHoveredRef = useRef(false);
tooltipHoveredRef.current = tooltipHovered;
const isLockedRef = useRef(false);
isLockedRef.current = isLocked;
const shouldBeOpen = isLocked || isHovered || tooltipHovered;

return (
Expand All @@ -181,9 +190,8 @@ const CalendarDayCard: React.FC<CalendarDayCardProps> = ({
marginRight={0.5}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => {
setTooltipHovered(false);
setTimeout(() => {
if (!isLockedRef.current && !tooltipHoveredRef.current) {
if (!isLocked && !tooltipHovered) {
setIsHovered(false);
}
}, 100);
Expand Down Expand Up @@ -304,10 +312,6 @@ const CalendarDayCard: React.FC<CalendarDayCardProps> = ({
event.approved === ConflictStatus.DENIED;
const bgColor = isPending ? getMutedColor(baseColor, 0.35) : baseColor;
const isLocked = lockedTooltipEventId === event.eventId;
const tooltipHoveredRef = useRef(false);
tooltipHoveredRef.current = tooltipHovered;
const isLockedRef = useRef(false);
isLockedRef.current = isLocked;
const shouldBeOpen = isLocked || isHovered || tooltipHovered;

return (
Expand All @@ -317,9 +321,8 @@ const CalendarDayCard: React.FC<CalendarDayCardProps> = ({
marginRight={0.5}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => {
setTooltipHovered(false);
setTimeout(() => {
if (!isLockedRef.current && !tooltipHoveredRef.current) {
if (!isLocked && !tooltipHovered) {
setIsHovered(false);
}
}, 100);
Expand Down Expand Up @@ -368,6 +371,7 @@ const CalendarDayCard: React.FC<CalendarDayCardProps> = ({
onClose={() => setLockedTooltipEventId(null)}
onEdit={handleEdit}
onDelete={handleDelete}
onExport={handleExport}
clickedDate={cardDate}
/>
</Box>
Expand Down Expand Up @@ -424,10 +428,6 @@ const CalendarDayCard: React.FC<CalendarDayCardProps> = ({
const [isHovered, setIsHovered] = useState(false);
const [tooltipHovered, setTooltipHovered] = useState(false);
const isLocked = lockedTooltipEventId === event.eventId;
const tooltipHoveredRef = useRef(false);
tooltipHoveredRef.current = tooltipHovered;
const isLockedRef = useRef(false);
isLockedRef.current = isLocked;
const shouldBeOpen = isLocked || isHovered || tooltipHovered;

return (
Expand All @@ -452,6 +452,7 @@ const CalendarDayCard: React.FC<CalendarDayCardProps> = ({
onClose={() => setLockedTooltipEventId(null)}
onEdit={handleEdit}
onDelete={handleDelete}
onExport={handleExport}
clickedDate={cardDate}
/>
</Box>
Expand Down Expand Up @@ -489,9 +490,8 @@ const CalendarDayCard: React.FC<CalendarDayCardProps> = ({
<Box
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => {
setTooltipHovered(false);
setTimeout(() => {
if (!isLockedRef.current && !tooltipHoveredRef.current) {
if (!isLocked && !tooltipHovered) {
setIsHovered(false);
}
}, 100);
Expand All @@ -512,10 +512,6 @@ const CalendarDayCard: React.FC<CalendarDayCardProps> = ({
const [tooltipHovered, setTooltipHovered] = useState(false);
const tooltipKey = `task-${task.taskId}`;
const isLocked = lockedTooltipEventId === tooltipKey;
const tooltipHoveredRef = useRef(false);
tooltipHoveredRef.current = tooltipHovered;
const isLockedRef = useRef(false);
isLockedRef.current = isLocked;
const shouldBeOpen = isLocked || isHovered || tooltipHovered;

return (
Expand Down Expand Up @@ -566,9 +562,8 @@ const CalendarDayCard: React.FC<CalendarDayCardProps> = ({
<Box
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => {
setTooltipHovered(false);
setTimeout(() => {
if (!isLockedRef.current && !tooltipHoveredRef.current) {
if (!isLocked && !tooltipHovered) {
setIsHovered(false);
}
}, 100);
Expand Down
56 changes: 22 additions & 34 deletions src/frontend/src/pages/CalendarPage/CalendarWeekView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ import { EventClickContent } from './EventClickPopup';
import EditEventModal from './Components/EditEventModal';
import DeleteSeriesConfirmationModal from './Components/DeleteSeriesConfirmationModal';
import NERDeleteModal from '../../components/NERDeleteModal';
import { useDeleteEvent, useDeleteScheduleSlot } from '../../hooks/calendar.hooks';
import { useDeleteEvent, useDeleteScheduleSlot, useGetIcsToken } from '../../hooks/calendar.hooks';
import { useToast } from '../../hooks/toasts.hooks';
import { useCurrentUser } from '../../hooks/users.hooks';
import { getMutedColor } from '../../utils/calendar.utils';
import { getTeamTypeIcon } from './CalendarDayCard';
import { TaskClickContent } from './TaskClickPopup';
import { apiUrls } from '../../utils/urls';

// ─── Layout constants ────────────────────────────────────────────────────────

Expand Down Expand Up @@ -164,6 +165,8 @@ const CalendarWeekView: React.FC<CalendarWeekViewProps> = ({
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showSeriesDeleteModal, setShowSeriesDeleteModal] = useState(false);
const [dragState, setDragState] = useState<DragState | null>(null);
const { data: tokenData } = useGetIcsToken();
const [, setCopied] = useState(false);

const isDraggingRef = useRef(false);
const dragStartRef = useRef<{ dayIndex: number; dayDate: Date; startMinutes: number } | null>(null);
Expand Down Expand Up @@ -374,16 +377,22 @@ const CalendarWeekView: React.FC<CalendarWeekViewProps> = ({
}
};

const handleExport = (event: EventInstance) => {
setSelectedEvent(event);
if (!tokenData) return;
const feedUrl = apiUrls.icsFeed(tokenData.icsToken, tokenData.organizationId, [], [event.eventId]);
navigator.clipboard.writeText(feedUrl);
setCopied(true);
toast.success('Copied calendar with event to clipboard!');
setTimeout(() => setCopied(false), 2000);
};

// ─── Sub-components ────────────────────────────────────────────────────────

const WeekEventBlock = ({ event, layout }: { event: EventInstance; layout: LayoutEvent }) => {
const [blockHovered, setBlockHovered] = useState(false);
const [tooltipHovered, setTooltipHovered] = useState(false);
const tooltipHoveredRef = useRef(false);
tooltipHoveredRef.current = tooltipHovered;
const isLocked = lockedTooltipEventId === event.eventId + event.scheduleSlotId;
const isLockedRef = useRef(false);
isLockedRef.current = isLocked;
const isOpen = isLocked || blockHovered || tooltipHovered;

const baseColor = getEventColor(event);
Expand All @@ -409,11 +418,7 @@ const CalendarWeekView: React.FC<CalendarWeekViewProps> = ({
enterDelay={0}
leaveDelay={200}
title={
<Box
onMouseEnter={() => setTooltipHovered(true)}
onMouseLeave={() => setTooltipHovered(false)}
onMouseDown={(e) => e.stopPropagation()}
>
<Box onMouseEnter={() => setTooltipHovered(true)} onMouseLeave={() => setTooltipHovered(false)}>
<EventClickContent
event={event}
eventTypes={allEventTypes}
Expand All @@ -423,6 +428,7 @@ const CalendarWeekView: React.FC<CalendarWeekViewProps> = ({
onClose={() => setLockedTooltipEventId(null)}
onEdit={handleEdit}
onDelete={handleDelete}
onExport={handleExport}
clickedDate={new Date(event.startTime)}
/>
</Box>
Expand All @@ -446,9 +452,8 @@ const CalendarWeekView: React.FC<CalendarWeekViewProps> = ({
data-testid="week-event-block"
onMouseEnter={() => setBlockHovered(true)}
onMouseLeave={() => {
setTooltipHovered(false);
setTimeout(() => {
if (!isLockedRef.current && !tooltipHoveredRef.current) setBlockHovered(false);
if (!isLocked && !tooltipHovered) setBlockHovered(false);
}, 100);
}}
onClick={(e) => {
Expand Down Expand Up @@ -500,11 +505,7 @@ const CalendarWeekView: React.FC<CalendarWeekViewProps> = ({
const AllDayEventBlock = ({ event }: { event: EventInstance }) => {
const [blockHovered, setBlockHovered] = useState(false);
const [tooltipHovered, setTooltipHovered] = useState(false);
const tooltipHoveredRef = useRef(false);
tooltipHoveredRef.current = tooltipHovered;
const isLocked = lockedTooltipEventId === event.eventId + event.scheduleSlotId;
const isLockedRef = useRef(false);
isLockedRef.current = isLocked;
const isOpen = isLocked || blockHovered || tooltipHovered;

const baseColor = getEventColor(event);
Expand All @@ -526,11 +527,7 @@ const CalendarWeekView: React.FC<CalendarWeekViewProps> = ({
enterDelay={0}
leaveDelay={200}
title={
<Box
onMouseEnter={() => setTooltipHovered(true)}
onMouseLeave={() => setTooltipHovered(false)}
onMouseDown={(e) => e.stopPropagation()}
>
<Box onMouseEnter={() => setTooltipHovered(true)} onMouseLeave={() => setTooltipHovered(false)}>
<EventClickContent
event={event}
eventTypes={allEventTypes}
Expand All @@ -540,6 +537,7 @@ const CalendarWeekView: React.FC<CalendarWeekViewProps> = ({
onClose={() => setLockedTooltipEventId(null)}
onEdit={handleEdit}
onDelete={handleDelete}
onExport={handleExport}
clickedDate={new Date(event.startTime)}
/>
</Box>
Expand All @@ -562,9 +560,8 @@ const CalendarWeekView: React.FC<CalendarWeekViewProps> = ({
<Card
onMouseEnter={() => setBlockHovered(true)}
onMouseLeave={() => {
setTooltipHovered(false);
setTimeout(() => {
if (!isLockedRef.current && !tooltipHoveredRef.current) setBlockHovered(false);
if (!isLocked && !tooltipHovered) setBlockHovered(false);
}, 100);
}}
onClick={(e) => {
Expand Down Expand Up @@ -593,12 +590,8 @@ const CalendarWeekView: React.FC<CalendarWeekViewProps> = ({
const AllDayTaskBlock = ({ task }: { task: CalendarTask }) => {
const [blockHovered, setBlockHovered] = useState(false);
const [tooltipHovered, setTooltipHovered] = useState(false);
const tooltipHoveredRef = useRef(false);
tooltipHoveredRef.current = tooltipHovered;
const tooltipKey = `task-${task.taskId}`;
const isLocked = lockedTooltipEventId === tooltipKey;
const isLockedRef = useRef(false);
isLockedRef.current = isLocked;
const isOpen = isLocked || blockHovered || tooltipHovered;

return (
Expand All @@ -612,11 +605,7 @@ const CalendarWeekView: React.FC<CalendarWeekViewProps> = ({
enterDelay={0}
leaveDelay={200}
title={
<Box
onMouseEnter={() => setTooltipHovered(true)}
onMouseLeave={() => setTooltipHovered(false)}
onMouseDown={(e) => e.stopPropagation()}
>
<Box onMouseEnter={() => setTooltipHovered(true)} onMouseLeave={() => setTooltipHovered(false)}>
<TaskClickContent task={task} onClose={() => setLockedTooltipEventId(null)} />
</Box>
}
Expand All @@ -638,9 +627,8 @@ const CalendarWeekView: React.FC<CalendarWeekViewProps> = ({
<Card
onMouseEnter={() => setBlockHovered(true)}
onMouseLeave={() => {
setTooltipHovered(false);
setTimeout(() => {
if (!isLockedRef.current && !tooltipHoveredRef.current) setBlockHovered(false);
if (!isLocked && !tooltipHovered) setBlockHovered(false);
}, 100);
}}
onClick={(e) => {
Expand Down
Loading
Loading