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
3 changes: 3 additions & 0 deletions docs/releases/unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ When a change has user-facing documentation, include a canonical tasknotes.dev l
## Fixed

- Fixed `PUT /api/tasks/:id` ignoring empty arrays for `contexts` and `blockedBy`: sending `{"contexts": []}` or `{"blockedBy": []}` now clears the corresponding frontmatter field instead of silently leaving the previous value in place. The deletion pass previously fired only on a literal `undefined`, which JSON cannot express, so HTTP clients had no way to clear these fields. Thanks to @tgrosinger for the contribution.
- (#2192) Agenda and list cards for Google and Microsoft calendar events now
show the name of the calendar the event belongs to, instead of the generic
"Calendar" label. Thanks to @martin-forge for the contribution.
33 changes: 29 additions & 4 deletions src/ui/ICSCard.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { setIcon, setTooltip } from "obsidian";
import TaskNotesPlugin from "../main";
import { ICSEvent } from "../types";
import { ICSEvent, ICSSubscription } from "../types";
import { ICSEventContextMenu } from "../components/ICSEventContextMenu";
import { formatTime } from "../utils/dateUtils";
import { ICSEventInfoModal } from "../modals/ICSEventInfoModal";
Expand Down Expand Up @@ -46,6 +46,31 @@ function renderRelatedNoteIndicator(
});
}

function getEventSourceName(
icsEvent: ICSEvent,
plugin: TaskNotesPlugin,
subscription: ICSSubscription | undefined
): string {
if (subscription?.name) {
return subscription.name;
}

const provider = plugin.calendarProviderRegistry?.findProviderForEvent(icsEvent);
if (provider) {
const { calendarId } = provider.extractEventIds(icsEvent);
const calendar = provider
.getAvailableCalendars()
.find(
(candidate) =>
candidate.id === calendarId ||
(calendarId === "primary" && candidate.primary === true)
);
return calendar?.summary || provider.providerName;
}

return plugin.i18n.translate("ui.icsCard.calendarFallback");
}

function formatTimeRange(icsEvent: ICSEvent, plugin: TaskNotesPlugin): string {
try {
if (!icsEvent.start) return "";
Expand Down Expand Up @@ -85,12 +110,12 @@ export function createICSEventCard(
card.dataset.relatedNoteCount = String(opts.relatedNoteCount);
}

// Determine subscription color and name
// Determine subscription color and source name
const subscription = plugin.icsSubscriptionService
?.getSubscriptions()
.find((s) => s.id === icsEvent.subscriptionId);
const color = icsEvent.color || subscription?.color || "var(--color-accent)";
const sourceName = subscription?.name || plugin.i18n.translate("ui.icsCard.calendarFallback");
const sourceName = getEventSourceName(icsEvent, plugin, subscription);

// Main row
const mainRow = card.createDiv({ cls: "task-card__main-row" });
Expand Down Expand Up @@ -228,7 +253,7 @@ export function updateICSEventCard(
?.getSubscriptions()
.find((s) => s.id === icsEvent.subscriptionId);
const color = icsEvent.color || subscription?.color || "var(--color-accent)";
const sourceName = subscription?.name || plugin.i18n.translate("ui.icsCard.calendarFallback");
const sourceName = getEventSourceName(icsEvent, plugin, subscription);

// Update icon color on wrapper to propagate to svg (icons use currentColor)
element.style.setProperty("--current-status-color", color);
Expand Down
90 changes: 90 additions & 0 deletions tests/unit/issues/issue-provider-calendar-source-name.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { describe, expect, it, jest } from "@jest/globals";
import { createICSEventCard, updateICSEventCard } from "../../../src/ui/ICSCard";
import type { ICSEvent } from "../../../src/types";

function createEvent(overrides: Partial<ICSEvent> = {}): ICSEvent {
return {
id: "google-primary-event-1",
subscriptionId: "google-primary",
title: "Team sync",
start: "2026-08-03T10:00:00",
end: "2026-08-03T11:00:00",
allDay: false,
...overrides,
};
}

function createPlugin() {
const provider = {
providerName: "Google Calendar",
extractEventIds: (event: ICSEvent) => ({
calendarId: event.subscriptionId.replace("google-", ""),
eventId: event.id,
}),
getAvailableCalendars: jest.fn(() => [
{
id: "person@example.com",
summary: "Personal",
primary: true,
},
]),
};

return {
app: {},
i18n: {
translate: (key: string) => (key === "ui.icsCard.calendarFallback" ? "Calendar" : key),
},
settings: {
calendarViewSettings: {
timeFormat: "24",
},
},
icsSubscriptionService: {
getSubscriptions: () => [],
},
calendarProviderRegistry: {
findProviderForEvent: jest.fn(() => provider),
},
};
}

describe("provider calendar source names on ICS cards", () => {
it("shows a provider calendar name for primary-alias Google events", () => {
const card = createICSEventCard(createEvent(), createPlugin() as any);

expect(card.querySelector(".task-card__metadata")?.textContent).toContain("Personal");
expect(card.querySelector(".task-card__metadata")?.textContent).not.toContain("Calendar");
});

it("refreshes the provider calendar name when an existing card updates", () => {
const plugin = createPlugin();
const card = createICSEventCard(createEvent(), plugin as any);
plugin.calendarProviderRegistry.findProviderForEvent.mockReturnValue({
providerName: "Microsoft Calendar",
extractEventIds: () => ({ calendarId: "work", eventId: "event-1" }),
getAvailableCalendars: () => [{ id: "work", summary: "Work" }],
});

updateICSEventCard(
card,
createEvent({ id: "microsoft-work-event-1", subscriptionId: "microsoft-work" }),
plugin as any
);

expect(card.querySelector(".task-card__metadata")?.textContent).toContain("Work");
expect(card.querySelector(".task-card__metadata")?.textContent).not.toContain("Personal");
});

it("uses the translated fallback when no subscription or provider owns the event", () => {
const plugin = createPlugin();
plugin.calendarProviderRegistry.findProviderForEvent.mockReturnValue(undefined);

const card = createICSEventCard(
createEvent({ id: "unknown-event", subscriptionId: "unknown" }),
plugin as any
);

expect(card.querySelector(".task-card__metadata")?.textContent).toContain("Calendar");
});
});
Loading