From 04979f6f9ac49ad388b9a505d7e9bfe1bf856d7e Mon Sep 17 00:00:00 2001 From: Michael Zick <1907006+michaelzick@users.noreply.github.com> Date: Sun, 7 Jun 2026 12:17:54 -0700 Subject: [PATCH 1/3] Preserve hidden calendar events on sync --- .../src/services/planner-service-calendar.ts | 12 +++- .../src/services/planner-service.test.ts | 24 ++++++- .../timeline-board-calendar-event.tsx | 47 ++++++++++-- .../src/components/timeline-board.test.tsx | 72 +++++++++++++++++++ apps/web/src/components/timeline-board.tsx | 70 ++++++++++++++++++ 5 files changed, 215 insertions(+), 10 deletions(-) diff --git a/apps/server/src/services/planner-service-calendar.ts b/apps/server/src/services/planner-service-calendar.ts index ea4699a..c2085c8 100644 --- a/apps/server/src/services/planner-service-calendar.ts +++ b/apps/server/src/services/planner-service-calendar.ts @@ -10,6 +10,7 @@ import { buildGoogleCalendarSyncScope, recordGoogleCalendarSync, } from "./planner-service-calendar-sync.js"; +import { resolveDismissedExternalUpdatedAt } from "./planner-domain.js"; import { syncGoogleTaskCompletionStatuses } from "./planner-service-google-tasks-sync.js"; export async function syncPlannerGoogleCalendar( @@ -39,6 +40,9 @@ export async function syncPlannerGoogleCalendar( const syncedExternalEventIds: string[] = []; for (const record of records) { syncedExternalEventIds.push(record.externalEventId); + const previousEvent = record.isAppManaged + ? null + : await repository.getCalendarEventByExternalEventId(record.externalEventId, pool); await repository.upsertCalendarEvent( { externalEventId: record.externalEventId, @@ -51,7 +55,13 @@ export async function syncPlannerGoogleCalendar( scheduleBlockId: record.scheduleBlockId, rawPayload: record.rawPayload, externalUpdatedAt: record.externalUpdatedAt, - dismissedExternalUpdatedAt: null, + dismissedExternalUpdatedAt: previousEvent + ? resolveDismissedExternalUpdatedAt({ + previousExternalUpdatedAt: previousEvent.externalUpdatedAt, + previousDismissedExternalUpdatedAt: previousEvent.dismissedExternalUpdatedAt, + nextExternalUpdatedAt: record.externalUpdatedAt, + }) + : null, sourceCalendarId: record.sourceCalendarId, sourceCalendarName: record.sourceCalendarName, }, diff --git a/apps/server/src/services/planner-service.test.ts b/apps/server/src/services/planner-service.test.ts index 7eb1e1d..04ee377 100644 --- a/apps/server/src/services/planner-service.test.ts +++ b/apps/server/src/services/planner-service.test.ts @@ -1094,7 +1094,7 @@ describe("planner-service", () => { ); }); - it("clears dismissals for synced Google events on every manual sync", async () => { + it("preserves dismissed synced Google events until Google updates them", async () => { const repository = createRepositoryMock(); repository.getIntegrationToken.mockResolvedValue({ provider: "google", @@ -1107,6 +1107,15 @@ describe("planner-service", () => { }, }); repository.listCalendarEventsForRange.mockResolvedValue([]); + repository.getCalendarEventByExternalEventId + .mockResolvedValueOnce({ + externalUpdatedAt: "2026-04-06T07:30:00.000Z", + dismissedExternalUpdatedAt: "2026-04-06T07:30:00.000Z", + }) + .mockResolvedValueOnce({ + externalUpdatedAt: "2026-04-06T07:30:00.000Z", + dismissedExternalUpdatedAt: "2026-04-06T07:30:00.000Z", + }); syncGoogleCalendarWindow .mockResolvedValueOnce([ @@ -1119,6 +1128,10 @@ describe("planner-service", () => { rawPayload: {}, scheduleBlockId: null, externalUpdatedAt: "2026-04-06T07:30:00.000Z", + backgroundColor: null, + foregroundColor: null, + sourceCalendarId: "primary", + sourceCalendarName: "Primary", }, ]) .mockResolvedValueOnce([ @@ -1130,7 +1143,11 @@ describe("planner-service", () => { isAppManaged: false, rawPayload: {}, scheduleBlockId: null, - externalUpdatedAt: "2026-04-06T07:30:00.000Z", + externalUpdatedAt: "2026-04-06T08:15:00.000Z", + backgroundColor: null, + foregroundColor: null, + sourceCalendarId: "primary", + sourceCalendarName: "Primary", }, ]); @@ -1143,7 +1160,7 @@ describe("planner-service", () => { 1, expect.objectContaining({ externalEventId: "evt-1", - dismissedExternalUpdatedAt: null, + dismissedExternalUpdatedAt: "2026-04-06T07:30:00.000Z", }), fakeDb, ); @@ -1155,6 +1172,7 @@ describe("planner-service", () => { }), fakeDb, ); + expect(repository.getCalendarEventByExternalEventId).toHaveBeenCalledWith("evt-1", fakeDb); }); it("records the selected calendar set when a manual calendar sync succeeds", async () => { diff --git a/apps/web/src/components/timeline-board-calendar-event.tsx b/apps/web/src/components/timeline-board-calendar-event.tsx index 44a7566..4cd11ab 100644 --- a/apps/web/src/components/timeline-board-calendar-event.tsx +++ b/apps/web/src/components/timeline-board-calendar-event.tsx @@ -8,15 +8,38 @@ import { cn, formatTime } from "@/lib/utils"; const FALLBACK_TIMELINE_EVENT_BORDER = "#6374ad"; +type CalendarEventLane = { + laneIndex: number; + laneCount: number; +}; + +type TimelineEventStyle = CSSProperties & { + "--timeline-event-left": string; + "--timeline-event-left-sm": string; + "--timeline-event-width": string; + "--timeline-event-width-sm": string; +}; + +function getLaneLeft(laneIndex: number, laneCount: number, insetPx: number) { + const gutterPx = insetPx * 2; + return `calc(${insetPx}px + ((100% - ${gutterPx}px) / ${laneCount}) * ${laneIndex})`; +} + +function getLaneWidth(laneCount: number, insetPx: number) { + return `calc((100% - ${insetPx * 2}px) / ${laneCount})`; +} + export function TimelineBoardCalendarEvent({ date, event, + lane, isSelected, onSelectCalendarEvent, onDismissCalendarEvent, }: { date: string; event: CalendarEventView; + lane: CalendarEventLane; isSelected: boolean; onSelectCalendarEvent: (calendarEventId: string) => void; onDismissCalendarEvent: (calendarEventId: string, title: string) => void; @@ -30,10 +53,17 @@ export function TimelineBoardCalendarEvent({ const isVeryShort = new Date(event.endAt).getTime() - new Date(event.startAt).getTime() < 30 * 60 * 1000; + const laneCount = Math.max(1, lane.laneCount); + const laneIndex = Math.min(laneCount - 1, Math.max(0, lane.laneIndex)); + const isSharedLane = laneCount > 1; const titleColor = "var(--calendar-event-title)"; const borderColorSource = event.backgroundColor ?? FALLBACK_TIMELINE_EVENT_BORDER; - const cardStyle: CSSProperties = { + const cardStyle: TimelineEventStyle = { + "--timeline-event-left": getLaneLeft(laneIndex, laneCount, 8), + "--timeline-event-left-sm": getLaneLeft(laneIndex, laneCount, 12), + "--timeline-event-width": getLaneWidth(laneCount, 8), + "--timeline-event-width-sm": getLaneWidth(laneCount, 12), top: placement.top, height: placement.height, backgroundColor: "transparent", @@ -53,7 +83,8 @@ export function TimelineBoardCalendarEvent({ key={event.id} data-planner-selectable="true" className={cn( - "absolute left-2 right-2 cursor-pointer overflow-hidden rounded-[24px] border px-3 text-sm sm:left-3 sm:right-3 sm:px-4", + "absolute left-[var(--timeline-event-left)] w-[var(--timeline-event-width)] cursor-pointer overflow-hidden rounded-[24px] border text-sm sm:left-[var(--timeline-event-left-sm)] sm:w-[var(--timeline-event-width-sm)]", + isSharedLane ? "px-2 sm:px-3" : "px-3 sm:px-4", isShort ? "py-0" : "py-3 sm:py-4", )} style={cardStyle} @@ -62,9 +93,13 @@ export function TimelineBoardCalendarEvent({