mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
Guard calendar and editor popout behavior
This commit is contained in:
parent
0e4758a448
commit
1c5f032ad0
10 changed files with 183 additions and 337 deletions
|
|
@ -17,11 +17,7 @@ import {
|
|||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||
import listPlugin from "@fullcalendar/list";
|
||||
import interactionPlugin, {
|
||||
type DropArg,
|
||||
type EventReceiveArg,
|
||||
type EventResizeDoneArg,
|
||||
} from "@fullcalendar/interaction";
|
||||
import interactionPlugin, { type EventResizeDoneArg } from "@fullcalendar/interaction";
|
||||
import multiMonthPlugin from "@fullcalendar/multimonth";
|
||||
import {
|
||||
generateCalendarEvents,
|
||||
|
|
@ -48,7 +44,7 @@ import { handleCalendarTaskClick } from "../utils/clickHandlers";
|
|||
import { TaskCreationModal } from "../modals/TaskCreationModal";
|
||||
import { CalendarEventCreationModal } from "../modals/CalendarEventCreationModal";
|
||||
import { ICSEventInfoModal } from "../modals/ICSEventInfoModal";
|
||||
import { Menu, Notice, Platform, TFile, setIcon, setTooltip } from "obsidian";
|
||||
import { Menu, Platform, TFile, setIcon, setTooltip } from "obsidian";
|
||||
import type { EventRef } from "obsidian";
|
||||
import { format } from "date-fns";
|
||||
import { createTaskCard } from "../ui/TaskCard";
|
||||
|
|
@ -67,10 +63,6 @@ import {
|
|||
CalendarRecreateNavigationState,
|
||||
shouldPreserveVisibleDateOnCalendarRecreate,
|
||||
} from "./calendarRecreateUtils";
|
||||
import {
|
||||
getDisplayedTaskLinkedGoogleEventIds,
|
||||
isDisplayedTaskLinkedGoogleEvent,
|
||||
} from "./calendarEventDeduplication";
|
||||
import { CALENDAR_END_TIME_MAX_HOUR, normalizeCalendarTimeValue } from "../utils/calendarTime";
|
||||
import type { CalendarEventData } from "../services/CalendarProvider";
|
||||
import { filterEmptyProjects, sanitizeForCssClass } from "../utils/helpers";
|
||||
|
|
@ -535,40 +527,12 @@ export function isCalendarElementReadyForSizing(
|
|||
return calendarEl.clientWidth > 0 && calendarEl.clientHeight > 0;
|
||||
}
|
||||
|
||||
type ExternalDropTaskPathSource = {
|
||||
dataTransfer?: DataTransfer | null;
|
||||
draggedEl?: HTMLElement | null;
|
||||
jsEvent?: MouseEvent | DragEvent | null;
|
||||
};
|
||||
|
||||
function normalizeDroppedTaskPath(value: string | undefined): string | undefined {
|
||||
const normalized = value?.trim();
|
||||
return normalized ? normalized : undefined;
|
||||
}
|
||||
|
||||
export function extractTaskPathFromExternalDrop(
|
||||
info: ExternalDropTaskPathSource
|
||||
): string | undefined {
|
||||
const eventDataTransfer =
|
||||
info.jsEvent && "dataTransfer" in info.jsEvent
|
||||
? info.jsEvent.dataTransfer
|
||||
: null;
|
||||
const dataTransfer = info.dataTransfer || eventDataTransfer;
|
||||
const transferredTaskPath =
|
||||
normalizeDroppedTaskPath(dataTransfer?.getData("application/x-task-path")) ||
|
||||
normalizeDroppedTaskPath(dataTransfer?.getData("text/plain"));
|
||||
|
||||
if (transferredTaskPath) {
|
||||
return transferredTaskPath;
|
||||
}
|
||||
|
||||
const draggedEl = info.draggedEl;
|
||||
return (
|
||||
normalizeDroppedTaskPath(draggedEl?.dataset.taskPath) ||
|
||||
normalizeDroppedTaskPath(
|
||||
draggedEl?.closest<HTMLElement>("[data-task-path]")?.dataset.taskPath
|
||||
)
|
||||
);
|
||||
export function isCalendarInPopoutWindow(
|
||||
containerEl: HTMLElement,
|
||||
mainWindow: Window = window
|
||||
): boolean {
|
||||
const ownerWindow = containerEl.ownerDocument.defaultView;
|
||||
return Boolean(ownerWindow && ownerWindow !== mainWindow);
|
||||
}
|
||||
|
||||
function getCalendarScrollElements(calendarEl: HTMLElement | null): HTMLElement[] {
|
||||
|
|
@ -801,6 +765,11 @@ export class CalendarView extends BasesViewBase {
|
|||
* Override to update FullCalendar size when container resizes.
|
||||
*/
|
||||
onResize(): void {
|
||||
if (this.isPopoutWindow()) {
|
||||
this.renderPopoutUnsupported();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.calendar || !this.canUpdateCalendarSize()) return;
|
||||
|
||||
this.calendar.updateSize();
|
||||
|
|
@ -1401,6 +1370,11 @@ export class CalendarView extends BasesViewBase {
|
|||
this._isRendering = false;
|
||||
return;
|
||||
}
|
||||
if (this.isPopoutWindow()) {
|
||||
this.renderPopoutUnsupported();
|
||||
this._isRendering = false;
|
||||
return;
|
||||
}
|
||||
if (!this.data?.data) {
|
||||
this._isRendering = false;
|
||||
return;
|
||||
|
|
@ -1552,7 +1526,7 @@ export class CalendarView extends BasesViewBase {
|
|||
},
|
||||
},
|
||||
...getCalendarSizingOptions(this.getEffectiveHeightMode()),
|
||||
handleWindowResize: true,
|
||||
handleWindowResize: false,
|
||||
stickyHeaderDates: false,
|
||||
locale:
|
||||
this.viewOptions.locale ||
|
||||
|
|
@ -1580,7 +1554,6 @@ export class CalendarView extends BasesViewBase {
|
|||
);
|
||||
},
|
||||
editable: true,
|
||||
droppable: true,
|
||||
selectable: true,
|
||||
...(Platform.isMobile
|
||||
? {
|
||||
|
|
@ -1624,12 +1597,6 @@ export class CalendarView extends BasesViewBase {
|
|||
eventDrop: (info) => {
|
||||
void this.handleEventDrop(info);
|
||||
},
|
||||
drop: (info) => {
|
||||
void this.handleExternalDrop(info);
|
||||
},
|
||||
eventReceive: (info) => {
|
||||
this.handleEventReceive(info);
|
||||
},
|
||||
eventResize: (info) => {
|
||||
void this.handleEventResize(info);
|
||||
},
|
||||
|
|
@ -1908,6 +1875,37 @@ export class CalendarView extends BasesViewBase {
|
|||
return isCalendarElementReadyForSizing(this.calendarEl, this.containerEl);
|
||||
}
|
||||
|
||||
private isPopoutWindow(): boolean {
|
||||
return isCalendarInPopoutWindow(this.containerEl);
|
||||
}
|
||||
|
||||
private renderPopoutUnsupported(): void {
|
||||
if (!this.calendarEl) return;
|
||||
|
||||
if (this.calendar) {
|
||||
this.calendar.destroy();
|
||||
this.calendar = null;
|
||||
}
|
||||
|
||||
this.calendarEl.replaceChildren();
|
||||
this.calendarEl.classList.add("advanced-calendar-view__calendar--popout-blocked");
|
||||
|
||||
const doc = this.calendarEl.ownerDocument;
|
||||
const noticeEl = doc.createElement("div");
|
||||
noticeEl.className = "advanced-calendar-view__popout-blocked";
|
||||
|
||||
const titleEl = doc.createElement("h3");
|
||||
titleEl.textContent = "Calendar view is unavailable in a separate window";
|
||||
noticeEl.appendChild(titleEl);
|
||||
|
||||
const messageEl = doc.createElement("p");
|
||||
messageEl.textContent =
|
||||
"Open this calendar view in the main Obsidian window. The calendar can freeze Obsidian when restored inside a separate window.";
|
||||
noticeEl.appendChild(messageEl);
|
||||
|
||||
this.calendarEl.appendChild(noticeEl);
|
||||
}
|
||||
|
||||
private resetTodayColumnWidths(): void {
|
||||
if (!this.calendarEl) return;
|
||||
|
||||
|
|
@ -2086,7 +2084,6 @@ export class CalendarView extends BasesViewBase {
|
|||
eventConfig
|
||||
);
|
||||
applyBasesSortIndexesToCalendarEvents(taskEvents, this.basesSortIndexByPath);
|
||||
const displayedTaskGoogleEventIds = getDisplayedTaskLinkedGoogleEventIds(taskEvents);
|
||||
allEvents.push(...taskEvents);
|
||||
|
||||
// Add property-based events from non-TaskNotes items
|
||||
|
|
@ -2113,10 +2110,7 @@ export class CalendarView extends BasesViewBase {
|
|||
|
||||
// Add Google Calendar events
|
||||
if (this.plugin.googleCalendarService) {
|
||||
const googleEvents = await this.buildGoogleCalendarEvents(
|
||||
relatedNoteCountsByEventId,
|
||||
displayedTaskGoogleEventIds
|
||||
);
|
||||
const googleEvents = await this.buildGoogleCalendarEvents(relatedNoteCountsByEventId);
|
||||
allEvents.push(...googleEvents);
|
||||
}
|
||||
|
||||
|
|
@ -2245,8 +2239,7 @@ export class CalendarView extends BasesViewBase {
|
|||
}
|
||||
|
||||
private async buildGoogleCalendarEvents(
|
||||
relatedNoteCountsByEventId: Map<string, number>,
|
||||
displayedTaskGoogleEventIds: Set<string>
|
||||
relatedNoteCountsByEventId: Map<string, number>
|
||||
): Promise<EventInput[]> {
|
||||
if (!this.plugin.googleCalendarService) return [];
|
||||
|
||||
|
|
@ -2257,7 +2250,6 @@ export class CalendarView extends BasesViewBase {
|
|||
// Check if this calendar is enabled
|
||||
const calendarId = icsEvent.subscriptionId.replace("google-", "");
|
||||
if (this.googleCalendarToggles.get(calendarId) === false) continue;
|
||||
if (isDisplayedTaskLinkedGoogleEvent(icsEvent, displayedTaskGoogleEventIds)) continue;
|
||||
|
||||
// Let FullCalendar handle date filtering
|
||||
const calendarEvent = createICSEvent(icsEvent, this.plugin, {
|
||||
|
|
@ -2386,46 +2378,6 @@ export class CalendarView extends BasesViewBase {
|
|||
}
|
||||
}
|
||||
|
||||
private async handleExternalDrop(info: DropArg): Promise<void> {
|
||||
this.expectImmediateUpdate();
|
||||
|
||||
const taskPath = extractTaskPathFromExternalDrop(info);
|
||||
if (!taskPath) {
|
||||
console.warn("[TaskNotes][CalendarView] External drop did not include a task path");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const task = await this.plugin.cacheManager.getTaskInfo(taskPath);
|
||||
if (!task) {
|
||||
console.warn("[TaskNotes][CalendarView] Dropped task not found:", taskPath);
|
||||
return;
|
||||
}
|
||||
|
||||
const scheduledDate = info.allDay
|
||||
? format(info.date, "yyyy-MM-dd")
|
||||
: format(info.date, "yyyy-MM-dd'T'HH:mm");
|
||||
|
||||
await this.plugin.taskService.updateProperty(task, "scheduled", scheduledDate);
|
||||
|
||||
new Notice(
|
||||
`Task "${task.title}" scheduled for ${format(
|
||||
info.date,
|
||||
info.allDay ? "MMM d, yyyy" : "MMM d, yyyy h:mm a"
|
||||
)}`
|
||||
);
|
||||
|
||||
this.calendar?.refetchEvents();
|
||||
} catch (error) {
|
||||
console.error("[TaskNotes][CalendarView] Error handling external task drop:", error);
|
||||
new Notice("Failed to schedule task");
|
||||
}
|
||||
}
|
||||
|
||||
private handleEventReceive(info: EventReceiveArg): void {
|
||||
info.event.remove();
|
||||
}
|
||||
|
||||
private refetchWhenCreatedTimeblockIsIndexed(result: TimeblockCreationResult): void {
|
||||
const hasCreatedTimeblock = (): boolean => {
|
||||
const timeblocks =
|
||||
|
|
|
|||
|
|
@ -1,47 +0,0 @@
|
|||
import type { ICSEvent, TaskInfo } from "../types";
|
||||
|
||||
type EventWithTaskInfo = {
|
||||
extendedProps?: {
|
||||
taskInfo?: Pick<TaskInfo, "googleCalendarEventId">;
|
||||
};
|
||||
};
|
||||
|
||||
export function getDisplayedTaskLinkedGoogleEventIds(
|
||||
taskEvents: EventWithTaskInfo[]
|
||||
): Set<string> {
|
||||
const eventIds = new Set<string>();
|
||||
|
||||
for (const event of taskEvents) {
|
||||
const eventId = event.extendedProps?.taskInfo?.googleCalendarEventId;
|
||||
if (typeof eventId === "string" && eventId.length > 0) {
|
||||
eventIds.add(eventId);
|
||||
}
|
||||
}
|
||||
|
||||
return eventIds;
|
||||
}
|
||||
|
||||
export function getGoogleProviderEventId(
|
||||
icsEvent: Pick<ICSEvent, "id" | "subscriptionId">
|
||||
): string | null {
|
||||
if (!icsEvent.subscriptionId.startsWith("google-")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const providerPrefix = `${icsEvent.subscriptionId}-`;
|
||||
return icsEvent.id.startsWith(providerPrefix)
|
||||
? icsEvent.id.slice(providerPrefix.length)
|
||||
: icsEvent.id;
|
||||
}
|
||||
|
||||
export function isDisplayedTaskLinkedGoogleEvent(
|
||||
icsEvent: Pick<ICSEvent, "id" | "subscriptionId">,
|
||||
displayedTaskGoogleEventIds: Set<string>
|
||||
): boolean {
|
||||
if (displayedTaskGoogleEventIds.has(icsEvent.id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const providerEventId = getGoogleProviderEventId(icsEvent);
|
||||
return providerEventId !== null && displayedTaskGoogleEventIds.has(providerEventId);
|
||||
}
|
||||
|
|
@ -215,6 +215,10 @@ const defaultProperties: ResolvedMarkdownEditorProps = {
|
|||
file: undefined,
|
||||
};
|
||||
|
||||
export function getMarkdownEditorTooltipParent(container: HTMLElement): HTMLElement {
|
||||
return container.ownerDocument?.body ?? activeDocument.body;
|
||||
}
|
||||
|
||||
class CodeMirrorEditorAdapter {
|
||||
constructor(private readonly view: EditorView) {}
|
||||
|
||||
|
|
@ -567,7 +571,7 @@ export class EmbeddableMarkdownEditor extends getEditorBase() {
|
|||
|
||||
extensions.push(
|
||||
tooltips({
|
||||
parent: activeDocument.body,
|
||||
parent: getMarkdownEditorTooltipParent(this.containerEl),
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
closeCompletion,
|
||||
} from "@codemirror/autocomplete";
|
||||
import { Extension, Prec } from "@codemirror/state";
|
||||
import { keymap } from "@codemirror/view";
|
||||
import { EditorView, keymap } from "@codemirror/view";
|
||||
import TaskNotesPlugin from "../main";
|
||||
import { NaturalLanguageParser } from "../services/NaturalLanguageParser";
|
||||
import { TriggerConfigService } from "../services/TriggerConfigService";
|
||||
|
|
@ -65,8 +65,8 @@ export function createNLPAutocomplete(plugin: TaskNotesPlugin): Extension[] {
|
|||
// Custom rendering for project suggestions with metadata
|
||||
addToOptions: [
|
||||
{
|
||||
render: (completion: Completion, _state: unknown, _view: unknown) =>
|
||||
renderProjectCompletionMetadata(completion),
|
||||
render: (completion: Completion, _state: unknown, view: EditorView) =>
|
||||
renderProjectCompletionMetadata(completion, view.dom.ownerDocument),
|
||||
position: 100, // After label (50) and detail (80)
|
||||
},
|
||||
],
|
||||
|
|
@ -167,23 +167,26 @@ export function createNLPCompletionSource(
|
|||
};
|
||||
}
|
||||
|
||||
export function renderProjectCompletionMetadata(completion: Completion): HTMLElement | null {
|
||||
export function renderProjectCompletionMetadata(
|
||||
completion: Completion,
|
||||
doc: Document = activeDocument
|
||||
): HTMLElement | null {
|
||||
const projectCompletion = completion as unknown as ProjectCompletion;
|
||||
if (!projectCompletion.projectMetadata) return null;
|
||||
|
||||
const container = activeDocument.createElement("div");
|
||||
const container = doc.createElement("div");
|
||||
container.className = "cm-project-suggestion__metadata";
|
||||
|
||||
for (const row of projectCompletion.projectMetadata) {
|
||||
const metaRow = activeDocument.createElement("div");
|
||||
const metaRow = doc.createElement("div");
|
||||
metaRow.className = "cm-project-suggestion__meta";
|
||||
|
||||
row.forEach((part, index) => {
|
||||
if (index > 0) {
|
||||
metaRow.appendChild(activeDocument.createTextNode(" "));
|
||||
metaRow.appendChild(doc.createTextNode(" "));
|
||||
}
|
||||
|
||||
const span = activeDocument.createElement("span");
|
||||
const span = doc.createElement("span");
|
||||
span.className =
|
||||
part.kind === "value"
|
||||
? "cm-project-suggestion__meta-value"
|
||||
|
|
@ -447,6 +450,7 @@ function buildProjectMetadataRows(
|
|||
}
|
||||
|
||||
function appendHighlightedText(container: HTMLElement, text: string, query: string): void {
|
||||
const doc = container.ownerDocument;
|
||||
const words = query.toLowerCase().split(/\s+/).filter(Boolean);
|
||||
if (words.length === 0) {
|
||||
container.textContent = text;
|
||||
|
|
@ -480,17 +484,17 @@ function appendHighlightedText(container: HTMLElement, text: string, query: stri
|
|||
let lastIndex = 0;
|
||||
for (const match of nonOverlappingMatches) {
|
||||
if (match.start > lastIndex) {
|
||||
container.appendChild(activeDocument.createTextNode(text.slice(lastIndex, match.start)));
|
||||
container.appendChild(doc.createTextNode(text.slice(lastIndex, match.start)));
|
||||
}
|
||||
|
||||
const mark = activeDocument.createElement("mark");
|
||||
const mark = doc.createElement("mark");
|
||||
mark.textContent = text.slice(match.start, match.end);
|
||||
container.appendChild(mark);
|
||||
lastIndex = match.end;
|
||||
}
|
||||
|
||||
if (lastIndex < text.length) {
|
||||
container.appendChild(activeDocument.createTextNode(text.slice(lastIndex)));
|
||||
container.appendChild(doc.createTextNode(text.slice(lastIndex)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -72,8 +72,6 @@ export class TaskLinkWidget extends WidgetType {
|
|||
// Store original text for reference
|
||||
card.dataset.originalText = this.originalText;
|
||||
|
||||
this.plugin.dragDropManager?.makeTaskCardDraggable(wrapper, this.taskInfo.path);
|
||||
|
||||
// Trigger update after status changes (for editor sync)
|
||||
// Listen for task updates within the card
|
||||
card.addEventListener("tasknotes:task-updated", () => {
|
||||
|
|
@ -120,12 +118,7 @@ export class TaskLinkWidget extends WidgetType {
|
|||
ignoreEvent(event: Event): boolean {
|
||||
// Ignore mouse events to prevent cursor from moving into widget
|
||||
// This keeps the widget rendered while interacting with it
|
||||
if (
|
||||
event.type === "mousedown" ||
|
||||
event.type === "click" ||
|
||||
event.type === "dragstart" ||
|
||||
event.type === "dragend"
|
||||
) {
|
||||
if (event.type === "mousedown" || event.type === "click") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,32 @@
|
|||
min-height: 0;
|
||||
}
|
||||
|
||||
.advanced-calendar-view__calendar--popout-blocked {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 240px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.advanced-calendar-view__popout-blocked {
|
||||
max-width: 420px;
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.advanced-calendar-view__popout-blocked h3 {
|
||||
margin: 0 0 8px;
|
||||
color: var(--text-normal);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.advanced-calendar-view__popout-blocked p {
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ICS Event Styles */
|
||||
.fc-event[data-ics-event="true"] {
|
||||
opacity: 0.9;
|
||||
|
|
|
|||
|
|
@ -1,91 +0,0 @@
|
|||
/**
|
||||
* Issue #1451 / #1538: Google Calendar events created by TaskNotes should not
|
||||
* render beside the same native TaskNotes task event.
|
||||
*
|
||||
* @see https://github.com/callumalpass/tasknotes/issues/1451
|
||||
* @see https://github.com/callumalpass/tasknotes/issues/1538
|
||||
*/
|
||||
|
||||
import {
|
||||
getDisplayedTaskLinkedGoogleEventIds,
|
||||
getGoogleProviderEventId,
|
||||
isDisplayedTaskLinkedGoogleEvent,
|
||||
} from "../../../src/bases/calendarEventDeduplication";
|
||||
import type { ICSEvent } from "../../../src/types";
|
||||
|
||||
function googleEvent(overrides: Partial<ICSEvent> = {}): ICSEvent {
|
||||
return {
|
||||
id: "google-primary-event-123",
|
||||
subscriptionId: "google-primary",
|
||||
title: "Synced task",
|
||||
start: "2026-01-08T09:00:00",
|
||||
end: "2026-01-08T09:30:00",
|
||||
allDay: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("Issue #1451: synced Google Calendar event duplicates", () => {
|
||||
it("extracts the provider event id without being confused by hyphenated calendar ids", () => {
|
||||
const event = googleEvent({
|
||||
id: "google-family-shared-calendar-event-id-with-hyphens",
|
||||
subscriptionId: "google-family-shared-calendar",
|
||||
});
|
||||
|
||||
expect(getGoogleProviderEventId(event)).toBe("event-id-with-hyphens");
|
||||
});
|
||||
|
||||
it("collects Google event ids only from displayed native task events", () => {
|
||||
const displayedIds = getDisplayedTaskLinkedGoogleEventIds([
|
||||
{
|
||||
extendedProps: {
|
||||
taskInfo: {
|
||||
googleCalendarEventId: "event-123",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
extendedProps: {
|
||||
taskInfo: {},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(displayedIds).toEqual(new Set(["event-123"]));
|
||||
});
|
||||
|
||||
it("filters a Google event that matches a displayed TaskNotes task export", () => {
|
||||
const displayedIds = new Set(["event-123"]);
|
||||
|
||||
expect(isDisplayedTaskLinkedGoogleEvent(googleEvent(), displayedIds)).toBe(true);
|
||||
});
|
||||
|
||||
it("does not filter unrelated events from the same Google calendar", () => {
|
||||
const displayedIds = new Set(["event-123"]);
|
||||
|
||||
expect(
|
||||
isDisplayedTaskLinkedGoogleEvent(
|
||||
googleEvent({ id: "google-primary-unrelated-event" }),
|
||||
displayedIds
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("does not filter the Google event when the matching TaskNotes task is not displayed", () => {
|
||||
expect(isDisplayedTaskLinkedGoogleEvent(googleEvent(), new Set())).toBe(false);
|
||||
});
|
||||
|
||||
it("ignores non-Google external calendar events", () => {
|
||||
const displayedIds = new Set(["event-123"]);
|
||||
|
||||
expect(
|
||||
isDisplayedTaskLinkedGoogleEvent(
|
||||
googleEvent({
|
||||
id: "microsoft-work-event-123",
|
||||
subscriptionId: "microsoft-work",
|
||||
}),
|
||||
displayedIds
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
import { EditorView } from "@codemirror/view";
|
||||
import { extractTaskPathFromExternalDrop } from "../../../src/bases/CalendarView";
|
||||
import { TaskLinkWidget } from "../../../src/editor/TaskLinkWidget";
|
||||
import TaskNotesPlugin from "../../../src/main";
|
||||
import { TaskInfo } from "../../../src/types";
|
||||
import { createTaskCard } from "../../../src/ui/TaskCard";
|
||||
|
||||
jest.mock("../../../src/ui/TaskCard", () => ({
|
||||
createTaskCard: jest.fn(() => {
|
||||
const card = document.createElement("span");
|
||||
card.className = "task-card";
|
||||
return card;
|
||||
}),
|
||||
}));
|
||||
|
||||
function createDataTransfer(values: Record<string, string>): DataTransfer {
|
||||
return {
|
||||
getData: jest.fn((type: string) => values[type] ?? ""),
|
||||
} as unknown as DataTransfer;
|
||||
}
|
||||
|
||||
describe("Issue #1811: calendar external task drops", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
Object.defineProperty(globalThis, "activeDocument", {
|
||||
value: document,
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("extracts task paths from FullCalendar external drop data", () => {
|
||||
const dataTransfer = createDataTransfer({
|
||||
"application/x-task-path": "Tasks/Drag me.md",
|
||||
"text/plain": "fallback.md",
|
||||
});
|
||||
const jsEvent = { dataTransfer } as unknown as DragEvent;
|
||||
|
||||
expect(extractTaskPathFromExternalDrop({ jsEvent })).toBe("Tasks/Drag me.md");
|
||||
});
|
||||
|
||||
it("extracts task paths from dragged task card elements", () => {
|
||||
const wrapper = document.createElement("span");
|
||||
wrapper.dataset.taskPath = "Tasks/Inline task.md";
|
||||
const child = document.createElement("span");
|
||||
wrapper.appendChild(child);
|
||||
|
||||
expect(extractTaskPathFromExternalDrop({ draggedEl: child })).toBe(
|
||||
"Tasks/Inline task.md"
|
||||
);
|
||||
});
|
||||
|
||||
it("registers inline task widgets with the calendar drag manager", () => {
|
||||
const task = {
|
||||
path: "Tasks/Inline task.md",
|
||||
title: "Inline task",
|
||||
status: "todo",
|
||||
} as TaskInfo;
|
||||
const dragDropManager = {
|
||||
makeTaskCardDraggable: jest.fn(),
|
||||
};
|
||||
const plugin = {
|
||||
settings: {
|
||||
inlineVisibleProperties: [],
|
||||
},
|
||||
dragDropManager,
|
||||
} as unknown as TaskNotesPlugin;
|
||||
const widget = new TaskLinkWidget(task, plugin, "[[Inline task]]");
|
||||
|
||||
const element = widget.toDOM({ dispatch: jest.fn() } as unknown as EditorView);
|
||||
|
||||
expect(createTaskCard).toHaveBeenCalledWith(task, plugin, [], expect.any(Object));
|
||||
expect(dragDropManager.makeTaskCardDraggable).toHaveBeenCalledWith(
|
||||
element,
|
||||
"Tasks/Inline task.md"
|
||||
);
|
||||
expect(widget.ignoreEvent(new Event("dragstart"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
import { NaturalLanguageParser } from "../../../src/services/NaturalLanguageParser";
|
||||
import { getMarkdownEditorTooltipParent } from "../../../src/editor/EmbeddableMarkdownEditor";
|
||||
import { renderProjectCompletionMetadata } from "../../../src/editor/NLPCodeMirrorAutocomplete";
|
||||
import { MockObsidian } from "../../__mocks__/obsidian";
|
||||
import {
|
||||
createCompletionPlugin,
|
||||
|
|
@ -127,4 +129,25 @@ describe("Issue #1870: user field file autosuggest NLP values", () => {
|
|||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("mounts NLP autocomplete UI in the editor document for pop-out windows", () => {
|
||||
const popoutDocument = document.implementation.createHTMLDocument("TaskNotes popout");
|
||||
const container = popoutDocument.createElement("div");
|
||||
popoutDocument.body.appendChild(container);
|
||||
|
||||
expect(getMarkdownEditorTooltipParent(container)).toBe(popoutDocument.body);
|
||||
expect(getMarkdownEditorTooltipParent(container)).not.toBe(document.body);
|
||||
|
||||
const metadata = renderProjectCompletionMetadata(
|
||||
{
|
||||
label: "Reference Note",
|
||||
projectMetadata: [[{ text: "Reference Note", searchable: true, kind: "value" }]],
|
||||
projectQuery: "Reference",
|
||||
} as never,
|
||||
popoutDocument
|
||||
);
|
||||
|
||||
expect(metadata?.ownerDocument).toBe(popoutDocument);
|
||||
expect(metadata?.querySelector("mark")?.ownerDocument).toBe(popoutDocument);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,7 +7,11 @@
|
|||
* container and has measurable dimensions.
|
||||
*/
|
||||
|
||||
import { isCalendarElementReadyForSizing } from "../../../src/bases/CalendarView";
|
||||
import {
|
||||
CalendarView,
|
||||
isCalendarElementReadyForSizing,
|
||||
isCalendarInPopoutWindow,
|
||||
} from "../../../src/bases/CalendarView";
|
||||
|
||||
function setElementSize(element: HTMLElement, width: number, height: number): void {
|
||||
Object.defineProperty(element, "clientWidth", {
|
||||
|
|
@ -85,4 +89,60 @@ describe("Issue #1873 - calendar pop-out resize guard", () => {
|
|||
|
||||
expect(isCalendarElementReadyForSizing(null, container)).toBe(false);
|
||||
});
|
||||
|
||||
test("detects calendar containers mounted in a separate window document", () => {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
expect(isCalendarInPopoutWindow(container)).toBe(false);
|
||||
|
||||
const iframe = document.createElement("iframe");
|
||||
document.body.appendChild(iframe);
|
||||
const popoutContainer = iframe.contentDocument?.createElement("div");
|
||||
if (!popoutContainer) {
|
||||
throw new Error("Expected iframe document");
|
||||
}
|
||||
iframe.contentDocument?.body.appendChild(popoutContainer);
|
||||
|
||||
expect(isCalendarInPopoutWindow(popoutContainer)).toBe(true);
|
||||
});
|
||||
|
||||
test("tears down an active calendar when Obsidian moves it to a separate window", () => {
|
||||
const iframe = document.createElement("iframe");
|
||||
document.body.appendChild(iframe);
|
||||
const popoutDocument = iframe.contentDocument;
|
||||
if (!popoutDocument) {
|
||||
throw new Error("Expected iframe document");
|
||||
}
|
||||
|
||||
const container = popoutDocument.createElement("div");
|
||||
const rootElement = popoutDocument.createElement("div");
|
||||
const calendarEl = popoutDocument.createElement("div");
|
||||
rootElement.appendChild(calendarEl);
|
||||
container.appendChild(rootElement);
|
||||
popoutDocument.body.appendChild(container);
|
||||
|
||||
const calendar = {
|
||||
destroy: jest.fn(),
|
||||
updateSize: jest.fn(),
|
||||
};
|
||||
const view = Object.create(CalendarView.prototype) as CalendarView & {
|
||||
containerEl: HTMLElement;
|
||||
rootElement: HTMLElement;
|
||||
calendarEl: HTMLElement;
|
||||
calendar: typeof calendar | null;
|
||||
};
|
||||
view.containerEl = container;
|
||||
view.rootElement = rootElement;
|
||||
view.calendarEl = calendarEl;
|
||||
view.calendar = calendar;
|
||||
|
||||
view.onResize();
|
||||
|
||||
expect(calendar.destroy).toHaveBeenCalledTimes(1);
|
||||
expect(calendar.updateSize).not.toHaveBeenCalled();
|
||||
expect(view.calendar).toBeNull();
|
||||
expect(calendarEl.textContent).toContain(
|
||||
"Calendar view is unavailable in a separate window"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue